7 minutes
xGB to USB - USB video capturing for Gameboy Advance and Gameboy Color

TL;DR
- Research project
- Shows how to capture Gameboy Advance and potentially Gameboy Color display signals using a CH32V305RBT6
- Uses TinyUSB library to output the signals via USB (USB Video Class)
- Code and binaries: https://codeberg.org/embedded-ideas/xgb2usb/
Introduction
This project is a proof-of-concept for capturing and outputting display signals from the 2nd and 3rd generation Game Boys using a low-cost, general-purpose microcontroller. It describes how to use a CH32V305 MCU, without any specialized peripherals (other than USB 2.0 HS), to capture the Gameboy Advance’s RGB565 display signals at approximately 60 Hz and full resolution, and stream them over USB using the UVC protocol in YUY2 format, all without any additional circuitry, allowing to build a recording/capturing device for the GBA. The code should also work for the Game Boy Color, as the signals are quite similar. There is only a difference in the resolution.
I’ll walk through my thought process and the ideas that led to my solution.
Motivation
Capturing parallel display signals isn’t very complicated when using popular microcontrollers with a wide range of fast parallel peripherals, such as the PIO on the Raspberry Pi Pico or the PARLIO on the ESP32-P4. Traditional interfaces like external memory controllers or camera interfaces can also be used to capture this data.
But what if you want to do the capturing on a very basic microcontroller? I wanted to test whether this would work using a general-purpose controller. I had a WCH CH32V203 lying around, so I decided to start with that. Due to my recent projects, I used the familiar signal from my Game Boy Advance.
Project Execution & Problem Solving
I had the following plan: I wanted to trigger on the VSYNC (start-of-frame) signal using an external interrupt. After this, another external signal interrupt from the pixel clock would trigger a DMA transfer, which moves the contents of a GPIO port input register containing 16 pin states into memory. Not a bad plan, but I was surprised to discover that it isn’t possible to trigger DMA transfers directly from external signals.
A deeper look into the DMA peripheral led me to the capture mode of the timer peripherals. This mode allows an external signal to trigger a DMA channel. Normally, it is intended to transfer the counter register, but there is nothing stopping you from using a GPIO port input register as the source address. And it actually worked.
With this setup, capturing the data became possible, but it was essentially useless because the frame data couldn’t be stored anywhere. A whole frame, 240 by 160 pixels with 2 bytes per pixel, would require 75 KiB of RAM, while the controller only had 20 KiB.
Great! A Frankenstein-like peripheral driver made from a timer, DMA, and basic GPIO, glued together with a few lines of code. Luckily, I also wanted to explore the TinyUSB library’s video capabilities and came across the CH32V305RBT6, which is essentially a CH32V203 with 50 percent more RAM and, most importantly, a high-speed USB port.
TinyUSB includes an implementation for UVC, or USB Video Class, normally used for webcams, and it was used in the Gameboy Interceptor, a Classic Gameboy (DMG-01) capture device by Sebastian Staaks (https://github.com/Staacks/gbinterceptor). It uses the full-speed USB port of a Pi Pico. However, for the higher resolution and color depth of the Gameboy Advance, a full-speed interface isn’t fast enough. This made the CH32V305 an excellent candidate, especially at a price of 1.50 USD per unit for 10 pieces. Still, there isn’t enough RAM to store a full frame, and TinyUSB only supports transferring complete frames.
After spending some time with the TinyUSB code, I managed to modify the video class driver to support “on-the-fly” data streaming without a frame buffer. I first experimented with generated data and then integrated my capture driver from the CH32V203. I modified it to provide a ring buffer that stores data line by line along with the line index. This allowed the USB video class code to synchronize on the first frame. After that, it retrieves data as quickly as possible. If it is too fast, it waits until new line data is received. With a ring buffer of about 50 lines, this worked quite well.
You can find my changes to the tinyUSB library here: https://github.com/embedded-ideas/tinyusb. I made a pull request so it might end-up in the mainline repo (update: it did).
Unfortunately, this was not the end of the challenge. The captured data was RGB565, but the supported video formats were only MJPEG, which we cannot calculate fast enough, and YUV2. YUV2 still requires calculations beyond the CPU’s capabilities. At this point, it was useful that the AGB doesn’t fully use RGB565: the lowest green bit is tied to ground. This means we are effectively dealing with RGB555, or 15-bit data.
This allowed an easy solution: a lookup table. By storing 8-bit YUV values for each possible color, we require 96 KiB, which can be stored in flash memory.
And voilà, it works.
Code
The code is separated into two parts. There is the capture driver (xgb_capture.c) which provides the captured data either as YUV2 or RGB565 data and the code that takes care of the tinyUSB interface (stored in main.c). The other files are taken from the video capture example of the tinyUSB library. They provide the right configuration and the USB descriptors.
You can find the code on codeberg.org: https://codeberg.org/embedded-ideas/xgb2usb/
Build (Linux)
First you have to prepare the toolchain. Download and extract the archive and add the path to the bin directory to your PATH variable.
Clone and build the project
git clone --recurse-submodules https://codeberg.org/embedded-ideas/xgb2usb.git
cd xgb2usb/tinyusb/tools/
python get_deps.py ch32v30x
cd ../..
mkdir build
cd build
cmake ..
make
Flash the binary to the MCU
The CH32V305 comes with a bootloader. Normally a development board should have a button labeled boot. You have to hold this button while pressing reset or plugin in the device. The MCU should start in bootloader mode which allows us to flash software via the USB. There is a nice tool for this: https://github.com/ch32-rs/wchisp/releases/tag/v0.3.0
Make sure you added the path to the executable to your PATH variable and run:
wchisp flash xgb2usb.hex
Testing
There are several options to test if the device works. You might consider OBS, QV4L2 or an Android app like “USB Camera”
Hardware
The code should work fine with every evaluation board which uses a CH32V305RBT6 and an external 8MHz crystal. I used the nanoCH32V305 board which isn’t ideal because two of the used inputs are only available via the second USB port data lines, hence I had to use an additional adapter board for A11/12 (see pictures).
Connections
| Signal | Gameboy Advance (32-pin connector) | Gameboy Advance (40-pin connector) | CH32V305 |
|---|---|---|---|
| VSYNC (SPS) | 25 | 26 | PB3 |
| PCLK (DCK) | 2 | 3 | PB8 |
| LDR1 | 11 | 12 | PA0 |
| LDR2 | 10 | 11 | PA1 |
| LDR3 | 9 | 10 | PA2 |
| LDR4 | 8 | 9 | PA3 |
| LDR5 | 7 | 8 | PA4 |
| LDG0 | 17 | 18 | - |
| LDG1 | 16 | 17 | PA5 |
| LDG2 | 15 | 16 | PA6 |
| LDG3 | 14 | 15 | PA7 |
| LDG4 | 13 | 14 | PA8 |
| LDG5 | 12 | 13 | PA9 |
| LDB1 | 22 | 23 | PA10 |
| LDB2 | 21 | 22 | PA11 |
| LDB3 | 20 | 21 | PA12 |
| LDB4 | 19 | 20 | PA13 |
| LDB5 | 18 | 19 | PA14 |
| GND | 32 | 35 | GND |
Thanks
- TinyUSB Library for this awesome project
- xPack Binary Development Tools for providing a nice way to get an alternative toolchain
- WCH-ISP for the easy to use flash tool
Files
Sourcecode and release binaries are hosted on codeberg.org
- Sourcecode: https://codeberg.org/embedded-ideas/xgb2usb/
- Binaries: https://codeberg.org/embedded-ideas/xgb2usb/releases
Disclaimer
This project is provided for educational and experimental use only. Use all code, hardware, and instructions at your own risk.
The author assumes no responsibility for damage, data loss, or malfunction resulting from its use. All trademarks (e.g., Game Boy Advance) belong to their respective owners. This project is not affiliated with WCH, Nintendo, or any other company.
1312 Words
2025-10-30 01:00