Thumby
Thumby is the TinyCircuits keychain game console emulated down to the chip: a real Raspberry Pi RP2040 (a dual ARM Cortex-M0+) running the official MicroPython firmware with the open-source Thumby game API, and real games loaded from a littlefs flash filesystem. The 72x40 one-bit OLED is rendered from the SSD1306 command stream the firmware clocks over hardware SPI, the d-pad and A/B buttons drive the real GPIO pins, and the piezo buzzer is synthesised. Because the whole chip is ordinary JavaScript it plugs into this site's shared debugger: single-step the Cortex-M0+, read and write r0-r15 and xPSR, disassemble the XIP flash, and set breakpoints and write-watchpoints over the real RP2040 memory map.
Runs on: Web browser
Thumby Online Emulator
Play Thumby using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Thumby Bounce (demo) | Thumby | TinyCircuits Thumby | open | Open ⛶ | |
| Silicon8 (CHIP-8) | Thumby | TinyCircuits Thumby | open | Open ⛶ | |
| Pogo Picross | Thumby | TinyCircuits Thumby | open | Open ⛶ | |
| White & Black | Thumby | TinyCircuits Thumby | open | Open ⛶ |
Chips
Notes
Embedding
The Thumby is a Raspberry Pi RP2040 (dual Cortex-M0+) with a 72x40 1-bit OLED, six buttons and a piezo. It runs on rp2040js — Uri Shaked's complete in-browser RP2040 SoC (two Cortex-M0+ cores, XIP flash, 264 KB SRAM, SIO/GPIO, timers, SPI, PWM, UART, USB, PIO), bundled once to a file-safe browser global (window.rp2040js) with esbuild and used unmodified. We build the Thumby board around it and own the run loop so the debugger can drive it.
Boot. Load the official MicroPython UF2 into mcu.flash, then apply a littlefs2 filesystem image at flash 0x000A0000 (block 4096, 352 blocks — the RP2040 MicroPython layout) containing the open-source Thumby API (/lib), the selected game (/Games) and a /main.py that powers on the panel and imports the game. MicroPython runs main.py on boot exactly as on hardware:
var sim = new rp2040js.Simulator();
var mcu = sim.rp2040;
mcu.loadBootrom(rp2040js.bootromB1);
loadUF2(micropythonBytes, mcu); // official MicroPython for RP2040
var board = THUMBY.buildBoard(mcu, rp2040js); // SSD1306 + buttons + buzzer
board.applyFS(THUMBY_FS[game]); // littlefs image -> flash @ 0xA0000
mcu.core.PC = 0x10000000;
The display is decoded from the real SPI stream. The Thumby's ssd1306.py clocks the framebuffer out of hardware SPI0 (SCK=GP18, MOSI=GP19), with DC=GP17 selecting command vs data. rp2040js models the PL022 as mcu.spi[0]; we hook its onTransmit to capture every byte, run the SSD1306 command/data protocol (honouring the 72x40 GDDRAM window — columns 28..99, pages 0..4) and keep a live 360-byte framebuffer that is drawn to the canvas each frame. Buttons drive mcu.gpio[n].setInputValue (active-low) and the buzzer is read straight off PWM slice 6.
Debugger integration
The plug-in (thumby-debug.js) describes the RP2040 to the shared debugger and reuses the ARM decoder; rp2040js is not patched:
- Reused decoder. The Cortex-M0+ runs ARMv6-M Thumb, a subset of the existing
/debugger/src/cpus/cortex-m7.jsdisassembler. It is reused as-is under the display name "ARM Cortex-M0+ (RP2040)"; no new decoder was written. - Registers are read live each refresh: r0-r12, SP, LR, PC, xPSR, the APSR flags N Z C V, and PRIMASK / nPRIV — each with a
set()that writes straight back intocore.registers/ the core's flag fields. - Memory is three chips, all read side-effect-free off the typed arrays with their real base addresses: the XIP flash at
0x10000000(disassembled — this holds MicroPython and, from0xA0000, the littlefs game filesystem), the SRAM at0x20000000, and the 16 KB boot ROM at0x00000000(also disassembled). - Single step is one
executeInstruction(). Breakpoints are host-side checks ofcore.PC; write-watchpoints wrapmcu.writeUint8/16/32and pause the loop the moment a watched bus address is written.
Architecture
The Thumby (TinyCircuits, 2021) is a keychain-sized MicroPython game console built on a single microcontroller:
- RP2040 — dual ARM Cortex-M0+ (ARMv6-M Thumb) at 125 MHz, 264 KB SRAM at
0x20000000, executing in place from QSPI flash at0x10000000, with a 16 KB boot ROM. - SSD1306 72x40 OLED over hardware SPI0 (SCK=GP18, MOSI=GP19, DC=GP17, CS=GP16, RES=GP20). The 72x40 panel is a window into the SSD1306 GDDRAM: the driver sets column range 28..99 and page range 0..4, and streams 360 bytes (72 columns x 5 pages of 8 vertical pixels) per frame.
- Six buttons on their real GPIO pins, active-low with pull-ups — LEFT=GP3, RIGHT=GP5, UP=GP4, DOWN=GP6, A(right)=GP27, B(left)=GP24.
- Piezo buzzer on GP28 (PWM slice 6, channel A), driven by MicroPython's
machine.PWM.
The firmware is the official MicroPython for RP2040 plus the open-source Thumby API (GPL-3.0) in the flash filesystem — real, unmodified code booting through the same path as hardware. Because it is real ARM machine code on a real SoC model, every instruction single-steps in the debugger. Pin map from the Thumby MicroPython library (thumbyHardware.py / ssd1306.py).
Honest limits. rp2040js is cycle-approximate, not cycle-exact; the second core, PIO and DMA are present but barely exercised by these games; MicroPython runs at interpreted speed so games play a little below hardware frame-rate; and the buzzer tone is recovered from the PWM registers and synthesised as a square wave (single-voice, as the hardware is).
Sound
Pattern S (authored, from the PWM registers). The Thumby has no sound chip — its piezo is driven directly by one machine.PWM channel on GP28 (PWM slice 6, output A). Each video frame the board reads that channel's live registers from rp2040js (CSR enable bit, TOP, CC compare, DIV fractional divider) and recovers the tone: freq = 125 MHz / (divider x (TOP+1)), active while the compare (duty) is non-zero. That matches exactly what thumbyAudio.py asks for — audio.play(freq, ms) sets pwm.freq(freq) and a 50% duty, then a one-shot Timer zeroes the duty.
A phase-continuous square wave at that frequency is written to the shared sink as exactly round(EmuAudio.sampleRate/60) interleaved-stereo Int16 samples per frame. The mute contract is the standard one: transport.setMute / transport.isMuted delegate to EmuAudio, and it starts muted until the Sound button's real click. The default boot is a real game (Pogo Picross); sound is fully supported and plays when a game drives the buzzer. A self-contained Thumby Bounce demo config is also available, which beeps off every wall.