16 megabytes for a 32-kilobyte game

Right now Rusty Nail runs one cart - the one baked into its firmware - plus whatever I push over USB from the viewer. That is fine for development. But the whole point of a fantasy console is the cartridge: a thing you hold, dock, and play. So I have been designing the cartridge slot. Nothing physical exists yet - this is the plan, written down before the parts arrive, partly so I stop changing my mind.

The W25Q128 flash breakout, still in its Adafruit bag, next to an empty Game Boy cartridge shell - 16 MiB of flash and the form factor I want to dock it into

That is the whole post in one photo: 16 MiB of flash on the left, and on the right the shape it is meant to live in.

The chip

The cart is a flash chip. Specifically a Winbond W25Q128JV - 16 MiB of SPI NOR flash in an 8-pin package, 3.3V, cheap, in everything.

The Adafruit W25Q128 SPI flash breakout - 16 MiB, with level shifting and a 3.3V regulator on board

For development I will start with the Adafruit breakout above: it adds level shifting and a 3.3V regulator and exposes the chip as flat flash, so I can wire it to the Nucleo before I design a single cartridge PCB.

Here is the funny part. A maximum-size PICO-8 cart is 32 KiB - PICO-8's manual caps the cart data at 32k, with the compressed code under 15,360 bytes. The W25Q128 is 16 MiB. That is room for about 512 max-size carts on one chip. For a console whose headline games are 32 kilobytes.

The overkill is on purpose. The chip costs about the same as the 4 MiB version, it is the most common one so it is the best supported, and the cartridge format is not only for PICO-8 - the roadmap has bigger cartridge types (CHIP-8, Game Boy ROMs) that will happily eat that space. Buying headroom you do not need yet is cheaper than a second board spin later.

One detail that will brick you if you get it wrong: buy the JV family (3.3V), not the JW family (1.8V). They look identical in the listing. The STM32H753's cart slot is 3.3V; a 1.8V part on a 3.3V bus is a bad afternoon.

The cartridge is a format I already have

This is the bit I am pleased about, and it is why this is a plan and not a rewrite.

The firmware already loads carts from two places: the embedded one, and a sideload over USB. There is a whole pipe for it - a staging buffer (UPLOAD_ACCUM), a NEW_CART signal that hands a validated cart to the present loop, and parse_bundle, which reads the cart's .fcb format (the Lua plus the PICO-8 memory image) and checks it.

A cartridge does not need any of that rebuilt. If I store the game on the flash chip as a verbatim .fcb bundle, then reading it off the cart is the same as reading it off USB:

embedded cart  -.
USB sideload   --+-- parse_bundle --> NEW_CART --> run
SPI cartridge  -'

One format, three sources. The cart reader's job is just to copy bytes from flash into UPLOAD_ACCUM and fire NEW_CART - the cart then runs through code that already works. The only genuinely new firmware is the part that talks to the flash chip, and SPI NOR is about as simple as a peripheral gets: read (0x03), write a page (0x02), erase a sector (0x20), and a busy-poll.

How the console knows a cart is in

No detect pin. When the slot is read, the firmware asks the chip who it is - the JEDEC ID command, 0x9F. A W25Q128 answers EF 40 18: Winbond, memory type, 128 megabit. If I get that back, there is a known-good cart in the slot. If I get 00, FF, or noise, there is nothing there - which is also how I find out the SPI wiring is dead, so it is two checks for the price of one.

Two slots, on purpose

There are two pieces of storage in the plan, deliberately on different buses so they can both be live at once:

  • A microSD card for a library - a launcher you scroll, full of games.
  • The cartridge you dock for a single game.

The SD goes on the H7's native SD controller, SDMMC1. The lucky bit: all six of its pins are free and sit consecutively on one header, CN8.

The Nucleo-H753ZI header pinout - SDMMC1's pins (PC8-PC12 + PD2) are free and grouped on CN8

Nothing else wants them. The panel is on its own SPI, the audio is on the SAI pins over on CN9, the gamepad is on a UART on CN9, and Ethernet sits in a cluster nowhere near. So the SD interface and its power all land on the one header - a couple of dollars of breakout and some jumper wires, no soldering. The cartridge's flash chip goes on a separate spare SPI, which leaves the SD untouched.

The part I actually want

Here is the version that makes it feel like a console instead of a dev board.

The launcher SD and the cartridge are on different buses, both live. So the console can read a game off the SD card and write it to a blank cartridge. The fantasy console becomes its own cart writer:

copy game.fcb onto the SD card
  -> insert the SD + a blank cartridge
  -> on the console: "burn cartridge"
  -> read game.fcb, validate it (parse_bundle, again)
  -> erase the flash sectors, write the pages, read back and verify
  -> the cartridge is now playable

No external programmer, no PC in the loop once the file is on the card. Make a game, drop it on an SD, walk to the console, burn a cart, play it. That is the whole fantasy.

What is done, and what is not

Honestly: the physical side is all ahead of me. No cart exists, no slot is wired, the breakout is not even ordered. What is done is the half that is usually the hard part - the firmware already runs carts through a validated pipe, and the cartridge reuses it; the pin map is worked out and clear of everything else; the chip is chosen and the one fatal mistake (JV vs JW) is written down.

The order I will build it in:

  1. SD launcher. A card full of games, a picker. Stands alone, useful straight away.
  2. Read SPI carts. Detect a docked W25Q, read the .fcb, run it.
  3. Burn from SD. The console writes its own cartridges.
  4. Saves. SPI NOR is awkward for save data - you erase a 4 KiB sector before you can rewrite it, which is miserable for a save file you touch often. So saves get a separate, byte-writable FRAM chip on the cart, not the NOR. Later.

None of it is hard, exactly. It is just all still in front of me. But the chip is picked and the firmware is waiting, which is a good place to start from.