P8X: An 8-Bit TTL CPU — Bus/Backplane Card Architecture¶
Reorganized around a passive backplane with six plug-in CPU cards (plus a PS/2 input card) and the 4×16-bit pointer register bank as the architectural centerpiece. The PC, SP, and MAR of the original SAP-8X design are all subsumed by the pointer bank.
Cards: 1. Control / Microcode card (clock, reset, sequencer, microcode EPROMs, IR, front-panel run controls) 2. Register Bank card (P0–P3, the 16-bit up/down pointer set) 3. ALU card (A, B, T, T2 registers, 74181 ALU, shifter, flags) 4. Memory card (ROM, RAM, address decode) 5. I/O card (toggle-switch input port, LED output port, RS-232 via 6850 ACIA) 6. CF-IDE card (CompactFlash in 8-bit True IDE mode, memory-mapped at $FF10–$FF17) 7. PS/2 card (keyboard + mouse at $FF58–$FF5F; optional — see ps2-card/)
(The optional front-panel LED display card of the first plan was dropped; the I/O card carries the bus-monitor LEDs. A bus test card for bring-up, and the backplane, complete the board set: hardware/. All boards are designed and routed in KiCad, 280 × 140 mm for the plug-in cards; none is fabricated yet.)
Total: ~130 logic ICs across the six CPU cards. Each card is independently testable on the backplane.
(The per-card "≈ N chips" notes below are original design estimates; the
authoritative live count is generators/gen_bom.py → hardware/p8x-bom.csv.)
1. Programmer's Model¶
| Register | Width | Role |
|---|---|---|
| P0 | 16 | Program counter |
| P1, P2 | 16 | General pointers (Forth IP/W, BASIC text pointer, etc.) |
| P3 | 16 | Stack pointer (empty-descending: points at next free byte) |
| A | 8 | Accumulator (ALU operand A) |
| B | 8 | Operand register (ALU operand B) |
| FLAGS | 4 | C, Z, N, V |
| T, T2 | 8 | Hidden microcode temporaries (not programmer-visible) |
Every pointer supports synchronous load, hold, increment, decrement (full 16-bit carry/borrow). The address bus is always driven by the currently selected pointer — there is no separate MAR and no address mux. An instruction fetch is simply "select P0, read memory, increment."
All I/O is memory-mapped in page $FF00–$FFFF.
Memory map (rev E, single-sourced in generators/gen_memmap.py — see the memory card):
- $0000–$17FF EEPROM, 6 KB (monitor + BIOS, ~4.9 KB used; a 28C256, or a 28C64) — shrunk from 8 KB on 2026-09-14
- $1800–$FEFF SRAM, 58 KB (2× 62256); $1800–$1FFF is the OS/BIOS scratch island, the OS loads at $2000, programs at $5900
- $FF00–$FFFF I/O page (RAM disabled here)
Reset forces P0 to $0000 (pointer clear via 74169 synchronous load of zeros — see §4.2).
2. Backplane¶
Passive backplane, DIN 41612 96-pin (rows A/B/C) connectors; the cards were first planned as 100×160 mm Eurocards and are laid out in KiCad at 280×140 mm. Row B is mostly ground guard between the signal rows, but B3–B26 alternate: odd pins are GND, even pins are spare bus lines SPARE12–SPARE23 (rev D); B27 = CLRC, B28 = BSEL, B29 = IRQ (rev C), B30 = SPARE11. See p8x-bus-definition.md for the full pin map.
2.1 Bus signals¶
| Group | Signals | Count | Driven by |
|---|---|---|---|
| Data bus | D0–D7 | 8 | one card per microcycle (one-hot by decode) |
| Address bus | A0–A15 | 16 | Register Bank card (always) |
| Data source select | DOE0–3 | 4 | Control card |
| Data destination select | DLD0–3 | 4 | Control card |
| Pointer select | PSEL0–2 | 3 | Control card |
| Pointer count | PINC, PDEC | 2 | Control card |
| ALU function | ALUS0–3, ALUM, CIN | 6 | Control card |
| Shifter | SH0–SH1 | 2 | Control card |
| Flag latch | LDF | 1 | Control card |
| Clock | CLK, CLK̄ | 2 | Control card |
| Reset | RES̄ | 1 | Control card |
| Power | +5 V, GND | rest | PSU |
≈ 48 signals + power/ground, plus FC/FZ/FN/FV flag lines and 20 spares (SPARE4–11 plus the rev-D row-B SPARE12–23) — fits the 96-pin connector with room to spare.
2.2 Distributed field decoding¶
The 4-bit DOE (data output enable) and DLD (data load) fields are broadcast encoded on the backplane; each card carries its own 74154 (or 74138) decoder and responds only to its assigned codes. This keeps the backplane narrow, makes bus contention structurally impossible (one-hot per field), and means adding a card never requires rewiring the others.
DOE field assignments (who drives D0–D7):
| Code | Source | Card |
|---|---|---|
| 0 | none (bus idle, pulled up) | — |
| 1 | A | ALU |
| 2 | B | ALU |
| 3 | T | ALU |
| 4 | T2 | ALU |
| 5 | ALU result (via shifter) | ALU |
| 6 | FLAGS | ALU |
| 7 | MEM (read) | Memory / I/O |
| 8 | PTRL — low byte of selected pointer | Reg Bank |
| 9 | PTRH — high byte of selected pointer | Reg Bank |
DLD field assignments (who latches D0–D7 at the clock edge):
| Code | Destination | Card |
|---|---|---|
| 0 | none | — |
| 1 | A | ALU |
| 2 | B | ALU |
| 3 | T | ALU |
| 4 | T2 | ALU |
| 5 | FLAGS (restore) | ALU |
| 6 | IR | Control |
| 7 | MEMW — memory/I-O write strobe | Memory / I/O |
| 8 | PTRL — low byte of selected pointer | Reg Bank |
| 9 | PTRH — high byte of selected pointer | Reg Bank |
Note MEMW is just another destination: the Memory and I/O cards decode DLD=7 and generate a write pulse (gated with CLK̄ for clean timing).
3. Control / Microcode Card¶
3.1 Microcode addressing (13 bits → 8K × 32)¶
| Bits | Source |
|---|---|
| 12–5 | IR (opcode, 74273) |
| 4–1 | Step counter (74161) |
| 0 | Selected condition (74151 mux: 0, 1, C, Z, N, V, …) |
3.2 Control word (32 bits, 4× 28C64 EPROM)¶
As implemented (rev C). The authoritative bit map is the w() encoder + header
comment in microcode/genucode.py; this table mirrors it.
| Bits | Field |
|---|---|
| 0–3 | DOE |
| 4–7 | DLD |
| 8–10 | PSEL (P0–P3 + PT=4 + PT2=5 hidden scratch; 3-bit since rev B) |
| 11 | PINC |
| 12 | PDEC |
| 13–16 | ALU S0–S3 |
| 17 | ALU M |
| 18 | CIN (pin, active-low carry) |
| 19 | SH0 (shift left) |
| 20 | SH1 (shift right) |
| 21 | LDF (latch all four flags) |
| 22–24 | FCOND (condition mux: never/always/C/Z/N/V/LT=N^V/LE=(N^V)|Z) |
| 25 | µRESET (step counter → 0, ends instruction) |
| 26 | HALT (gates clock off; resume via front panel) |
| 27 | LDZN (latch Z,N from the bus on loads) |
| 28 | SHCIN (shifter shift-in = C, for rotates) |
| 29 | SETC (force C=1) |
| 30 | CLRC (force C=0) |
| 31 | BSEL (ALU B-input mux: 0=B register, 1=T register) |
Pipeline latch: the 32 EPROM outputs are registered in 4× 74374 clocked on the opposite edge (CLK̄), so glitching ROM outputs never reach the backplane. Non-negotiable for reliability.
3.3 Also on this card¶
- Crystal oscillator + 74161 divider (÷1/2/4/8 selectable)
- Power-on/pushbutton reset (RC + 7414), drives RES̄ and clears step counter and IR
- Front-panel run controls: RUN/HALT toggle, single-STEP pushbutton (debounced 74279), clocked through a 7474 synchronizer so the machine always stops on a microcycle boundary. HALT microcode bit ORs into the same stop logic.
BOM: 4× 28C64, 4× 74374, 74273 (IR), 74161 (step), 74161 (clk div), 74151 (cond), 74154 (DLD decode for IR), 7414, 7474, 74279, osc, glue ≈ 15 chips
4. Register Bank Card¶
The biggest card, and the heart of the machine.
4.1 Structure¶
- 16× 74169 synchronous up/down counters: 4 chips per pointer, RCO→ENT cascaded for full 16-bit carry
- Pointer selection: PSEL → 74139; the selected pointer's outputs are enabled onto an on-card 16-bit pointer bus via 2× 74244 per pointer (8× 74244 total)
- Address bus drivers: pointer bus → 2× 74244 → backplane A0–A15 (always enabled — this card owns the address bus)
- Byte readback: pointer bus hi/lo → 2× 74257 mux → 74244 → data bus (DOE codes 8/9)
- Byte loads: DLD codes 8/9 + PSEL → 74138 → eight load strobes (4 pointers × 2 bytes). A byte load asserts L̄D̄ on just the two 74169s of that byte; the other byte's chips hold
- Inc/Dec: PINC ∨ PDEC → count-enable on the selected pointer's slices (gated through the 74139 selection); PDEC drives U/D̄. Count direction/enable apply to all 4 slices so carry propagates 16 bits
4.2 Reset behavior¶
RES̄ forces a synchronous load of $0000 into P0 (gates the P0 load strobes and pulls the load inputs low via the data-bus pull-downs / forced-zero buffer). One 74244 wired to all-zeros, enabled at reset, does this cleanly.
4.3 Timing note¶
74169s are synchronous: during a microcycle the current value drives the address bus; load/inc/dec take effect at the clock edge. So "read MEM at P3 and decrement P3" in one microcycle uses the pre-decrement address — which is exactly what the empty-descending stack convention wants (write-then-dec for push, inc-then-read for pop).
BOM: 16× 74169, 8× 74244 (select), 2× 74244 (addr drive), 2× 74257 + 74244 (readback), 74139, 74138, zero-buffer 74244, gates ≈ 32 chips
(If one Eurocard gets crowded, this splits naturally into two half-bank cards — P0/P1 and P2/P3 — sharing the bus pinout.)
5. ALU Card¶
- A, B, T, T2: each 74377 (load) + 74244 (bus drive) = 8 chips. A and B feed the 74181 inputs directly
- ALU: 2× 74181 + 74182, function lines straight from backplane
- Shifter: 2× 74157 after the ALU (pass / <<1 / >>1, carry in/out for rotates), then 74244 to the bus
- Flags: 74175 (C, Z, N, V), Z from 74260+gate over the shifter output, latched on LDF. FLAGS↔bus paths for push/pop of status
- Decode: 1× 74154 for DOE codes 1–6, 1× 74154 for DLD codes 1–5
BOM: ≈ 18 chips
6. Memory Card¶
- 28C256 EEPROM, selected when A15 = 0
- 62256 SRAM, selected when A15 = 1 and not the I/O page: 7430 (8-input NAND on A8–A15) detects $FFxx and inhibits RAM CS
- 74245 transceiver to the data bus: direction from read (DOE=7) vs write (DLD=7); enabled only for on-card addresses
- Write pulse: DLD=7 ∧ CLK̄ → W̄Ē, giving address/data setup in the first half-cycle and a clean strobe in the second
BOM: 28C256, 62256, 74245, 7430, 74154 or 74138 + gates ≈ 6 chips
7. I/O Card¶
Decodes the $FFxx page (same 7430 trick) plus A1–A2 via a 74138 → up to 8 port selects.
| Address | Port |
|---|---|
| $FF00 | Switch input: 8 toggle switches → 74244 → data bus on read |
| $FF02 | LED output: 74374 latch → 8 LEDs (write) |
| $FF04–05 | 6850 ACIA control/status + data, RS-232 |
- 6850 ACIA + MAX232 (the one non-TTL concession for RS-232 levels — alternatively 1488/1489 with ±12 V) + baud clock from a 74161 divider chain or a dedicated 2.4576 MHz can ÷16
- Bus-monitor LEDs (passive): 3× 74244 permanently buffering A0–A15 and D0–D7 to LED banks. Costs nothing logically, and with the single-step button on the control card it gives you a full Altair-style "watch the machine think" front panel
- Software handles everything: the monitor program in ROM polls the switches and ACIA — no bus-mastering front panel needed since the ROM bootstraps the machine
BOM: 6850, MAX232, 74244 (switches), 74374+LEDs, 7430, 74138, baud divider, 3× 74244 monitors ≈ 10 chips
Expansion I/O beyond this card. The I/O card owns only
$FF00–$FF0F. Later ports live on their own cards / on the FPGA graphics card and are recorded in p8x-bus-definition.md §5: the 2nd ACIA ($FF08), the MDU ($FF30–$FF3F), the GL graphics port ($FF50–$FF54), and the PS/2 keyboard+mouse window ($FF58–$FF5F, see ps2-card/ — a standalone design). Every I/O symbol is single-sourced ingenerators/gen_memmap.py.
8. Microcode Examples¶
Notation: one line per microcycle. Every instruction begins with the shared fetch cycle.
Fetch (all instructions, step 0)¶
PSEL=P0, DOE=MEM, DLD=IR, PINC
LDA (P1)+ — load A indirect via P1, post-increment¶
0: fetch
1: PSEL=P1, DOE=MEM, DLD=A, PINC, LDF, µRESET
LDA #imm¶
0: fetch
1: PSEL=P0, DOE=MEM, DLD=A, PINC, LDF, µRESET
STA (P2)¶
0: fetch
1: PSEL=P2, DOE=A, DLD=MEMW, µRESET ; add PINC/PDEC variants as separate opcodes
JMP abs¶
0: fetch ; P0 → operand lo
1: PSEL=P0, DOE=MEM, DLD=T, PINC ; target lo → T
2: PSEL=P0, DOE=MEM, DLD=PTRH ; target hi → P0H (no PINC!)
3: PSEL=P0, DOE=T, DLD=PTRL, µRESET ; T → P0L
JZ abs (condition bit selects between two microcode paths)¶
cond=0: steps 1–2 just PINC twice past the operand, µRESET
cond=1: same as JMP abs
JSR abs (pushes address of operand-lo; RTS compensates)¶
0: fetch ; P0 → operand lo = return-2
1: PSEL=P0, DOE=PTRH, DLD=T2 ; return hi → T2
2: PSEL=P0, DOE=PTRL, DLD=T ; return lo → T
3: PSEL=P3, DOE=T2, DLD=MEMW, PDEC ; push hi
4: PSEL=P3, DOE=T, DLD=MEMW, PDEC ; push lo
5: PSEL=P0, DOE=MEM, DLD=T, PINC ; target lo → T
6: PSEL=P0, DOE=MEM, DLD=PTRH ; target hi → P0H
7: PSEL=P0, DOE=T, DLD=PTRL, µRESET ; → P0L
RTS¶
0: fetch
1: PSEL=P3, PINC ; SP → pushed lo
2: PSEL=P3, DOE=MEM, DLD=T, PINC ; lo → T
3: PSEL=P3, DOE=MEM, DLD=T2 ; hi → T2 (wait, hi is at SP now)
— order: push was hi-then-lo, so pop is lo-then-hi: step 2 reads lo, step 3 reads hi ✓
4: DOE=T2, DLD=PTRH (PSEL=P0)
5: DOE=T, DLD=PTRL (PSEL=P0)
6: PSEL=P0, PINC
7: PSEL=P0, PINC, µRESET ; skip the operand bytes
Forth NEXT (P1 = IP, jump indirect through threaded list)¶
0: fetch (the NEXT opcode itself, if implemented as an instruction)
1: PSEL=P1, DOE=MEM, DLD=T, PINC ; word addr lo
2: PSEL=P1, DOE=MEM, DLD=PTRH, PINC → P0H ; word addr hi
3: DOE=T, DLD=PTRL (PSEL=P0), µRESET
9. Suggested Instruction Set Additions over Rev 1¶
Beyond the Rev 1 set (loads/stores, ALU ops, jumps, stack, JSR/RTS):
| Mnemonic | Operation |
|---|---|
| LDA/STA (Pn)+ / (Pn)− / (Pn) | indirect with post-inc / post-dec / plain, n = 1,2 |
| LDP n,#imm16 | load pointer immediate (3 bytes) |
| INP n / DEP n | 16-bit pointer inc/dec |
| TPA n / TAP n (lo/hi) | pointer byte ↔ A transfers |
| PHP / PLP | push/pop flags |
| PSH n / POP n | push/pop a full pointer (microcoded via T/T2) |
Implemented in rev D (pure microcode, opcodes $74–$77) — added to shrink
compiler-generated code, where 16-bit word moves to/from the stack and pointer
loads dominate:
| Mnemonic | Operation |
|---|---|
| PHW a | push the 16-bit word at memory address a (replaces LDA/PHA ×2) |
| PLW a | pop a 16-bit word into memory address a (replaces PLA/STA ×2) |
| LPW1 a / LPW2 a | load pointer P1 / P2 from the 16-bit word at a (replaces LDA/TAPnL/LDA/TAPnH) |
MOVW dst,src ($78) |
16-bit memory→memory move (replaces LDA/STA/LDA/STA) |
PHW/PLW/LPW1/LPW2 are pure microcode. MOVW needs one hardware
addition: a second hidden scratch pointer PT2 (PSEL = 5) as the write cursor,
since a mem→mem move holds two addresses live at once. PT2 is another 74169
counter set on the register-bank card — no backplane change (PSEL is already
3 bits: PSEL0–2 on C20/C21/C27, and U33 already decodes select 5). Making it
work on real hardware also requires upgrading PT from load-only 74377 latches to
74169 counters, because PHW/PLW/LPW (and MOVW) increment PT — a
requirement the earlier "pure-microcode" ops introduced but the regbank card has
not yet been revised for. See the register-bank card theory (rev D).
Tier A — the C-compiler ISA (2026-09, pure microcode, 39 opcodes). The next
step after rev D, from the measured cost breakdown of compiled programs (see
p8x-isa-c-extensions.md): a real 16-bit pointer load,
displacement addressing, and 16-bit memory-word arithmetic. Every one is built
from the existing datapath — PT/PT2 as address scratch, T as the ALU's second
operand, and the condition planes as the carry chain (an ALU step latches C,
the next step routes it to the plane mux, the step after is a C=0/C=1 pair).
No new register, no new bus line; the emulator and the FPGA run the regenerated
u0–u3.bin unchanged; the TTL machine needs its control-store EPROMs reburned.
| Mnemonic | Operation |
|---|---|
LDPn #imm16 ($38–$3A) |
Pn := imm16 in one 3-byte instruction (was the LPLn/LPHn pair, 4 bytes) |
ADDP3 / SUBP3 #imm8 ($3C/$3D) |
P3 ± imm8 — allocate / free a stack frame |
LDA / STA (Pn+d) ($88–$8A / $8C–$8E) |
byte at Pn + d, d an unsigned 8-bit displacement (computed into PT) |
LDW a,(Pn+d) / STW (Pn+d),a ($90–$92 / $94–$96) |
a 16-bit frame local to / from a memory word |
LDW a,#imm8 / #imm16 ($98/$99) |
a 16-bit constant into a memory word (the compiler's most frequent idiom, 10 → 4 bytes) |
ADDW / SUBW / CMPW a,b ($9A–$9C) |
16-bit memory-word arithmetic, carry chained; CMPW sets flags only |
INCW / DECW a ($9E/$9F) |
16-bit increment / decrement in memory |
ADDW / SUBW / CMPW a,#imm8 ($A0–$A2) |
the same with an 8-bit immediate (zero-extended): x + k, pointer stepping, if (n < k) |
ADDW / SUBW / CMPW a,#imm16 ($B1–$B3) |
16-bit immediate: x + &table, if (n == 1300) |
ANDW / ORW / XORW a,b ($B4–$B6), a,#imm8 ($B7–$B9), a,#imm16 ($BA–$BC) |
16-bit bitwise on a memory word; ANDW x,#1 / JZ tests a bit, XORW x,#$FFFF is bitwise NOT |
PHW (Pn+d) ($BD–$BF) |
push the word at Pn + d (high byte first) — a C argument straight from its frame slot, 2 bytes |
LEAW a,(Pn+d) ($A4–$A6) |
word at a := Pn + d — the address of a frame local (arrays, &x) |
LPW3 a ($79) |
P3 := word at a — restore a saved stack pointer |
JMP / BZ / BNZ / BCP / JNC / BLT / BGE / BLE / BGT rel8 ($A8–$B0) |
2-byte relative branches, signed displacement from the next instruction. Taken: 8 steps, A and the flags are clobbered (B kept); not taken: 2 steps, nothing touched. Until the 2026-09-11 speed audit the taken path saved and restored A and the flags (14 steps) — 11 cycles per taken branch against the 3-step absolute form, 6.5% of a compiled program's time — so that was dropped and the compiler's dependent idioms rewritten. The assembler emits them for .relax sources (compiler output) or an explicit .R suffix; .A forces the absolute form, which the compiler uses for every always-taken jump |
Contracts: the memory-to-memory forms clobber A (it is the ALU's only A input)
and latch the flags; d is unsigned; after ADDW/SUBW/CMPW, C is the
16-bit carry / no-borrow and N^V the signed order. Z is the full 16-bit result
on every immediate form (a,#imm8 / a,#imm16, 14 steps: after the low byte
the microcode keeps a 0/1 marker of its Z in T2 and, when the high byte comes
out zero, re-latches Z from the marker through the Z plane — N is 0 in exactly
that case, so LDZN from a 0/1 value is correct) but high byte only on the
a,b forms, which have no spare steps. The rev-D upgrade of PT/PT2 to 74169
counters covers these too.
PHW byte order (changed 2026-09-11): PHW now pushes the high byte first,
so the pushed word lies little-endian at P3+1..P3+2 — the same layout JSR
leaves for a return address, and what LDW a,(P3+d) reads. That is what lets
the C compiler keep its call frames on the hardware stack: arguments pushed with
PHW are plain frame words to the callee. PHW/PLW were only ever used as a
pair, so nothing else observed the order.
Opcode space: 256 slots, 143 used.
10. Build & Bring-Up Order¶
- Backplane + PSU: verify power on every connector, grounds solid
- Control card alone: scope CLK/CLK̄, verify reset, single-step, and that the pipeline latch outputs a stable all-zeros word with blank-pattern EPROMs
- + Register Bank: burn microcode that does nothing but
PSEL=P0, PINCforever → watch the address LEDs count. Then test load, dec, each pointer - + Memory card: program ROM with $EA-style NOPs, microcode the fetch → IR should track ROM contents (probe IR or temporarily bus it)
- + ALU card: registers first (bus loopback A→bus→B), then ALU functions against a truth-table program, then flags
- + I/O card: LED port write from microcode, switch read, then ACIA loopback (TX→RX jumper) before wiring real RS-232
- Monitor program in ROM (
$0000, serial console): examine/modify (E), dump (D), CF init/format/boot (I/F/B), go (G), help (?/H), plus a BIOS jump table at$0100— from there everything else is software. See p8x-monitor.md for the full command reference.
11. Power & Practical Notes¶
- ~130 LS-TTL chips ≈ 1.5–2 A at 5 V; size the PSU at 4–5 A with per-card 10 µF bulk + 0.1 µF per chip
- Keep CLK/CLKB on adjacent backplane pins with guard traces; AC termination (100 Ω + 150 pF to GND) footprints are provided DNP at the far slot — populate only if scope shows ringing. Do not use Thevenin termination: it biases lines into the HCT threshold region and wastes 25 mA per line at idle (see p8x-backplane-design.md §3)
- Wire-wrap or PCB both fine at ≤4 MHz; keep the 74181 carry chain and the 74169 RCO cascades short
- 74169 vs 74193: 74169 is fully synchronous (single clock + direction pin), which is why it's specified here; 74193's dual-clock scheme is glitch-prone in this application