The Card Edge — the FPGA as a pure P8X graphics card¶
The decision (2026-08-28): the FPGA CPU proved the P8X
design could live in silicon, and that proof is banked (tag
stage10-complete rebuilds the all-in-one machine forever). The FPGA's
FUTURE is the add-on GRAPHICS CARD for the machine that will live in
hardware. This arc cuts the design at the bus:
ideas, in order of appearance:
1) the Mac EMULATOR is the CPU, driving the real card over a
serial bus bridge <- THIS ARC
2) a second FPGA runs the P8X CPU, same card, inter-board link
3) the real TTL machine, same card, 5V bus on a DIN 41612 slot
All three drive the SAME card through the SAME contract. Ideas 1 and 2 are rehearsals of 3 — each retires risk on the interface before the TTL bus exists. This document is the contract.
1. The card edge (the contract)¶
The card IS its register window — the same one software already knows:
$FF50-$FF57 the GL port (GLDATA/GLSTAT/GLRB/GLERR/GLID)
(As designed, v1 also carried the $FF20-$FF2F 2D-device window; the single-interface migration closed it on 2026-09-01 — device idx now read $FF and swallow writes, exactly like the closed window on the bus. The protocol frames are unchanged.) Semantics are EXACTLY the memory map's (gen_memmap stays the authority; the graphics theory doc stays true).
New, bridge-era additions to the window (unused addresses):
BRIDGEV $FF55 read: protocol version, $01 (version byte FIRST:
BRIDGID $FF56 read: 'B' three masters will
speak this edge; we
can only add this
cheaply NOW)
A master that reads GLID='G' sees a graphics card (the GID0/GID1 "PG" signature retired with the device window); BRIDGID='B' additionally identifies a bridge front-end (absent on the all-in-one build and, later, on the TTL bus card).
2. What leaves the chip, what stays¶
LEAVES (the computer): STAYS (the card):
p8x_cpu (+ its BSRAM ROMs) p8x_geom + scratchpad + tables
cf_sd, sd_spi (storage) gfx + gfx_mem + gfx_span
the console role of the UART mdu_core (the geom datapath's)
sdram stack + scanout + panel
the UART, reborn as the bridge
Budget consequence (measured, stage-10f era): the CPU is ~1,184 LUT
sites and CF/SD ~1,310 more — roughly 2,500 freed against a 19,048
build. That is MORE than stages 10e + 10g + 10h were denied for.
Resurrecting 10e is git revert of its revert, THEN the room exists.
3. Build targets: addition, never subtraction¶
build.sh grows a fourth target beside the untouched three:
lcd the all-in-one computer -- UNCHANGED, still built, still
placed, still passing its six frames: the regression
baseline and the fallback personality
card p8x_gcard_top.v: bridge front-end + the card modules only
The engine RTL is SHARED between lcd and card in identity — the same files, no forks. Only the top level differs. The board flips personality by re-flash; both bitstreams are kept.
4. The bridge protocol (serial, v1)¶
Transport: the board's USB-serial UART, owned entirely by the bridge in card personality (there is no console — there is no CPU). Baud: negotiated by configuration, start at 115200, characterize the BL616's ceiling early (a RISK item: pixel-heavy pushes want every baud we can get; GL command streams do not care).
Host-driven, binary, byte-oriented. The 6-bit register index addresses the window: idx = I/O address minus $FF20, so idx $30-$37 is the GL port ($FF50-$FF57) -- one subtraction, mechanical in both directions. (idx $00-$0F was the 2D device, retired: reads $FF, writes swallowed.)
$00 PING: card replies 'P' '8' 'X' 'G' then BRIDGEV
$80|idx val WRITE val to register idx (no reply)
$40|idx READ register idx -> card replies one byte
$01 n b0..b(n-1)
BURST: n (1-64) bytes to GLDATA; card replies $06
(ACK) after the LAST byte is ACCEPTED by the FIFO
-- the ack is flow control: at most one burst in
flight, so the card's FIFO and the UART receiver
can never be overrun
$02 STATUS: card replies GLSTAT (a read alias that
spares the mux for the hottest poll)
Everything else reserved; the card ignores unknown commands (and a future v2 can extend behind BRIDGEV). The host idles the line between commands; there is no card-initiated traffic except replies.
Reset story: opening the port MUST NOT reset the card (lesson learned: the current board does not reset on open either — probe, never assume). PING is the state probe. A protocol-level RESET is deliberately absent from v1: the GL RESETF verb already covers graphics state, and a wedged bridge FSM is reflash territory.
5. The emulator as CPU¶
p8xemu -B <serial-device>: every CPU access to the GL port forwards
over the bridge; everything else (CPU, RAM, storage, console) stays
emulated locally. Flag-gated: default behavior is byte-identical to
today, so every existing test runs unchanged.
The payoff beyond the demo: pointing the EXISTING frame suites at -B runs the golden model against REAL SILICON — the co-sim's final form. POINT/GLERR/GLSTAT reads become serial round-trips; the FIFO contract already tolerates arbitrary latency, and GCHECK's polls translate to STATUS commands.
Known slow path: IMAGE-class per-pixel pushes (~200KB of pokes for a 256x256 P8I) are minutes at 115200 — hence the baud characterization up front, and no pretense that idea 1 is a gaming bus. The language model (stage 10's whole thesis) is what makes a serial bus sufficient: scenes are tens of bytes.
6. RTL: p8x_gcard_top.v¶
A small top: clocks/PLL as today, the SDRAM/scanout/panel stack as today, geom+gfx as today, and in place of the CPU a bridge FSM:
UART rx -> command decoder -> {reg write strobe, reg read mux,
GLDATA push with FIFO backpressure}
<- reply mux (PING string, read data, ACK, STATUS)
The bridge FSM is deliberately dumb — no buffering beyond one command, state machine ~a dozen states. Estimated well under 300 LUTs against ~2,500 freed.
7. Proof ladder (the usual discipline)¶
- This document.
- Protocol reference implementation host-side (tools/glbridge.py: open/ping/read/write/burst) + a pure-python unit test against a mock endpoint.
- p8x_gcard_top.v +
cardtarget; tb_gcard.v drives the bridge with UART BYTES and replays the existing six-frame scenes through it — the same .ppm compares, transport swapped. (The benches already isolate scene-from-transport; this is the dividend.) - Placement (trivially: the card is ~6,500 sites in a 20,736 chip).
- Board first light: flash
card, PING over the wire, GLID via $40-read, one gl scene streamed by glbridge.py — the panel draws with NO CPU ON THE CHIP. - p8xemu -B: the frame suites against silicon; then the fun (BASIC on the Mac emulator, panel on the desk).
- THEN: 10e resurrection in the roomy build; 10g/10h become schedulable again.
8. Status (2026-08-28): FIRST LIGHT — the ladder is climbed¶
Every rung proven, same day: glbridge.py mock-proven; the card
target PLACES at 16,808 LUT4 / 81% with BSRAM 5/46 (41 blocks + ~2.2k
LUT freed -- the 10e/10g/10h room); tb_gcard renders the 10a frame
byte-identical THROUGH protocol bytes; p8xemu -B drives a MockCard
from a full BASIC session. Then the board: PING answered P8XG v1 with
the full identity (P/G/G/1/B), a register-driven PLOT read back $F800
exactly, the PG-640A house streamed from the Mac, recorded to a list
and drawn -- a graphics card with NO CPU ON THE CHIP -- and p8xemu -B
ran the LINFUN rubber-band program against real silicon: -2017 / 31 /
-1, the emulator's golden values from the card's actual framebuffer.
The card bitstream ran from SRAM for first light; flashing it (or
keeping lcd in flash) is a per-board personality choice.
8b. Risks, named¶
- UART integrity at speed: imgsend's history (transport acks are not content checks) — the protocol's per-burst ack helps, a periodic CRC op can join v2 if reality demands it.
- The lcd target must never break: it builds in CI-discipline (the test ladder) until the day it is retired on purpose.
- One serial port, two personalities: scripts must PING to learn who is listening (a monitor banner means lcd personality; PING replies mean card) — never assume.
- Idle means TWO polls: GLSTAT bit6 clears while the 2D engine may still be draining its final span to SDRAM (tb_gcard found the frame 19 pixels short). Hosts poll GLSTAT.busy then GSTAT.busy. (BASIC's GCHECK once did the same dance before probing GID0; it is GLID-only since the single-interface migration — GLID answers regardless of walker state.)