Search › A-Z › P › Playdate (ARM)
Playdate (ARM)
This runs the real Playdate hardware in your browser. Games are compiled C, executed on a from-scratch in-browser ARM Cortex-M7 (the STM32F746 at the heart of the Playdate) with the Playdate C API reimplemented in JavaScript as a high-level emulation — no proprietary Panic firmware or SDK. It has a full Thumb-2 debugger for single-stepping the ARM core, the mechanical crank (turned with the mouse wheel), and 1-bit graphics. It ships three original CC0 C games.
Runs on: Web browser
Playdate (ARM) Online Emulator
Play Playdate (ARM) using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Crank Orbit | Playdate (ARM) | Panic Playdate | open | Open ⛶ | |
| Crank Pong | Playdate (ARM) | Panic Playdate | open | Open ⛶ | |
| Maze Runner | Playdate (ARM) | Panic Playdate | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
This emulator is a two-layer, clean-room stack. The lower layer is a from-scratch ARMv7-M CPU; the upper layer is our own JavaScript implementation of the Playdate C API. No proprietary Panic firmware or SDK is used — the games are compiled against our own pd_api.h.
- The CPU.
cortex-m7.jsexecutes Thumb-2 (16- and 32-bit) plus the single-precision FPv5-SP VFP — exactly the instruction surface clang emits for an STM32F746. It is a plain-JS port of the rp2040js Cortex-M0+ core (MIT) extended up to ARMv7-M, driven one instruction at a time so the debugger can step it. - The game. A compiled-C program is a flat Thumb-2 image loaded at
0x08000000. Its entryeventHandler(pd, kEventInit, 0)runs first; it callssetUpdateCallback, and each video frame the host calls that stored ARM function pointer and blits the framebuffer. - The HLE, in JavaScript.
playdate-hle.jsmaps a real device address space, builds thePlaydateAPItables in emulated RAM, and services every SDK call. Because it ownsrealloc, it owns the heap; because it owns the 400x240 framebuffer memory,getFrame()hands back a real address you can poke.
| Piece | Kind | What it does |
|---|---|---|
PlaydateArm.create(canvas) | factory | Build the CPU + memory map + API tables and return the runtime. |
loadImage(bytes, entry) | method | Load a flat Thumb-2 game image into FLASH and record its eventHandler entry. |
bootInit() / runFrame() | method | Run eventHandler(kEventInit) once, then the stored update callback once per frame. |
present() | method | Blit the 1-bit framebuffer (real address 0x20050000) to the canvas at 2x. |
setButton / addCrank / setCrankDocked | input | Feed d-pad + A/B, the crank change/angle, and dock state to the API thunks. |
Debugger integration
playdate-arm-debug.js reads window.EMU_BOOT and hands the shared debugger a genuine ARM Cortex-M7 machine — the same core the game runs on, exposed instruction-accurately.
- Registers.
registers()reads r0-r12, SP, LR, PC, xPSR, the APSR flags N/Z/C/V/Q, FPSCR, and all 32 VFP single registers s0-s31 live each refresh; each has aset()that writes straight back into the core's register file. - Disassembly. The FLASH chip is decoded with the shared
cortex-m7decoder (debugger/src/cpus/cortex-m7.js) — the matching Thumb-2 + VFP disassembler — so the code window shows real ARM mnemonics at real addresses. - Single-step. Step calls
transport.stepInsn, which advances the core exactly one Thumb-2 instruction (starting the next update frame if the CPU is between frames). PC and the registers update after each step. - Breakpoints & watchpoints. Execution breakpoints are a PC set the run-loop checks before each instruction; write watchpoints wrap the emulated memory write path and pause when a watched address is written. Both are real, not stubs.
- Memory map. Four chips read the real address space side-effect-free: FLASH (game image, disassembled), SRAM (the PlaydateAPI tables + C stack), the 400x240 1-bit FRAMEBUFFER (bits/hex/tiles), and the 16 MB FMC HEAP.
Architecture
The Playdate is a small yellow handheld by Panic built on an STM32F746 (ARM Cortex-M7): a 400x240 one-bit reflective screen with no backlight, a d-pad and A/B buttons, a speaker, and its signature crank that folds out and reports an absolute angle. Games are written in C (or Lua) against the playdate API.
- Trap-dispatch HLE. Every API function pointer in emulated RAM points at address
0xF0000000 + index*2, where the stub page holds aBKPT #index. The game's call traps into the core'sonSyscallhook; the JS handler reads the arguments per the ARM hard-float ABI (r0-r3 then stack, floats in s0-s15), performs the service, writes the result to r0/s0 and returns with PC = LR. - Real address map. FLASH
0x08000000, SRAM0x20000000(stack top0x20050000), the 1-bit framebuffer at0x20050000(rowstride 52), and the 16 MB external FMC RAM at0xC0000000holding the heap. The exact firmware framebuffer/heap addresses are not public, so plausible in-region addresses are chosen and documented in README.txt. - Crank. Driven by the mouse wheel, an on-screen dial you drag, and a dock/undock toggle, surfaced as
getCrankChange/getCrankAngle/isCrankDocked. - Coverage. The commonly-used system / graphics / display / sound / file surface is implemented; sprites, the nested SDK sound tree, JSON and asset-file loading are simplified or omitted. See README.txt for the exact implemented-vs-stubbed list and the honest ISA gaps.
Sound
Sound uses Pattern S (an authored synth): the HLE's sound API is our own small polyphonic engine in playdate-hle.js. A game creates a synth, sets its waveform (sine / square / saw / triangle / noise), and calls synthPlayNote(synth, freqHz, vol, seconds), which pushes a voice with a linear decay envelope. Each video frame the mixer produces exactly Math.round(EmuAudio.sampleRate / 60) interleaved-stereo Int16 sample pairs at the context rate (so no resampling) and calls EmuAudio.push().
Mute contract. window.EMU_BOOT.transport.isMuted() / setMute() delegate straight to EmuAudio; it starts muted (browsers block audio before a gesture) and the on-page Sound button unmutes from a real click.
Sound. Sound is fully supported and plays whatever the software makes. It starts muted, because browsers block audio before a gesture, so click the Sound button to hear it.