Why a third try
I had three major revisions, s8008, v8008 and finally b8008. ( My thinking was soft8008, verbose8008 and block8008. s8008 reached hardware before I understood its timing model was wrong. v8008 had too much shared logic and conditional soup to debug. b8008 started from a clean slate in November 2025 with one module per block-diagram box, and every module got its own testbench before the top level existed.
Architecture
About 27 modules, each one a box from the Intel diagram: state timing generator, machine cycle control, instruction register, instruction decoder, register and ALU control, register file, temp registers, ALU, carry look-ahead, condition flags, address stack, stack pointer, interrupt and ready flip-flops, I/O buffer, memory mux. Each is 50 to 100 lines and does one thing. The stack pointer is 69 lines and knows two words, push and pop. It has no idea what CALL or RST or an interrupt is.

There is no separate program counter, because the real chip doesn't have one. The PC is whichever of the eight 14-bit address-stack registers the stack pointer selects. CALL is the SP moving on and the old slot keeps the return address. RET is the SP moving back. Seven nested returns, and the eighth CALL wraps onto the oldest. I built it with a separate PC first and a family of workaround flags grew around it. Rebuilding it as the diagram shows deleted all of them.
Every documented instruction takes the datasheet's 5, 8 or 11 T-states. Fetch cycles of multi-cycle instructions end at T3, not-taken conditionals end early, and a regression counts simulated states per timing class against docs/isa.json. Interrupts are recognized only at instruction boundaries, per Figure 2 of the User's Manual. READY parks the CPU in a real WAIT state between T2 and T3.
The ALU's adder is the carry look-ahead block. For a while that block compiled and had a testbench and nothing instantiated it, and the toolchain was inferring the adder from numeric_std. It's wired in now, with a 9-bit carry-out and an exhaustive sweep behind it.
The whole design runs in one 25 MHz clock domain. phi1 and phi2 are one-cycle enable pulses off a phase_clocks state machine, with the pulse widths parameterized so the 8008's 0.8 and 0.6 microsecond phases hold at any system clock. The internal bus is a mux. An early version used tri-state signals internally and they turned out to be the critical path.
Hardware
The board is a Lattice ECP5-5G Versa development kit, so there's no PCB of mine in this project. The 100 MHz oscillator goes through an on-chip PLL to 25 MHz, and everything runs on that. A T-state measures about 4.4 microseconds against the real chip's 4 at 500 kHz.
Serial is the on-board FTDI at 115200 8N1, local echo off, DEL for rubout because that's what the 1976 software expects. ROM and RAM are block RAM inside the FPGA. Firmware gets patched into the bitstream with `ecpbram` after place and route, which takes 0.66 seconds against a 4 minute resynthesis.
DIP switch 1 is reset. Switches 2 through 8 are disconnected after too many sessions where the wrong resting position halted the CPU or put the LEDs into a capture mode. The seven user LEDs show one RAM byte, bit n to LED n. LED0 is the CPU-running light.
There's a logic analyzer header carrying INT, phi1, phi2, SYNC, the three state bits and the eight data lines, with a DSView config and Python decoders in the repo for reading traces back into instruction streams.
In January 2026 I tried an AT28C64B EEPROM as external ROM, the way the SIM8-01 did it. It executed code when it booted, and boot was unreliable for months. The TXS0108E level shifters were being run past absolute maximum. ROM went back inside the FPGA.
Firmware
Toolchain is GHDL, Yosys, nextpnr-ecp5 and ecppack from OSS CAD Suite on macOS. 8008 assembly goes through the AS assembler in 8080 syntax. Everything goes through the Makefile.
b8008_monitor. ROM 4 KB at 0x0000, RAM 12 KB at 0x1000. The monitor...
Read more »
Robert Rico