

CH32Console
Despite memory limitations, it runs games with full 16-bit color\ graphics at 30 FPS (It can technically hit 60 FPS in all example games, but I designed them for 30, so I locked it there.) It supports either terminal…
- Size
- 82.7 × 30.5 mm
- Copper layers
- 2
- Thickness
- 1.6 mm
- Parts
- 37
- Nets
- 35
- Pads
- 130
- Tracks, vias
- 178, 35
- Pours
- 2
- Routed
- Yes
- KiCad
- KiCad 9.0 (file 20241229)
- Created
- 11 Feb 2026
- Updated
- 11 Feb 2026
Overview
CH32Console is an open-source printed circuit board by max7d3, from the max7d3/CH32V003-MiniGameConsole repository on GitHub, published under the Apache-2.0 licence. It is a 2-layer board of 82.7 × 30.5 mm with 37 parts on 35 nets, routed with 178 tracks and 35 vias.
It is built around the TL3305AF160QG. Its parts include 8 capacitors, 7 resistors, 3 ICs, 1 connector and 1 switch. It belongs with the RISC-V boards of this library.
Main components
1README
README.mdCH32V003 Mini Game Console

Introduction
Hi! I present to you a little project I have been working on... honestly, for a few years (on and off). But it's finally finished! This Mini Game Console uses one of the cheapest MCUs available, the CH32V003 (chosen when the project began in mid-2023), paired with also one of the cheapest color LCD display featuring a mind-blowing 80x160 resolution.
Despite memory limitations, it runs games with full 16-bit color* graphics at 30 FPS (It can technically hit 60 FPS in all example games, but I designed them for 30, so I locked it there.) It supports either terminal grid and sprite graphics, has simple sound system and even option to save high score data.
It has simple "game engine" and each game can be kind of treated as independent program. There is API for buttons, sound and graphics, but it's all low-level anyways, so making games still require some knowledge how it all works overall.

Advanced game like Dino Run takes about ~2 kB in code alone. But for simple one like snake it's around 0.5 kB. With the example games and lto enabled there is about 1 kB of FLASH memory left, as well there are two free GPIOs. I didn't have idea what else to add despite e.g. flashlight functionality, but the project still can be expanded.
*Single sprite can have 3 colors + transparency, but every sprite can use different palette.
Specifications
- MCU: CH32V003F4P6 (16 kB FLASH, 2 kB RAM, 48 MHz RISC-V)
- Display: ST7735 80x160 IPS LCD (SPI)
- Audio: PWM Buzzer
- Input: 5 Buttons
- Current Games:
- Snake
- Pong
- Jump Bird
- Dino Run
- Other Features:
- High Score Saving
- Flashlight Mode
- Low Battery Detection
Showcase Video
Detailed Overview
Below I described the most important functionalities in more detail.
Graphics System (termGFX)
Obviously, there is no way to fit a full frame buffer for this LCD into the MCU's available RAM. To solve this, graphics rendering is based on 8x8 blocks, and the main mode of operation is simple terminal-like graphics.
Terminal Graphics
Instead of storing whole picture in the memory, MCU uses map of chars and their color on the grid. Each block contains 2 bytes of information: character, 4-bit background color, 4-bit foreground color. On 80x160 display there are 200 blocks, so whole "frame buffer" for terminal uses 400 bytes of memory.
However, the buffer cannot be send to the LCD in format like that. That's why next step is rendering each block into little 8x8 frame buffer and drawing it on the screen in the right position. Rendering function checks which character to draw and in which colors, then renders the bitmap in the output buffer. Next the buffer is send to the LCD.
That's the simple version. To achieve the higher frame rate there are further optimizations.
First, there is a map of 'dirty' blocks. An array of variables that act as bit fields, where '1' means change and '0' means lack of changes. All the exposed drawing functions write into that map. When the tGFX_Update is called to send the frame to LCD, the rendering function reads the map and skip all the blocks that didn't change. It also resets state of all the drawed blocks to '0'.
Second, the DMA of MCU is utilized to send data to LCD. And when data is being send to the LCD over SPI, the CPU switches output buffer and begin to render the next block. So we trade x2 RAM usage for almost x2 throughput increase.
Overall the terminal graphics in used configuration uses ~700 bytes of RAM. The usage can be further decreased by e.g. using only B/W colors. It can be also increased if we want more colors of text. But in this project I decided on compromise of 4-bit foreground/background colors of text.
This mode can be already used to make simple games. The Snake game implementation uses only this grid terminal graphics and custom characters. But for other games the 'free' graphics is needed, something that doesn't need to be aligned with the grid.
Sprite Graphics
Sprite system is based on map similar to the map of blocks to update. This time however, the '1' means block containing sprite. That's the simple part. From there it gets a bit complicated (at least in code), but the overall idea is pretty straightforward.
First step, or more like, step zero: sprites allocation. The code requires to know how many sprites are used, as well as information about them: position, size and color palette. Because when step one occurs and the block with sprite is processed, first thing that code does is iterating through all the allocated sprites and checking which one, and which part of it is on the current block.
When it finds out everything, the next step is rendering the sprite. To save memory, the sprites are encoded using 2-bit colors, so there is a bit of bit shuffling to decode them. The real colors are taken from palette attached to the sprite descriptor. Because color 0 is transparency, there are only 3 colors per sprite. But because of transparency, many sprites can be stacked on each other to achieve more colors.
And that's it. The efficiency of this solution heavily depends on what sprites and how many of them are used. If sprites are very large, probably stacking three or four on each other would make it... Well, maybe not so much lagging as just weird to look at, because of the block based rendering. With very small sprites, we can use a lot more, but sooner or later the boundary checking loop with o(n) complexity would become a problem.
There is probably a way to make it better, but it works fine for the games I wanted to make.
The "game engine"
Calling this a 'game engine' might be a bit of an exaggeration. In reality, engine.c is primarily a system that manages function callbacks via a timer. It allows the current running 'program' (game) to be switched by simply changing the callback.
The current running 'program' is changed by changing the callback.
It also contains the common 512 bytes of memory that all the games share. A simple array that allows for simple dynamic allocation. The whole system is minimal, basically game can only allocate bytes, check how much memory is left, and clear it all.
Menu
The main menu simply changes the engine callback to the entry point of selected game/utility. Besides displaying the list of games, it also displays a low-battery warning in the footer. It can also toggle sound generation.
Going back to the main menu is a matter of changing the callback back to main menu main function. And that entirely depends on whichever 'program' is being executed right now.
Games
Each game act as indepent program with its own setup function and loop function. Every game has also struct gs (game state) that holds all the variables. The struct is actually a pointer mapped on the common memory pool. So, each game uses only 4 bytes of statick RAM for this pointer.
When I'm looking at this system now, it could be done better. Everything could just happen on stack. But it works, and I wanted to finally call this project finished, so I left it like that.
I made four example games listed below:
Snake

Pong

Jump Bird (Flappy Bird clone)

Dino Run ("No Internet" Chrome game)

For gameplay watch the video (1:12): https://youtu.be/mhWHda4XlT0?si=jpvWHffCmz2jvb9t&t=72
Game Over Screen
To save memory, the game over screen is also a 'game' that gets turn on when the game is over. The game that was running can set which button brings back main menu, what score to display etc.
High Scores System
The console has a persistent memory to save high scores in games. It utilizes last page of CH32V003 flash. The pages of memory are 64 bytes in size, so we don't have to sacrifice lot of internal storage for that functionality.
The memory can be wiped by user. The scores are obviously saved only if current score is higher than previous one in memory.
Other
Sound
Sound is generated using PWM and buzzer. Game can either play single tone for specific amount of time, or use a buffer of frequency and time values that will play in sequence automatically.
Input
Buttons are debounced and reading of their state is based on pooling. Games can also read raw state of the GPIO inputs and don't wait for debaunced input.
Battery Level Reading
Battery level is measured using PVD peripheral, not ADC. So, console can only tell if voltage is low or not.
Hardware
[!CAUTION] I designed example PCB that you can see on all the pictures. I share it, however, some resistors values depend on parts that you would use (LEDs, charging current). Also I modified the PCB and schematic indempedly while working on the project, so some fixes might be required. If you intend to build one please double check it! I designed it assuming Li-Po cell with build in protection. DO NOT USE UNPROTECTED CELLS! There is a minor symmetry issue in the design: the power switch is centered relative to the LCD's active area, not the PCB itself
Code Compilation
[!TIP] To build the MounRiver Studio project with the lto (Link Time Optmalization) enabled, you must use GCC 12. The GCC 8 selected by default is bugged (at least from my experience): it cuts out all interrupt functions and volatile variables.
Parts
37| Qty | Value | Footprint | References |
|---|---|---|---|
| 6 | Conn_01x01 | TestPoint:TestPoint_Pad_1.0x1.0mm | 3.3V, GND, NRST, PD5, PD6, SWIO |
| 5 | TL3305AF160QG | footprints:SW_TL3305AF160QG | BTN_A, BTN_D, BTN_L, BTN_R, BTN_U |
| 2 | 100n | Capacitor_SMD:C_0603_1608Metric | C1, C3 |
| 2 | 10p | Capacitor_SMD:C_0603_1608Metric | C4, C5 |
| 2 | 4.7u | Capacitor_SMD:C_0603_1608Metric | C2, C6 |
| 2 | 5.1k | Resistor_SMD:R_0603_1608Metric | R2, R3 |
| 1 | 1u | Capacitor_SMD:C_0603_1608Metric | C7 |
| 1 | 2.2u | Capacitor_SMD:C_0603_1608Metric | C8 |
| 1 | 220 | Resistor_SMD:R_0603_1608Metric | R4 |
| 1 | 24MHz | Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm | Y1 |
| 1 | 470 | Resistor_SMD:R_0603_1608Metric | R6 |
| 1 | BAT | Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical | +BAT- |
| 1 | Buzzer | buzzer:buzz9mm | BZ1 |
| 1 | CH32V003FxPx | Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm | U1 |
| 1 | LD2980CM33TR | footprints:SOT95P280X145-5N | U3 |
| 1 | LED_CHRG | LED_SMD:LED_0603_1608Metric_Pad1.05x0.95mm_HandSolder | LED_CHRG |
| 1 | LED_PWR | LED_SMD:LED_0603_1608Metric_Pad1.05x0.95mm_HandSolder | LED_PWR |
| 1 | MCP73831-2-OT | Package_TO_SOT_SMD:SOT-23-5 | U2 |
| 1 | R_buzz | Resistor_SMD:R_0603_1608Metric | R1 |
| 1 | R_lcd | Resistor_SMD:R_0603_1608Metric | R7 |
| 1 | R_prog | Resistor_SMD:R_0603_1608Metric | R5 |
| 1 | SLW-66527511-SMT-TR | footprints:SW_SLW-66527511-SMT-TR | S1 |
| 1 | ST7735 | st7735:st7735_footprintv1 | LCD1 |
| 1 | USB_C_6P | Connector_USB:USB_C_Receptacle_GCT_USB4125-xx-x-0190_6P_TopMnt_Horizontal | J1 |
Similar boards
- 2 layers70 × 55 mm

- 2 layers69.4 × 89.9 mm

- 4 layers44 × 44 mm

Boot console
clarkmcc/my-first-linux-board
my-first-linux-board is a small F1C200S Linux board built around removable microSD storage, USB-C power and USB device data, a 3.3 V UART console, and power/status LEDs.
93 parts1 - 2 layers30 × 65 mm

- 2 layers58.2 × 73.7 mm

STM32U5A5-min circuit
Bobrowicz/STM32U5A5-min-circuit-dev-board
Breakout board for STM32U5A5VJT6
28 parts - 2 layers72 × 49 mm

- 2 layers141 × 90.9 mm

Haptic bracelet
earlynerd/ELRS-haptics
Battery-powered attitude feedback for FPV flying, with eight haptic actuators around each wrist. An ESP32-C6 receives telemetry over ESP-NOW and controls eight M2003FC1AE/DRV2625 pods using the music robot's UART ring…
269 parts - 4 layers

Questions
How big is the CH32Console PCB?
It measures 82.7 × 30.5 mm and has 2 copper layers, 1.6 mm thick.
What parts does the CH32Console use?
37 parts, built around the TL3305AF160QG. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
Where can I download the CH32Console KiCad files?
From the max7d3/CH32V003-MiniGameConsole repository on GitHub, at the commit this page was made from: the board is Hardware/PCB/CH32Console/CH32Console.kicad_pcb and its schematic Hardware/PCB/CH32Console/CH32Console.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Can I use the CH32Console design in my own project?
Its repository publishes it under the Apache-2.0 licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).
