Blog ·
Doom on a chip with 2 KB of RAM: a RISC-V computer made of serial memory
Sergey Alekseev
The WCH CH32V003 is a tiny RISC-V microcontroller: 48 MHz, 16 KB of flash, 2 KB of RAM, in a 20-pin package. Doom needs megabytes. So the chip doesn’t try to run Doom. Its firmware is a small interpreter for a larger RISC-V computer, and that computer’s memory is two serial chips on the same board: an 8 MB SPI PSRAM for its RAM and a 16 MB SPI flash for its program. It is the old “Linux on an 8-bit AVR” trick, applied to Doom.
It is slow, and we knew it would be. We built it anyway because it is the clearest test of whether PCBKit simulates buses and chips rather than programs: if the chip model, the SPI timing and the memory models are right, Doom boots; if any of them is wrong, it doesn’t.

v003-emu: a CH32V003, an 8 MB PSRAM and a 16 MB flash behind a Nokia 5110 screen.
How a 2 KB chip runs a bigger computer
The board is v003-emu. Its firmware fills 15.6 KB of the chip’s 16 KB of flash:
- An RV32IMAC interpreter, with a write-back cache of 32 lines of 16 bytes in the chip’s 2 KB of RAM.
- The guest program is Doom built for
pcbkit-virt, PCBKit’s own plain RISC-V machine, with the game’s data linked in. It sits in the flash; its code and constants are read where they lie, and its writable part is copied into the PSRAM at boot. - Every guest load or store goes through the cache. A miss is one SPI transaction (a command, a 24-bit address and 16 bytes at 24 MHz): 6.7 µs with the chip selected, inside the PSRAM’s 8 µs limit.
- Each guest frame (320 × 200, 8-bit) is scaled down to 84 × 48 with ordered dithering and sent to the screen’s PCD8544 controller, which shares the SPI bus with both memories.
The CH32V003’s own core is RV32EC: 16 registers and no multiply instruction, so the interpreter does the guest’s multiplies in software.
The same trick, where it is watchable
The STM32F411, the chip of the “Black Pill” boards, is a Cortex-M4F at 100 MHz with 128 KB of RAM and no memory-mapped QSPI. Not enough RAM for Doom, so f411-psram uses the same design: the same PSRAM and flash on SPI, the same interpreter, a much bigger cache.

f411-psram: an STM32F411 runs a RISC-V computer built from an SPI PSRAM and an SPI flash.
| v003-emu | f411-psram | |
|---|---|---|
| Processor | CH32V003, RV32EC, 48 MHz | STM32F411, Cortex-M4F, 100 MHz |
| RAM / flash on chip | 2 KB / 16 KB | 128 KB / 512 KB |
| Interpreter firmware | 15.6 KB | 19.5 KB |
| Cache | 32 lines × 16 bytes | 2048 lines × 32 bytes (64 KB) |
| SPI clock, one miss | 24 MHz, 6.7 µs | 50 MHz with fast read, 5.9 µs |
| Screen | Nokia 5110, 84 × 48 mono | 2.0″ IPS, 240 × 320, colour |
| Title screen, device time | about 200 s | about 40 s |
| After that | a frame every several seconds | a few frames a second |
| Build in the page | 5.9 s | 6.1 s |
| Simulation in the browser | 0.27x real time | 0.21x real time |
The device times are from PCBKit’s simulator: instruction-accurate on the host chip, with the memories’ SPI timing. They are simulated seconds on the board, not seconds you wait in the browser; in the page the simulation runs at about a quarter of the real chip’s speed (the last row, a dev-profile WebAssembly build). Neither board has been built yet, so none of this is measured on silicon.
Why so slow? Doom on pcbkit-virt runs about 1.1 million guest instructions a frame. On the
CH32V003 every one of those is decoded by a 48 MHz chip without a multiplier, and every cache miss
waits for the SPI bus. It is a demonstration board, not a game console.
Two programs, two chips to flash
Press Build & run in the Laboratory and the page builds both programs: the interpreter for the
host chip (emu/) and Doom for the computer it emulates (doom/). It puts the second into the
flash model and runs the board. To put them on a real board you export both images. The CH32V003
takes its interpreter through WCH’s one-wire debug port with a WCH-LinkE. For the flash, hold MENU
while switching on: the interpreter lets go of the SPI bus, and a USB SPI programmer with a SOIC-8
clip writes the guest in place. The F411 takes its firmware over USB-C through the ROM’s DFU
loader.
What we threw away first
The guest computer, pcbkit-virt, runs on PCBKit’s own RV32 core. It didn’t start that way.
An off-the-shelf interpreter. Our first browser computer was rv32emu (MIT) inside its own bounded WebAssembly instance: a 37,179-byte module, every memory access bounds-checked, unknown services faulting. In Chrome it ran 120 frames of a small control program, 741,989 instructions, in 0.0229 s. But its fast translated-block mode overshot requested instruction budgets by up to 32 instructions, which breaks deterministic simulation next to other components, and its step-by-step mode needed patches to work at all. Its pace stepped per instruction was about 32 million instructions a second.
Our own core. The RV32IMAC core in PCBKit’s machine framework passes 75 of 75 applicable
riscv-tests, validates CoreMark at 2.72 CoreMark/MHz and runs at 183 million instructions a second
natively and 135 million as WebAssembly under Node, with a cache of decoded instructions. On a
100 MHz guest, Doom on pcbkit-virt gives a frame every 28.6 ms (35 fps, every game tick), 5.1x
faster than real time natively.
An STM32 engine that answered zero. We also tried an open-source STM32F103 simulator (MIT) in WebAssembly. A real binary booted, ran timer interrupts, toggled GPIO and sent UART bytes, natively and in WebAssembly with identical results. But the timer configured for a 1,000-cycle period ticked every 1,006 cycles. The UART reported bytes immediately where a real frame at that baud rate takes 6,250 cycles. And reading the USB peripheral, which wasn’t implemented, quietly returned zero as if it worked. For a game that might be fine. For firmware you are about to flash onto a board you ordered, a model that answers zero hides exactly the bug you wanted to find.
PCBKit’s chip models follow the opposite rule: what firmware does that would misbehave on silicon stops the model with the reason, and what isn’t modelled is a stop saying so, never a guess.
Licences
The Doom engine on both boards is sysprog21/doom_riscv,
id Software’s Linux Doom source under the GNU GPL v2, and the boards and their interpreters are
GPL-2.0 too. They live in github.com/pcbkit-io/boards.
The game data, id Software’s shareware DOOM1.WAD, is not in the repository: each design names it
by URL and SHA-256 and the build downloads and checks it.
What is next
The obvious next thing is to order these two boards and see whether a real CH32V003 reaches the title screen in about the time the simulation says. Our models charge the SPI time of every transaction, so this is the board where we expect the simulation to be closest to the desk. After that, the browser speed: translating hot guest code into WebAssembly instead of interpreting it, which matters twice here, since the browser interprets a chip that itself interprets a computer.