CHGame is built to feel familiar to an Arduboy developer:
- the button names are the same;
nextFrame()andpollButtons()work the same way;- you can play your game on the PC before you put it on the device.
This page covers what is the same, what is different, and what happens behind the scenes, and walks through a first sketch.
One #include <CHGame.h> brings buttons, graphics, the house palette and drawing helpers, sound, saving and the debug protocol, as Arduboy2.h does. The API reference documents every call (as the Arduboy2 library's Doxygen pages do), and the library's README is the guide; all twenty games in games/ are built on it.
| Arduboy | CHGame | |
|---|---|---|
| CPU | ATmega32U4, 8-bit, 16 MHz | CH32X035, 32-bit RISC-V, 48 MHz |
| Flash for a sketch | 28 KB | 50,944 B (the bootloader with the game menu takes 12 KB) |
| RAM | 2.5 KB | 20 KB, of which a sketch may use 18,416 B statically; the stack is 2 KB |
| Screen | 128x64, 1 bit | 128x128, 16 colours out of 4,096 (or 65,536 at 16 bpp), ST7735S over SPI |
| Buttons | 6 | 8: the Arduboy six plus START and SELECT |
| Sound | two-pin piezo | one piezo, driven by a timer |
| LED | RGB | one status LED |
| Storage | 1 KB EEPROM | no EEPROM: two 256 B flash pages for saves, and a microSD card |
| USB | upload + Serial | upload with no buttons, + Serial |
platform.md has the pins, the memory map and measured speeds. Its section C++ on this chip lists the AVR habits that do not apply here. For example, int is 32 bits, and PROGMEM is not needed.
- Colours are palette indices. The framebuffer has 4 bits a pixel: 8 KB for 128x128. A colour is a number 0-15, and the palette (16 RGB565 values) decides what each number looks like. Changing a palette entry recolours every pixel that uses it, at no cost; the games use this for fades, flashes and cycling colours.
- The screen is sent in the background.
gfx_flushAsync()starts sending the frame by DMA and returns at once.- The next frame's logic runs while the picture goes out (about 8 ms at 12 bpp).
- Before you draw again, call
gfx_wait(). - Drawing into rows that are still being sent shows as tearing on the device. In the simulator it is reported as a
BUG:line.
- Saving writes flash, not EEPROM.
- The games keep their settings and a game in progress in two flash pages that survive uploads (platform.md).
- A save takes about 1.4 ms, and must run after
gfx_wait(). - All games share the pages, and each recognises its own records by a magic number.
- There is a game menu.
- The bootloader shows the games on the SD card at power-on (sd-menu.md).
- Holding START for 3 s in any game goes back to it. The library does this for you (
chgame.startExits). - Uploading from the IDE works from the menu or from a running game, with no buttons.
chgame is a ready-made global, where an Arduboy2 sketch has arduboy. Graphics come from CHGfx (which CHGame.h includes): either the gfx_* functions the games use, or the Gfx. object, whose method names follow Adafruit_GFX and Arduboy2. The library adds the house style on top: named colours, panels, sprites, the 3x5 font, lettering.
| Arduboy2 | CHGame |
|---|---|
arduboy.begin() |
chgame.boot(); gfx_begin(GFX_DIV2, GFX_12BPP); pal::init(); (or gfx_setPalette(yours, 16)) |
setFrameRate, nextFrame, everyXFrames, frameCount |
the same |
pollButtons, pressed, anyPressed, justPressed, justReleased |
the same, plus repeat(b) for auto-repeat; notPressed(b) is !anyPressed(b) |
A_BUTTON … RIGHT_BUTTON |
the same, plus START_BUTTON, SELECT_BUTTON |
clear() |
gfx_clear(colour) |
display() |
gfx_flushAsync() (with gfx_wait() before the next drawing), or gfx_flush() to wait. gfx_flushRectAsync() sends only part of the screen. |
drawPixel, drawLine, drawRect, fillRect, drawCircle, fillCircle, drawRoundRect, fillRoundRect, drawFastHLine, drawFastVLine |
gfx_pixel, gfx_line, gfx_rect, gfx_fillRect, gfx_circle, gfx_fillCircle, gfx_roundRect, gfx_fillRoundRect, gfx_hline, gfx_vline, or Gfx. with the Arduboy names |
drawBitmap, Sprites::drawOverwrite / drawPlusMask |
gfx_blit(spr, x, y, w, h, transparent) for 4 bpp images; gfx_sprite4(spr, x, y, remap, scale) for packed sprites that can be scaled and recoloured (make them with extras/sprite4.py) |
setCursor + print |
text35(x, y, "TEXT", colour) (the 3x5 font), text35x2 (twice the size), or CHGfx's gfx_text (5x7) and gfx_setFont() for larger fonts. There is no cursor or Print stream; format numbers into a buffer with fmtInt, fmtMoney ... (snprintf costs 3.5 KB). |
BLACK, WHITE, invert() |
the house colours INK, WHITE, GOLD ... (pal::init()), or your own palette; gfx_setInverted() |
setRGBled, digitalWriteRGB |
audio::led(pattern), or digitalWrite(LED_BUILTIN, …) |
ArduboyTones, audio.on()/off() |
audio::sfx(effect) from a table of your effects, audio::blip(hz, ms), audio::music(score) / audio::melody(m), audio::setOn(on). Arduino's tone() is compiled out by the default Peripherals setting. |
EEPROM.put/get |
save::store(MAGIC, version, data) / save::load(...): up to 244 bytes, kept across power cycles and re-uploads. The core's EEPROM library does not save on this chip (commit() writes nothing). |
initRandomSeed() |
randomSeed(micros()) once the player has pressed something; for presentation, fx::rnd() (repeatable in the simulator) |
PROGMEM, pgm_read_byte, F() |
not needed: const data stays in flash and is read normally |
exitToBootloader() |
chgame.exitToMenu() (the SD menu) |
idle(), boot logo, systemButtons() |
none |
The library's example Hello is a whole sketch in sixty lines: a ball to steer, a sound effect, a counter saved in flash, the felt re-dyed with B. Its loop is the one every game uses:
void loop() {
dbg::poll(); // the simulator and tools/device.py talk through this
if (!chgame.nextFrame()) return;
// Logic.
chgame.pollButtons();
if (chgame.pressed(LEFT_BUTTON) && x > 8) x--;
// ...
if (chgame.justPressed(A_BUTTON)) audio::sfx(0);
pal::tick();
audio::update();
// Drawing.
gfx_wait(); // the last frame has been sent
pal::commit();
gfx_clear(FELT);
text35x2s(10, 9, "HELLO", WHITE);
gfx_fillCircle(x, y, 7, FX_A); // FX_A cycles through a rainbow by itself
gfx_flushAsync(); // DMA sends it while loop() runs again
}It uses 10.8 KB of flash and 11.9 KB of RAM, most of the RAM being the 8 KB framebuffer.
To build it:
- From the repository (the library and CHGfx come from
platform/board/arduino/CHGame/libraries):chgame --sketch Hello build chgame --sketch Hello upload
- Arduino IDE:
- Install the CHGame board package, 0.3.0 or later (README).
The CHGame and CHGfx libraries come with it. (With 0.2.4, copy
platform/board/arduino/CHGame/libraries/CHGfxand.../CHGameinto your sketchbook'slibraries/, and choose Tools > Optimize > Smallest + LTO.) - Open File > Examples > CHGame > Hello, then Upload. Tools > Optimize is Smallest + LTO by default.
- Install the CHGame board package, 0.3.0 or later (README).
The CHGame and CHGfx libraries come with it. (With 0.2.4, copy
The Tools menus:
- Peripherals: Game (the default) leaves out
Serial1,tone()and PWM, which saves about 4 KB. - USB: Upload only drops
Serial, which saves about 0.7 KB. Uploading still needs no buttons. (tools/device.pysets both for a release build.)
The simulator (tools/chsim) compiles a sketch with the real CHGfx drawing
code and runs it in lockstep, producing screenshots and GIFs. It drives the
sketch through the library's serial debug protocol (dbg::begin() in
setup(), dbg::poll() in loop()), so any sketch on the library can be
scripted:
chgame --sketch Hello run myscript.txt out/helloA script is a list of wait 30, tap A, hold RIGHT, snap name,
gif name 40 2 ... (the full list is at the top of
tools/chsim/chdrivelib.py). The same script runs on the board against a
debug build (chgame build --debug), so the screenshots can be
compared.
A .chgame file is how CHGame games travel, as .arduboy files do for the
Arduboy: a ZIP with the program, its title, credits, pictures and the files
it reads from the SD card. Uploaders, card builders and emulators take it as
it is.
chgame --sketch Hello export # build/Hello.chgameWhat goes in comes from the sketch's chgame.json. Every key is optional:
{"title": "HELLO", "author": "you", "genre": "Demo", "license": "MIT",
"url": "https://example.com/hello"}- Defaults fill in the rest: the folder's name in capitals for the
title, the version from
config.h, the README's first paragraph,LICENSEandNOTICE, thesdcard/folder (its tree goes onto the card's root).docs/gameplay.gifstays out of the cart; a game without adocs/cart.pnggets a picture of its title drawn over its first frame. - On a CHGame:
chgame cart deploy build/Hello.chgameflashes it. Give it--card E:\with the card mounted, and it also lands in the menu, with its SD files. - Several games in one cart:
chgame cart new MyCart.chgame Hello.chgame CHFour ..., thenadd,remove,order,set --game ID folder=PUZZLES,launch ID(the game started at power-on),background menu.png. - The format is spec/chgame.md.
Copy the game closest to yours. CHFour is small and recent, and has the full tooling: check.py, tests, scripts and the asset pipeline. Then follow CLAUDE.md's "Starting a new game". From the library you already have the frame loop's parts, the palette effects, the sound engine, saving, the debug protocol and the simulator; the game folder holds its rules, its screens, its art, its sounds and its scripts.
game-anatomy.md explains how a game is put together, and performance.md what costs time on this chip.
When the board is switched on:
- The bootloader (12 KB at 0x0000) runs first.
- It shows the SD card's games, with the installed one preselected. A starts it.
- A game is installed from the card only after it has been checked completely.
- The sketch itself lives at 0x3000 (memory-map.md, boot-flow.md).
An upload:
chgame-uploadtouches the USB serial port at 1200 baud, the sketch resets into the bootloader, and the image is written and verified in about half a second.- After an upload the sketch starts directly.
One frame of a game:
-
Logic runs at a fixed 60 Hz. If drawing fell behind, up to three logic ticks run back to back.
-
gfx_wait(), then the frame is drawn into the 8 KB framebuffer. -
gfx_flushAsync()sends it. CHGfx converts each pair of rows through the palette into a small buffer and DMA sends it at 24 MHz SPI, while the CPU is already working on the next frame.A full flush costs about 2.5-2.9 ms of CPU for the conversion. The games redraw only what changed, and draw still lettering once.
Code in RAM. Flash has three wait states and no cache, so code from flash runs at about a third of the speed of code from SRAM. Hot loops are marked RAMFUNC(name) to run from SRAM (performance.md).
The simulator compiles the same sources for the PC, with stand-ins for the SPI/DMA and the timer. Time is virtual, so scripted runs give the same frames every time. That makes "pixels are the test" possible: a change that should not alter the picture must give identical frames.