An emulator for the CHGame handheld — a WCH CH32X035G8U6 (QingKe V4C RISC-V, 48 MHz) driving a 128x128 ST7735S — written in C with SDL3. It runs natively and in the browser (Emscripten).
It is an emulator, not a simulator: it executes the real RISC-V machine code of a
compiled game (.bin, .hex or .elf, as the Arduino IDE builds them for the
CHGame board) and models the chip’s peripherals and the hardware wired to them.
Nothing in it knows anything about any particular game.
Made with the help of Claude (Anthropic).
This is an independent, unofficial piece of software, not affiliated with or endorsed by the makers of the CHGame. Where the emulator and the hardware disagree, the hardware is right and the emulator has a bug.
c.lbu,
c.lhu, c.sb, c.sh and their sp-relative forms that -march=rv32imacxw
emits). The decoding is checked against WCH’s own assembler, see
tests/xw_golden.txt. Also the QingKe specifics the board package relies on:
vectored absolute-address mtvec, the PFIC interrupt controller with
priorities, nesting and NVIC_SystemReset, the HPE hardware prologue/
epilogue that WCH-Interrupt-fast handlers need, and CSRs 0x800/0x804.src/rv32.c).Saves — flash pages a game writes are kept in <game>.sav beside it.
src/bootloader_image.c) sits in the first 12 KB, the loader writes the
metadata page at 0xF700 the way the uploader does, and the bootloader checks
the image’s CRC and jumps to it. --no-bootloader starts at 0x3000 instead.USB is present as registers only (the core’s CDC code runs, no host ever
enumerates it, so Serial output is dropped exactly as on a board with no PC
attached). The microSD slot is not emulated.
Needs CMake and SDL3 (on Windows, MSYS2’s mingw-w64-x86_64-SDL3 works; the exe
is then linked statically).
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
With the Emscripten SDK:
emcmake cmake -S . -B build_web -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build_web
Serve build_web/ over http (browsers do not allow fetch from file://).
tools/make_web.sh serve does all of it: builds, copies roms/ beside the
page with a games.json for its Games menu, and serves it on
http://127.0.0.1:8000/CHGame_Emulator.html with Python.
Programs load from:
CHGame_Emulator.html?rom=https://…/game.binCHGame_Emulator.html?rom=game.bin — a file in the same folder as the pagegames.json beside the page (["blips.bin", {"file": "znax.bin", "name": "Znax"}])
fills a Games menuSaves go to the browser’s IndexedDB.
./build/CHGame_Emulator path/to/game.bin
or drop a file on the window, or press F3.
| D-pad | Arrow keys or WASD, gamepad d-pad or left stick |
| A / B | X or Space / Z — gamepad east / south button |
| START / SELECT | Enter / Right Shift or Backspace — gamepad Start / Back |
| Help | F1 |
| Reset | F2 |
| Open | F3 |
| Pause / fast forward | P / hold Tab |
| Stats overlay (game fps, speed, MIPS, host load) | F9 |
| Screenshot | F10 |
| Fullscreen | F11 or Alt+Enter |
| Volume | + / - |
The game fps in the overlay is counted at the display: a new frame is a write window that starts higher up the screen than the one before it.
chg_headless game.bin [seconds] [out.ppm] [--press btn@sec[:dur]]... [--save]
runs without a window and reports how fast it ran, for testing and profiling.xw_test checks the XW decoder (ctest runs it).tests/sketches/chg_cal is the one the cycle model is built from: 51 assembly
loops (bench.S), each once in flash and once in RAM where it matters, whose
layout is fixed to the byte — sequential 32- and 16-bit code of two lengths,
misaligned branch targets, loads, stores and load-use against RAM, flash and
peripherals, store and load pairs, divides, multiplies, branches, and
writePixels’ byte swap as gcc builds it. It prints cycles per iteration (x100)
over USB serial and keeps them in cal_results[], which
chg_headless chg_cal.bin 3 NUL --mem <address of cal_results> 51 dumps from the
emulator, so the two can be compared line by line.
tests/sketches/chg_bench is the quick check: nine loops (SPI by hand from
flash and from RAM, SPI by DMA, ALU loops in flash and in RAM, divides, flash
table loads, the pixel byte swap, delay(100)) timed in microseconds and shown
on the screen. chg_bench.bin is prebuilt. On a real CHGame and in the
emulator they agree to within 1%.
tests/sketches/chg_flashtest exercises the flash page write the games use for
saves: green is a pass, and a white bar is added per run.
src/rv32.c |
the CPU: decoder, execution, traps, interrupts, HPE |
src/bus.c |
memory map and peripherals |
src/st7735.c |
the display controller and panel |
src/audio.c |
the buzzer, turned into samples |
src/machine.c |
reset and the run loop |
src/loader.c |
.bin/.hex/.elf loading, save files |
src/main.c |
SDL3 front end |
web/shell.html |
the page around the web build |
MIT, see LICENSE. The embedded CHGame bootloader (src/bootloader_image.c)
is Kevin Bates’ CH32SerialBoot, also MIT, with its notice in that file. SDL3
is Zlib licensed.