P8X on FPGA¶
A standalone FPGA implementation of P8X — the whole machine (CPU, memory,
UART console, SD disk) in one FPGA, no backplane. It keeps the same
microarchitecture as the TTL build: the horizontal microcode word, the
sequencer, the pointer/address model, DOE/DLD/PSEL selects. The microcode
binary (from microcode/genucode.py) and the C emulator stay the reference, so
the monitor / OS / BASIC / C-compiler / assembler run unmodified.
This is a parallel track to the TTL / bus-connected hardware build, which
continues (just delayed). Nothing here touches generators/ or the card designs.
Target board: Sipeed Tang Nano 20K (Gowin GW2AR-18). See
tang-nano-20k/ for the board-specific files and the
Milestone-0 build.
The verification spine¶
Every milestone is "make the RTL match the emulator." We run the same program
on an Icarus sim of the RTL and on the C emulator (emulator/), and diff
architectural state (PC, registers, flags, memory writes) cycle by cycle. The
emulator is the golden model; a divergence is a bug with an exact cycle and
signal. Same adversarial-diff discipline used elsewhere in the project.
Getting started¶
Two independent paths. The simulator needs no hardware and is the whole of Milestones 1-2; the board adds 0, 3 and 4.
Prerequisites¶
| For | You need |
|---|---|
| Everything | python3, a C compiler (Apple clang / gcc) |
| Simulation | iverilog — brew install icarus-verilog |
| The board | oss-cad-suite (yosys, nextpnr-himbaechel, apicula, openFPGALoader) |
| The board | a Sipeed Tang Nano 20K and a USB-C data cable |
oss-cad-suite is a tarball, not a package: download the darwin-arm64 (or your
platform's) release from
oss-cad-suite-build,
extract to ~/oss-cad-suite, then — on macOS, or every binary refuses to launch:
xattr -dr com.apple.quarantine ~/oss-cad-suite
No admin rights are needed for any of this; build.sh sources the environment
itself.
Step 0 — build the emulator first (both paths need it)¶
The microcode images u0-u3.bin are build products and are not in the repo,
and the emulator is the co-sim's reference model. From the repo root:
cd emulator && make
Path A — simulation only, no hardware¶
fpga/sim/run.sh 20000 # monitor boot, RTL vs emulator
fpga/sim/run.sh 60000 isa_test.asm # the original 88 opcodes
fpga/sim/run.sh 200000 "" console_in.txt # driven monitor + console diff
fpga/sim/run.sh 2000000 "" boot_in.txt os/run-disk.img # boots P8X/OS
Each prints PASS: RTL matches emulator for N cycles, or a DIVERGENCE dump
naming the exact microcycle. To drive the machine by hand instead of diffing:
fpga/sim/console.sh "" os/run-disk.img # real terminal into the RTL
(The graphics engine is byte-compared against the emulator by the GL RTL
battery, emulator/test/c_gl_rtl_test.sh — the old sim/gfx.sh device-door
co-sim retired with the single-interface migration.)
B boots the OS, then pwd / dir / cat README.TXT. Ctrl-D or Ctrl-C quits.
Writes persist for the session — they land in fpga/sim/work/disk.img, a copy, so
your real image is untouched and your changes are replaced next launch.
It is slow but usable — roughly 30-50k CPU cycles/second.
Path B — the real board¶
fpga/tang-nano-20k/build.sh cpu load # build + program (volatile SRAM)
fpga/tang-nano-20k/build.sh cpu flash # ... or persist it across power cycles
Then talk to it. The onboard bridge enumerates two serial devices and the
higher-numbered one is the console (the other is JTAG and returns garbage);
use /dev/cu.*, not /dev/tty.*:
tang-nano-20k/tools/term.py # auto-picks the port; Ctrl-] quits
term.py is a dependency-free terminal that picks the console port for you and
quits with Ctrl-]. A stock terminal is equally fine —
screen /dev/cu.usbserial-<N>1 115200, exit with Ctrl-A then k — because the
firmware expands newlines itself (see docs/p8x-monitor.md).
Press Enter for the monitor's * prompt. ? for help, I to identify the
microSD, B to boot the OS from it.
Updating the board — TWO surfaces, not one¶
A change may live in either place, and a feature can need both:
| Surface | Carries | Update with |
|---|---|---|
| bitstream | CPU, microcode, monitor ROM, graphics RTL | build.sh lcd load |
| SD card | P8X/OS, /bin, BASIC |
tools/imgsend.py os/run-disk.img |
The graphics ellipse is the cautionary example: it is RTL and a BASIC
statement. Rebuilding only the bitstream left the hardware understanding a
command that nothing on the card could issue, which presents as ?SYNTAX ERROR
from BASIC — nothing about it points at the card being stale.
Check p8x_cpu.fs's timestamp after a build. This concealed two separate
build failures here. The script itself is no longer the hole — it runs under
set -euo pipefail and each of synthesise / place-and-route / pack exits 1 on
failure, so load cannot be reached after a failed build. What remains is that
cpu and lcd write the same p8x_cpu.fs: a failed lcd build leaves the
previous file sitting there looking perfectly valid, and it may well be a cpu
bitstream with no graphics in it at all. Note also that a | tail-style pipeline
hides the exit status, which is how the failure reads as success. If a fix appears
to do nothing on the board, check the mtime before debugging anything else — and if that is current, reload once: a freshly loaded
bitstream has come up not answering more than once, and the same file loaded
again fixed it.
A card needs a P8XFS image on it. Writing one from the host needs root, so if you
do not have it the board can install its own over the serial console — see
tang-nano-20k/tools/.
If the board goes silent, load the Milestone-0 echo bitstream
(build.sh echo load) as a known-good baseline; if that is silent too, the USB
bridge has wedged and a replug fixes it. After any replug, drain the port before
testing — the factory boot text sits buffered and reads like a reply.
Milestones¶
| # | Milestone | Board? | Proves |
|---|---|---|---|
| 0 | First light: UART echo + heartbeat LED ✅ | yes | toolchain, bitstream, console path |
| 1 | CPU core in simulation ✅ | no | the microarchitecture is correct (all 88 opcodes) |
| 2 | Peripherals in simulation (ACIA-UART) ✅ | no | monitor boots to a sim console; console output diffed |
| 3 | Core on real hardware ✅ | yes | P8X talks over USB for real, full 64K map |
| 4 | SD disk (SD-over-SPI behind the BIOS block API) ✅ | yes | OS boots from SD, full FS |
| 5 | Polish: clock-up, IRQ (backlog #26), stretch goals | yes | performance + extras |
| 6 | Graphics: 480x272 panel + drawing engine ✅ | yes | BASIC draws on a real panel; the engine matches the emulator pixel for pixel across all three payloads |
Milestones 0–2 are most of the effort and only 0 needs the board — the CPU is built and proven in simulation before the hardware ever runs it.
Status 2026-08-12: 0–4 done. P8X/OS boots from a microSD on the Tang Nano
20K. The CPU runs at 27/3 = 9 MHz (three fabric phases per microcycle, see
tang-nano-20k/rtl/p8x_top.v) with the full 64K map — affordable because the
microcode ROM is compacted from 8192 to 4096 words, so no SDRAM controller was
needed. Build and flash with tang-nano-20k/build.sh cpu load.
P8X is written to the board's onboard flash, so it comes up standalone on
power with no host involvement. (build.sh cpu load puts a build in volatile
SRAM instead, which is what you want while iterating — but note that a power
cycle then reverts to whatever is in flash, and the factory LiteX demo answers on
the same serial port and will silently swallow anything a script sends it.)
Layout¶
fpga/
├── README.md this file
├── docs/architecture.md module hierarchy, memory/peripheral map, co-sim spec
├── rtl/ board-independent core (shared by sim and board)
│ ├── README.md what is shared vs sim-only, and the `cen` contract
│ ├── p8x_cpu.v the CPU. `cen` clock enable; otherwise one
│ │ microcycle per clock, matching the emulator
│ ├── gfx.v graphics: registers, drawing engine, framebuffer
│ ├── video_rgb.v 480x272 panel timing + 2x-doubled scanout
│ └── p8x_soc.v sim-only SoC: async-read arrays + modelled I/O
├── sim/ co-simulation against the C emulator
│ ├── README.md how the trace-diff works, and why -N exists
│ ├── run.sh build + run + diff [CYCLES] [ROM] [RX] [CF]
│ ├── console.sh interactive console on the RTL (not diffed)
│ ├── mk_ucode_mem.py 4 ROM images → 32-bit ucode.hex
│ ├── tb_p8x.v testbench: canonical per-cycle trace, ACIA, CF
│ ├── isa_test.asm directed exerciser, the original 88 opcodes
│ └── console_in.txt, cf_id.txt, boot_in.txt scripted keystrokes
└── tang-nano-20k/ the board build
├── README.md toolchain, pinout, flashing, board-sim benches
├── build.sh [echo|cpu|lcd] [build|load|flash]
├── mk_compact_ucode.py 8192→4096-word microcode remap (buys the 64K map)
├── tangnano20k.cst pin constraints (verified)
├── rtl/
│ ├── top.v Milestone-0: UART echo + heartbeat
│ ├── p8x_top.v the real SoC: 3-phase microcycle, BRAM, ACIA, CF
│ ├── uart.v 8N1 UART (TX+RX)
│ ├── cf_sd.v $FF10-$FF17 CF task file over the SD controller
│ └── sd_spi.v microSD in SPI mode: init, read block, write block
├── sim/ board-level benches (see tang-nano-20k/README.md)
│ ├── tb_top.v Milestone-0 echo path
│ ├── tb_p8x_top.v whole board top: monitor + OS boot off a card
│ ├── tb_video.v panel frame geometry (480x272, 54.11 Hz)
│ ├── tb_scanout.v which fb pixel reaches which panel pixel
│ ├── sd_model.v behavioural SPI card, with +sdfail fault injection
│ └── tb_sd_spi.v sd_spi's error paths
└── tools/ host-side helpers, none needing root
├── README.md
├── term.py serial terminal; translates P8X's bare-LF output
├── osload.asm N sectors → LBA 1.., patch OSCNT
└── imgload.asm clone a whole P8XFS image from LBA 0
How we work¶
- RTL + testbenches + co-sim harness written here; board bitstreams flashed and reported from hardware.
- Sim green before hardware — never debug unproven RTL and an unproven board at once.
- Small commits into
fpga/, docs alongside, emulator stays the spec.