SearchA-ZP › Playdate (ARM)

Playdate (ARM)

2022 MIT core · CC0 games Online

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.

Visit the official site ↗

Runs on: Web browser

Playdate (ARM) Online Emulator

Play Playdate (ARM) using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Crank OrbitPlaydate (ARM)Panic PlaydateopenOpen ⛶
Crank PongPlaydate (ARM)Panic PlaydateopenOpen ⛶
Maze RunnerPlaydate (ARM)Panic PlaydateopenOpen ⛶

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.js executes 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 entry eventHandler(pd, kEventInit, 0) runs first; it calls setUpdateCallback, and each video frame the host calls that stored ARM function pointer and blits the framebuffer.
  • The HLE, in JavaScript. playdate-hle.js maps a real device address space, builds the PlaydateAPI tables in emulated RAM, and services every SDK call. Because it owns realloc, it owns the heap; because it owns the 400x240 framebuffer memory, getFrame() hands back a real address you can poke.
PieceKindWhat it does
PlaydateArm.create(canvas)factoryBuild the CPU + memory map + API tables and return the runtime.
loadImage(bytes, entry)methodLoad a flat Thumb-2 game image into FLASH and record its eventHandler entry.
bootInit() / runFrame()methodRun eventHandler(kEventInit) once, then the stored update callback once per frame.
present()methodBlit the 1-bit framebuffer (real address 0x20050000) to the canvas at 2x.
setButton / addCrank / setCrankDockedinputFeed 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 a set() that writes straight back into the core's register file.
  • Disassembly. The FLASH chip is decoded with the shared cortex-m7 decoder (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 a BKPT #index. The game's call traps into the core's onSyscall hook; 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, SRAM 0x20000000 (stack top 0x20050000), the 1-bit framebuffer at 0x20050000 (rowstride 52), and the 16 MB external FMC RAM at 0xC0000000 holding 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.