chgame_emulator

an emulator for bateske's chgame


Project maintained by joyrider3774 Hosted on GitHub Pages — Theme by mattgraham

CHGame Emulator

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).

Not an official CHGame project

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.

What is emulated

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).

Building

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

Web

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, then serves the repository on http://127.0.0.1:8000/build_web/CHGame_Emulator.html with Python (so ?rom=../roms/bateske/CHChess.bin works). The web build carries the games listed in web/games.json (its Games menu); other games are published on the games site only, which has its own copy of the emulator. Programs load from:

Saves go to the browser’s IndexedDB.

Running

./build/CHGame_Emulator path/to/game.bin

or drop a file on the window, or press F3.

Command line

CHGame_Emulator [program] [options]

The program is a .bin, .hex, .elf or .chg (a CHG package for the SD menu).

Option What it does
--bootloader file.bin another bootloader instead of the built-in one, e.g. one of CHGame’s SD menus (tools/build_bootloaders.sh builds them into bootloaders/); without a program the flash starts erased
--no-bootloader start the program at 0x3000, no bootloader
--sd folder\|card.img the microSD card: a folder (made into a FAT32 card) or a card image (default: the sdcard folder next to the emulator)
--sd-size MB the size of a card made from a folder (default: room for its files)
--no-sd an empty card slot
--piezo the piezo’s sound (F7 switches)
--no-piezo the bare pin signal (the default)
--integer-scale whole multiples of the screen only, for this run
--no-integer-scale fill the window, for this run (F8 switches between the two and remembers the choice)
--scale N the window’s first size, N times the 128x128 screen (1-10, default 4)
--help, -h list the options and exit (in a message box on Windows)
   
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
Back to the bootloader (game menu) F4
Turn the screen 90 degrees clockwise; the d-pad turns with it (kept per game) R — gamepad north button
Pause / fast forward P / hold Tab
Stats overlay (game fps, speed, MIPS, host load) F9
Scaling: fill the window / whole multiples only; remembered (--integer-scale, --no-integer-scale for one run) F8
Screenshot F10
Record a GIF; press again to stop and choose where to save it F6
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.

Another bootloader: the SD game menu

./build/CHGame_Emulator --bootloader bootloaders/chgame_sdboot.bin            # the bootloader alone
./build/CHGame_Emulator --bootloader bootloaders/chgame_sdboot.bin game.bin   # with a program installed
./build/CHGame_Emulator chg/CHCHESS.CHG                                       # a package, directly

tools/build_bootloaders.sh builds that bootloader from CHGame’s source (the c:/github/CHGame clone, with the board package’s toolchain) and puts the binary in bootloaders/chgame_sdboot.bin. tools/make_chg.py packs every ROM in roms/ as a .CHG package into chg/ (8.3 names, chg/INDEX.TXT lists them; build_roms.sh runs it at the end): copy them into a card’s GAMES folder. The emulator also opens a .CHG directly, like a .bin, after the same checks the bootloader makes.

--bootloader file.bin puts another bootloader (at most 12 KB) at 0x0000 instead of the built-in one, which stays the default. It was made for CHGame’s SD menu bootloader (release/chgame_sdboot.bin): at power-on it lists the /GAMES/*.CHG packages on the microSD card (tools/chgpack.py in CHGame makes them from a game’s .bin), and A installs the chosen one into the program flash, the same way a USB upload writes it, then starts it.

Without a program the emulator starts like a board with only that bootloader on it: the program flash is erased. Everything the bootloader flashes, and whatever the games save afterwards, is kept in <bootloader>.sav beside the bootloader file, so the installed game is still there next time.

F4 goes back to the bootloader the way a game returns to the menu (a casino game in CHGame when START is held for 3 s): a software reset with no boot request pending. With the built-in bootloader it simply restarts the program. chg_headless takes the same option, - for no program, and --back sec for F4.

The microSD card

./build/CHGame_Emulator game.bin --sd folder|card.img [--sd-size MB]   # --no-sd: an empty slot

By default the card is the folder sdcard next to the emulator (on macOS next to the .app bundle, not inside it). A folder’s files go on a FAT32 card built in memory; what the program writes, creates or deletes goes back into the folder every few seconds and when the emulator closes. An image file is a whole card (as a card reader dumps it), read and written in place, and is created empty (256 MB, or --sd-size) when it does not exist. chg_headless --sd-make card.img [folder] [MB] writes a card image, empty or holding a folder’s files. In the browser the card is kept in the browser, with buttons to add files or a folder, empty it, and download it as an image.

Manage… opens the card manager (sdcard.html?card=chgame, also usable on its own). The card comes out of the emulator, and the game pauses, while it is open. It lists the card’s folders and files. Files download one at a time, a folder or the whole card as a .zip. Files, folders, a .zip or a FAT card image can be uploaded (or dropped on the list) into the folder shown. Folders can be made, and files and folders deleted. Done puts the card back in.

?sd=card.zip or ?sd=card.img (a file beside the page or a URL) fills the card from a zip file or a FAT12/16/32 card image before the first program starts. It does so once: as long as that file stays the same, later visits keep what the programs wrote to the card. Emptying the card makes the next visit fill it again. Files put on a card that was never filled (with the card manager on its own, before any program ran) stay: the image’s files are added beside them. Without ?sd=, a sdcard.zip (or else a sdcard.img) beside the page is used the same way. The page only asks the server for the file’s size and date on each visit, and downloads it only when they changed.

FileBrowser needs Arduino’s SD library (arduino-cli lib install SD); tools/build_roms.sh builds it against a copy with the CH32 pin-map fix.

Tools

Calibration

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.

Layout

   
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

Licence

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.