Flash Hamr: 4,800 lines of FAT32 Verilog lose to a PicoRV32 running FatFS
What I tried
Block Hamr served one image baked into flash. The images were always going to come from an SD card: FAT32, drop a .po on it, plug it in.
First attempt was pure RTL. Verilog state machines parsing FAT32, chasing cluster chains, talking SPI to the card. The commit message was “Attempt one, no softcore.” About 4,800 lines across fat32_reader.v, sd_controller.v, sd_mount.v, sd_persist.v and friends. It kind of worked. Every FAT32 edge case, long filenames, directory parsing, sector math, was another state machine branch and another week.
Then PicoRV32: a RISC-V RV32I soft core, one Verilog file, 3,049 lines, written by Claire Xenia Wolf, who also wrote Yosys. It targets the toolchain I already use, with first-class ECP5 support. With a real CPU I could use elm-chan’s FatFS. The firmware is about 970 lines of C in main.c, diskio.c and spi_sd.c. It opens a file, reads blocks on demand, and writes them back.
The Apple side did not change: a 6502 driver ROM in the slot space talks to the register interface. A block request goes to the PicoRV32 through a toggle-based mailbox for the clock crossing. A block already cached in SDRAM gets copied to the block buffer. A miss reads from SD through FatFS, lands in SDRAM, then gets served. Writes go to SDRAM first so the IIe does not wait, then the CPU persists them to the .po on the card.



What I measured
- Cache hit: the 6502 gets its block in about 200 us.
- Cache miss: 5 ms to 20 ms for the first access to a block.
- CATALOG, SAVE, CREATE and nested directories all survive a cold reboot.
- 14 RTL FAT32 modules archived. Tag flash-hamr-v5 is the checkpoint.
What broke or surprised me
The worst one. I spent days on what looked like CDC timing, SDRAM cache misses and stretch pulses. hamr_rom.mem, the 6502 driver ROM, was stale. The build never called the assembler to regenerate it, so the RTL was right the whole time and the ROM did not match the register interface. The Makefile now rebuilds it from the Merlin source.
- UART debug prints before f_write were eating the 1.28 s write budget. Moved the print after the write.
- Some SDSC cards return 0x00 tokens after idle. An f_lseek(0) and f_read after mount wakes them up.
- The $C800 expansion ROM collided with the IIe’s internal ROM and gave a Relocation/Configuration Error. I rewrote the driver to fit in the $Cn page at 220 bytes, then fixed addr_decoder.v to handle the shared $C800 space properly.
Next
Multiple drives and a picker. That became v6 through v8 over the next two days.