This is the 2nd iteration of an ongoing project where I'm attempting to create my own homemade "retro" game console.
The rules are pretty straightforward:
- No modern ports or protocols (no USB, HDMI, WiFi, Bluetooth, SD cards, etc.)
- Games must be cartridge based (and must use parallel communication rather than serial)
- Composite AV only (no VGA, S-Video, etc.)
An RP2350B (via the Pimoroni PGA2350) generates NTSC color composite video straight out of its GPIO through a resistor ladder, plays chiptune audio through an I2S DAC, reads two controller ports, and boots games off real cartridges over a parallel memory-mapped bus. The board, the cartridge PCBs and the 3D-printed shell are all in this repo alongside the firmware, the games, and a host emulator for developing them without touching hardware.
The console is flashed over SWD using a debug probe (i.e. a separate Pico 2). PLCC-32 based cartridges are programmed using a T48.
- NTSC 240p color composite, 320×240 RGB332 framebuffer @ 60 fps. A 6-bit R-2R ladder on GPIO2–7 feeds the RCA jack; PIO+DMA stream the 4×fsc sample stream, and core1 renders each scanline just-in-time out of a small SRAM ring.
- The whole design hangs off a 315 MHz system clock: it is the only RP2350 clock that divides to an exact 14.31818 MHz = 4 × 3.579545 MHz subcarrier rate, so the TV's color decoder locks with zero drift.
- Audio through a PCM5102A I2S DAC - 16-bit stereo at exactly 48 kHz off the same clock. Games
get an NES-style 4-voice chip synth (2 pulse + triangle + noise), a flash-resident 30-instrument
sample bank (drums, stabs, pads - synthesized offline by
tools/gen_sample_bank.py), and the option to install their own 48 kHz DSP fill callback and do everything themselves. - Two controller ports on a generic 1-wire transport. Uses a 4-pin Bulgin SA2367/SA2368 connector. Compatible with the N64 controller, however the protocol layer is split so a custom controller only swaps the command module, not the transport.
- Cartridges on a 44-pin edge connector: a 16-bit address bus, an 8-bit data bus, and three strobes, driven by PIO + DMA. Two cartridge classes share the bus - a dumb NOR-ROM cart, and a "smart" cart with its own MCU that can also act as a compute coprocessor for the game (still in the concept stage atm).
- A BIOS that idles on an INSERT CARTRIDGE screen, detects a cart the moment it is seated, CRC-verifies the game code as it loads it, and jumps in. Pull the cart mid-game and it unwinds back to the idle screen.
| Subsystem | Wiring |
|---|---|
| Video | 6-bit R-2R ladder (150 Ω / 300 Ω) on GPIO2–7 → RCA center, 300 Ω terminator to GND |
| Audio | PCM5102A: DIN=GPIO8, BCK=GPIO9, LRCK=GPIO10, SCK tied to GND (SCK-less mode) |
| Controllers | 1-wire data on GPIO40/41, external pull-ups to 3.3 V, one PIO state machine each |
| Cart bus | A0–A15 = GPIO16–31, D0–D7 = GPIO32–39, /RD = GPIO44, /CS = GPIO45, /WR = GPIO46 |
| Cart presence | /BUSY on GPIO11: held LOW while a cart is seated, and doubles as the coprocessor's ready/busy line |
| Logging / flashing | UART0 on GPIO0/1 and SWD, both to a second Pico running debugprobe |
The four PIO/peripheral users are deliberately disjoint: video owns pio0 sm0, audio pio0 sm1 (plus DMA_IRQ_1), the cart bus master pio1, and the controller ports pio2.
hardware/ holds the KiCad projects - the console board, a PLCC-32 parallel ROM cart, and an
RP2350B smart cart - plus fab/assembly outputs and the STLs for the 3D-printed shell, feet, buttons
and cartridge cases.
Every cart presents a 64-byte GCV2 header at offset 0 over the parallel bus. A flag in that header
picks the class; presence itself is a single GPIO read, so hot-swap needs no bus probing.
- Dumb ROM cart - an SST39SF040 NOR flash (512 KiB), a
74HCT273bank latch, and nothing else. Address lines reach 16 KiB at a time; a byte written over /WR selects the bank. Burned on a bench programmer. - Smart cart - a second MCU that serves the same bus from its own flash and can additionally run
coprocessor jobs: the game writes a small job descriptor to an MMIO window, the cart computes
while the console renders, and /BUSY pulses when the result is ready. Deliberately a job
descriptor, not a data pipe - a cart that needs bulk data generates it itself. RP2350 and
Teensy 4.1 implementations live in
cart/emu/, and each game supplies its own compute function.
Games are freestanding ARM binaries linked at a fixed 192 KiB game-RAM window, packed into a ROM
image by tools/pack_cart.py. They call the console only through one append-only ABI table
(console_api.h): video, the 2D engine, a software 3D rasterizer, audio, input, assets, coprocessor,
and a few system calls. Asset data is cached into PSRAM at load; anything bigger streams off the cart
on demand.
See games directory README.
Games are portable C that only talk to console_api_t, so the emulator recompiles each one natively
into a .so and supplies the API table - there is no CPU emulation. It renders real frames, mixes
real audio, and can dump PNGs, write a WAV, or replay scripted controller input against a golden
per-frame CRC.
cmake -S emu -B emu/build && cmake --build emu/build
scripts/run-emu.sh doogus --frames 900 --png /tmp/frames
scripts/run-emu.sh bananaboat --frames 900 \
--input tests/input/bananaboat_play.in --hash-check tests/golden/bananaboat.hashbash tests/run.sh # audio DSP, cart header format, sample-bank validator, game logic/solversThe console has no USB - it is flashed over SWD from a second Pico running debugprobe
(GP2→SWCLK, GP3→SWDIO, GND↔GND), with its UART bridged on the same probe for logs.
PICO_SDK_PATH=/path/to/pico-sdk scripts/flash-console.sh # build + flash
scripts/console-log.py # stream the console's UART
scripts/flash-console.sh --rescue # unwedge a hung board, then flashscripts/build-rom-cart.sh testrom --program --verify # build, pack, burn a dumb ROM cart (minipro)
scripts/build-flash-cart-rp2350.sh nbody # smart cart: firmware + ROM image in one shot
scripts/build-flash-cart-teensy.sh nbody # Teensy 4.1 variantsrc/ console firmware (the cartridge BIOS) + library code: video/ audio/ input/ cart/ engine/
cart/sdk/ the game SDK: ABI headers, linker script, toolchain file
cart/emu/ smart-cart firmware library (RP2350 and Teensy 4.1 bus responders + coprocessor worker)
games/ the games; games/<g>/cart/ is that game's companion smart-cart firmware
emu/ host emulator
demos/ opt-in bring-up and measurement apps (overscan test card, bus validator, 3D scene, engine demo)
tools/ cart packer, sample-bank generator
scripts/ build/flash/log helpers
tests/ host unit tests + golden replay hashes
hardware/ KiCad projects (console + both carts), fab outputs, 3D-printed shell
Great question! This is just a fun challenge and sort of childhood wish fulfillment. I don't plan on ever releasing this or its future iterations as something you could buy in stores (there's already tons of "retro" game console knockoffs out there). Might be cool to give it out as a limited run merch item though.
Yes they do. The three main games (Banana Boat, Doogfighter, and It's Time to Party!!) are entirely AI-generated and more or less intentionally crappy for enterainment purposes. I don't have any plans on making a legit game for this unfortunately. I have a troubled history with starting ambitious game projects and losing interest after 2-3 months. Maybe in future iterations once this project is more refined, others will be compelled to make their own games for this console. That'd be pretty insane and also a huge honor.
You can't! This is a hobby project. If you want to make your own you can find the gerbers, BOMs, and
3D print materials in the hardware/ directory. Just be warned that it's going to be expensive and
you'll be doing a lot of 3D printing and soldering. The semi-assembled console and ROM cart PCBs
alone cost me over $400. Probably not worth it unless you're as stupid with money as I am.
It sure isn't. I plan to address this in V3 of this project by replacing it with an actual 8-bit or 16-bit CPU, depending on what I can find. The RP2350 acts as a convenient stepping stone to get me to that final goal of making a retro console that's as authentic as possible to that late 80s/early 90s time period.
The console supports N64 controllers as a convenience for testing purposes. They only require a single data wire and run on 3.3V, which makes them very easy to use with an MCU. The physical port is not an N64 controller port, but a generic circular 4-pin Bulgin SA2367/SA2368. The 4th wire is hooked up to some of the ADC GPIOs on the RP2350, but currently not used in the console firmware or any of the games; reserved for future applications. I still have yet to design my own controller, which is a separate project in itself. I'll likely tackle this in V3.
GCV1 (aka the "Catbox") is literally just a Pico 2 wired up to a VGA connector, aux jack, N64 controller, and a 6-pin "cartridge" slot for a W25Q* memory card/ROM chip. All contained in a gray 3D printed box with a pic of my parents' cat embossed on the front. Honestly not too fundamentally different from GCV2, but a lot less polished. I haven't bothered pushing it to GitHub because it's a messy thrown together prototype that I made to get familiar with the minimal core design of a game console.

0 comments
log in to comment.