LiteX 8008 Node

hardware / status: done

This is the “Intel 8008 you can talk to over the network” thing from the intro, and I’m still not sure why. The b8008 core sits in the fabric of a Lattice ECP5-5G Versa next to a LiteX SoC. The 8008 runs its ROM monitor at real 1972 speed, 2.2 µs per φ-cycle, and its serial console goes into a pair of FIFOs that a host reads and writes over Etherbone on UDP. make login finds the board on the LAN and drops you at the monitor prompt, and Ctrl-] gets you out.

The product is two operations, read a byte and write a byte. There is no host-side load, peek, poke, run or step, because the monitor already has D, W, L, G and H and you type at them the same way you would over a serial cable. The one promise is that no byte between your UDP socket and the core’s UART pins gets lost, reordered or duplicated. To keep that promise the design stalls the 8008 when the host stops reading, so it is faithful and slow rather than real-time. The spec was written before the RTL was fixed and it found 12 places where the gateware disagreed with it.

The repo is called remote_8008. It was carved out of the core repo in July 2026 and it first ran on silicon on 2026-08-08.

Current state

As of 2026-09-10: Works on the board as of 2026-08-09, and the tree as of 2026-09-10 has not been on the board. Those are two different states and the second needs a bench session.

On the board: the stack runs end to end on the Versa with the sys clock at 60 MHz. DHCP lease, discovery over broadcast, make login into the 8008 monitor, and memory read and write through the monitor’s D and W commands checked by hand. The transport is a software Etherbone server on the VexRiscv over the plain ethmac, because LiteEth’s hardware Etherbone path never answered on silicon.

In the tree: the wrapper now follows the core’s current b8008_top, which absorbed the 16 KB RAM on 2026-08-08. Same logic, but the RAM is inferred from the GHDL netlist now instead of being a Migen Memory. Both boot sims reach the banner, make test is green, and the verification workflow went green on GitHub for the first time on 2026-09-10. No bitstream has been built from it.

The verification plan stands at 44 of 133 rows passing. make selftest exists with five hardware checks and has never had a live run.

The thing I keep dreaming about and haven’t designed is a supercomputer made of these. A pile of 8008s on the network, each one reachable the same way this one is. No drawings yet.

Architecture

Two clock domains off the Versa’s 100 MHz oscillator. cd_sys carries the LiteX SoC, the VexRiscv, LiteEth and the console FIFOs. It was 75 MHz on paper and failed timing at about 66 MHz on the first real build, so it runs at 60 MHz now. cd_b8008 is 25 MHz and holds only the b8008 core with its ROM and RAM, the same clock it runs at in the core repo.

The console crosses between the two domains as an asynchronous serial line at 115200 baud. A LiteX RS232 PHY in cd_sys talks to the core’s own USART in cd_b8008, so the byte path has no parallel clock-domain crossing at all. The whole design has four crossings between the domains. Two are the serial rx and tx lines, one is the backpressure stall level, and one is the reset ordering that releases cd_b8008 after cd_sys.

The RX FIFO is 4096 bytes and the TX FIFO is 256. When the host stops draining and the RX FIFO reaches 4032 bytes, a stall crosses into cd_b8008 and holds the core’s phase clock state machine in place through its run_enable input. That is a hold on the φ generator and no clock is gated. It releases at 3968 bytes. The 64-byte headroom above the high-water mark covers the three bytes that can still be in flight after the stall asserts, with about 20 times margin. If the host never comes back the 8008 stays stopped forever. There is no timeout.

The host sees six registers in one console CSR bank. Reads have no side effects anywhere. console_rx returns data, a valid bit and the FIFO level in one 32-bit word, and a separate write to console_rx_pop consumes the byte. That matters because LiteX’s UDP client retries a timed-out read, and the old destructive read lost a byte every time a reply packet got dropped. console_tx_data pushes a byte, console_tx reports level and full, and console_err is three sticky bits for rx overflow, tx rejected and pop-on-empty that survive a reset and clear by writing 1.

Transport is Etherbone over UDP. Reads are one 32-bit word per round trip because litex_server clamps them, and writes burst up to 255 words, so the Etherbone buffer depth is 255 instead of LiteEth’s default of 16. On the board the Etherbone server is firmware on the VexRiscv. The host side broadcasts its requests to the subnet and the board replies unicast to the requester’s MAC, mirroring the request’s ports, because the bench’s mesh WiFi router drops or NATs unicast from a WiFi client to the wired LAN and passes broadcast.

The core’s raw signals, D0 to D7, S0 to S2, SYNC, φ1, φ2 and INT, go out to the X3 header for a logic analyzer or whatever else gets built later.

Hardware

Lattice ECP5-5G Versa development kit, with no board of my own. The Ethernet PHY is the Versa’s Marvell 88E1512 at MDIO address 0, and the link comes up at gigabit. Its MSCR2 register reads 0x1076, which means the PHY adds the RGMII delays itself in both directions, so the FPGA-side delays are set to zero.

During bring-up a debug UART goes out on X3, FPGA transmit on B19 and receive on B12, to an external 3.3 V FTDI. That console is where the firmware prints its PHY status, MAC counters and loopback results at boot. The b8008 debug bus shares the X3 header. Reset is the board’s reset button, and that is the whole recovery path for a wedged monitor.

Gateware and firmware

LiteX 2026.04 pinned into a local venv, with LiteEth vendored at master 276c9e3 because the pinned release corrupts hybrid-mode transmit frames. Yosys, nextpnr-ecp5, ecppack and GHDL from OSS CAD Suite, FuseSoC pinned at 2.4.6. The b8008 core comes in as the FuseSoC core greygiant:retro:b8008 through its ghdl_synth_verilog generator, the same way the Byte Hamr card consumes it, so nothing in the core repo changes for this project.

The SoC is soc/versa_soc.py. The production config is --ethmac-only, plain LiteEth MAC with the CPU doing everything above it. The hybrid flags and a static-IP bisect flag are still in the tree for when I go back to that. The console bridge is its own Migen module, ConsoleBridge, with the FIFOs, the CSRs, the sticky error register and the backpressure hysteresis. The VHDL wrapper src/b8008_net_core.vhdl holds the core, its monitor ROM in block RAM, a 2 ms auto-start pulse so the 8008 runs without a button, and the run_enable gate for the stall.

Firmware on the VexRiscv is C. eb8008.c is a software Etherbone server that speaks the litex CommUDP dialect, so a stock RemoteClient works against it. eb_serve.c is the glue between the UDP callback and the reply path, split out of main.c so it could be tested on the host. udp.c is a local fork of libliteeth’s, fixing an upstream bug where the destination filter in process_frame makes the broadcast callback unreachable, and adding subnet-broadcast acceptance, minimum-frame padding, a gratuitous ARP announce every 30 s, a forced gateway ARP refresh every 10 s, and MDIO helpers. The linker script has fixed ROM and SRAM ceilings because the generated regions file gets rewritten to the previous firmware’s size and every build that grows fails with “region rom overflowed”.

Host side is the b8008net Python package in host/. discovery.py tries a broadcast probe first, then a cache, then DNS, then a subnet sweep. eb_server.py replaces litex_server --udp with the broadcast transport. make login spawns that bridge and puts you at the monitor.

Sims and tests. A GHDL boot of the wrapper waits about 400 ms of sim time for the monitor banner, and a Verilator run does the same on the gate-level netlist. The old Verilator CSR bench is gone because it drove a RAM window and run/stop control that the spec removed. make test runs everything hermetically. The firmware C files compile on the host against a mocked ethmac, four test binaries with 112 checks and a 2149-case adversarial bounds sweep, and a coverage gate that fails below 100 percent of lines on eb8008.c, dhcp8008.c, udp.c and eb_serve.c. main.c is the one file without a harness. A golden differential test encodes every request with litex’s own EtherbonePacket and decodes the C server’s replies through a pipe. A fork-drift test pins the sha256 of the vendored udp.c and checks that every load-bearing change is still present. A conftest guard makes any UDP send to a non-loopback address raise, after the broadcast probe leaked onto the bench LAN from a test run. Host pytest is 105 tests and the SoC tier is 48, which elaborates all three ethernet configurations through the real CLI and asserts the production csr.csv has no hardware Etherbone. The C tier was mutation-tested with seven planted bugs, and the two bounds-check mutants survived the first time because the sweep checked reply sizes without checking bus side effects. It checks both now. A GitHub Actions workflow runs the whole thing on every push, bootstrapping the LiteX environment and checking out the core repo. make selftest is the hardware tier, five checks over the live board.

Decisions

Open problems

Build logs

Dated working notes for this project, newest first. Corrections land as new entries.