test_intel: phase A — 8080 INTA bus protocol

Replace the synthesised-RST-7 stub with a real INTA bus cycle. When
int_pending && IME, the chip emits status byte 0x23 on D during T1
(M1+INTA+WO̅) and samples the RST opcode external hardware drives on
the data bus during DBIN. Decodes RST n (0xC7..0xFF) and push+vectors.
Multi-byte INTA opcodes (CALL nnn) deferred.

Test rewrites the INT case to install a fixture INTA driver: snoop
SYNC + status byte, latch a pending flag, drive RST 5 (0xEF) on the
data bus during the next DBIN edge. Driver registers AFTER fake_rom
so its late drive overrides fake_rom's program-byte drive on the
same DBIN edge.

Tests: 8080 17→18 passing; test_intel 63→64 passing.

Adds master plan doc autosearch/18_complete_emulation_plan.md
covering phases A-G (this commit completes phase A).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-04-30 04:33:55 +02:00
parent 323366dc53
commit 19f50d9abc
3 changed files with 382 additions and 18 deletions

View File

@ -0,0 +1,329 @@
# Complete Emulation Plan — Phases A-G
This document is the master plan for taking the test_intel chip suite
from "baseline silicon contracts validated" to "real-software emulation
that runs CP/M, ZEXDOC, CPUDIAG, Busicom 141-PF, and DOS-era 8086
programs". It is updated as each phase completes; the sentinel at the
top of each phase reflects status.
## Constraints
- **No frontend or backend modifications.** Velxio core stays
untouched; all work happens under `test/test_intel/`.
- **Clean-room implementation.** No GPL code. Permissive references
(MIT/BSD/zlib/Apache) only, used for cross-validation never copying.
- **Test-first.** Every chip / feature gets a test before any
permanent .c change.
- **Internet research authorized.** Download datasheets, public-domain
ROMs, permissive open-source emulators as references.
- **Document each phase on completion.** Append a "Phase X completed"
section below with: what was done, what was deferred, lessons
learned, test count delta.
## Phases at a glance
| Phase | Scope | Effort | Status |
| --- | --- | --- | --- |
| **A** | 8080 INTA bus cycle | low | ✅ done 2026-04-30 |
| **B** | Z80 ISA polish for ZEXDOC | high | ⏸️ pending |
| **C** | Support chip ecosystem (4001, 4002, 8259, 8253, 8255, 8251, rom-1m) | high | ⏸️ pending |
| **D** | 4004/4040 I/O completion (uses chips from C) | medium | ⏸️ pending |
| **E** | 8086 ISA completion | high | ⏸️ pending |
| **F** | Real software validation (CPUDIAG, ZEXDOC, Busicom, 8088 V2) | medium | ⏸️ pending |
| **G** | Cycle accuracy (optional) | high | ⏸️ deferred |
---
## Phase A — 8080 INTA bus protocol
### Goal
Replace the current "synthesize RST 7 internally" hack in `8080.c`
with a proper INT-acknowledge bus cycle. When the chip detects INT
asserted (with IME=1), it should perform an INTA M1 cycle (status byte
0x23), read the opcode from the data bus, and execute it. External
hardware (an 8259 PIC, or a test fixture) drives the RST opcode onto
the data bus during INTA.
### Deliverables
- Modify `test_8080/8080.c`: replace `if (G.int_pending && G.ime)` block
with a real bus-cycle that emits ST_INTA and reads the data bus.
- Test: drive INT high, drive RST 5 (0xEF) on the bus during INTA,
observe PC = 0x0028 + observe ISR runs.
- Update `test_8080/README.md` status.
### Sources
- [I8080-1975] User's Manual section on Interrupt Acknowledge
- Cross-check against `superzazu/8080`'s INTA implementation
---
## Phase B — Z80 ISA polish for ZEXDOC
### Goal
Bring the Z80 chip from "passes our 11 active tests" to "passes
ZEXDOC" (the documented-flags subset of Frank Cringle's ZEXALL test
ROM). This requires implementing several features that real Z80
software depends on but which our current chip stubs.
### Sub-phases
- **B.1** CB prefix (256 ops): BIT n,r / SET n,r / RES n,r and the
rotates RLC/RRC/RL/RR/SLA/SRA/SLL/SRL on r ∈ B/C/D/E/H/L/(HL)/A.
- **B.2** DDCB / FDCB indexed bit ops: e.g. `BIT 0, (IX+d)` — fetched
as `DD CB d byteOpcode`.
- **B.3** Undocumented X (bit 3) and Y (bit 5) flag bits — copies of
result bits 3/5. ZEXALL fails without these. Apply to all
flag-affecting instructions.
- **B.4** MEMPTR (WZ) internal register — affects bits 3/5 of F after
`BIT n,(HL)` and DD/FD-prefixed BIT. Update list per Sean Young §4.1.
- **B.5** Z80-specific DAA — uses N flag to determine direction
(additive vs subtractive); H-flag table per Sean Young §4.7.
- **B.6** Block I/O exact flags (INI/IND/INIR/INDR/OUTI/OUTD/OTIR/OTDR)
per Sean Young §4.3.
- **B.7** CPI/CPD/CPIR/CPDR with H/PV/Z exactly per Sean Young §4.2.
- **B.8** RLD/RRD instructions.
- **B.9** 16-bit ADC HL,rr / SBC HL,rr with bit-12 half-carry +
16-bit overflow flag.
- **B.10** All 8 NEG aliases (ED 44/4C/54/5C/64/6C/74/7C).
### Deliverables
- ~600 LOC additions to `test_z80/z80.c`.
- New tests under `test_z80/`: per-feature unit tests + ZEXDOC
integration test (runs the 9 KB ROM to completion, verifies the
printed result byte sequence).
- Vendoring of ZEXDOC ROM (public domain, Frank Cringle 1994).
### Sources
- Sean Young, *The Undocumented Z80 Documented* v0.91 (in `pdfs/`)
- Zilog UM008003-1202 (in `pdfs/`)
- Cross-check: `floooh/chips/z80.h` for MEMPTR map
---
## Phase C — Support chip ecosystem
### Goal
Build the supporting chips that real systems used. Without these,
none of our CPUs can run actual programs on the canvas. All chips
follow the existing custom-chip API and have unit tests.
### Sub-phases
- **C.1** `4001` ROM (16-pin DIP, 256 bytes, 4-bit nibble bus matching
4004 SRC protocol; CMROM-strobed; ROM image baked in like rom-32k)
- **C.2** `4002` RAM (16-pin DIP, 80 nibbles + 4 output port lines,
SRC-addressed, CMRAM-strobed)
- **C.3** `8259` PIC — 28-pin, 8 IRQ inputs, INT/INTA cycle to CPU,
programmable vector base. Used by 8080/Z80/8086 for real interrupt
systems.
- **C.4** `8253` PIT — 24-pin, 3 channels of 16-bit countdown timers.
Essential for BIOS-style code (system tick, speaker frequency).
- **C.5** `8255` PPI — 40-pin, three 8-bit ports (A, B, C), 4 modes.
Generic peripheral interface used in many 8080/Z80/8086 systems.
- **C.6** `8251` USART — 28-pin, async serial UART. Enables "hello
world" via terminal emulation.
- **C.7** `rom-1m` — variant of rom-32k with 20-bit address bus
(A0..A19) so 8086 can fetch from CS:IP=0xFFFF0 on canvas.
### Deliverables
- ~1500 LOC across 7 chips.
- Per-chip test file (pin contract + protocol behavior).
- Per-chip README.md.
- Updated `test_buses/README.md` chip table.
### Sources
- Each chip's Intel datasheet (download from bitsavers.org).
---
## Phase D — 4004/4040 I/O completion
### Goal
Wire up the I/O group instructions (WRM/RDM/ADM/SBM/WRR/RDR/WR0..3/
RD0..3) so they actually access RAM/ROM ports through the SRC + CMRAM
mechanism. Requires `4001` and `4002` from Phase C.
### Sub-phases
- **D.1** SRC instruction emits chip-select address on D bus during X2
with appropriate CMROM/CMRAMᵢ strobing, latched by external chip
- **D.2** Subsequent I/O instruction (WRM/RDM/etc.) re-asserts the
selected CMROM/CMRAMᵢ during M2 + X2/X3 to drive R/W to that chip
- **D.3** WRM/RDM/ADM/SBM hit 4002 RAM character cells
- **D.4** WRR/RDR hit 4001 ROM I/O port lines
- **D.5** WR0..WR3 / RD0..RD3 hit 4002 RAM status characters
- **D.6** 4040's BBS reissues the saved SRC at the X2/X3 of the BBS
cycle so the chip selected before the interrupt is re-armed
### Deliverables
- Updates to `test_4004/4004.c` and `test_4040/4040.c`.
- Integration tests using `4001` + `4002` chips on the same board:
4004 reads/writes RAM, drives output port, reads input port.
### Sources
- MCS-4 manual §III.B (in `pdfs/`)
- MCS-40 manual §1 (in `pdfs/`)
---
## Phase E — 8086 ISA completion
### Goal
Bring the 8086 from ~50 opcodes (~30% of ISA) to substantially
complete (~95%). Target: subset of 8088 V2 SingleStepTests passing.
### Sub-phases
- **E.1** Shifts and rotates: SHL/SHR/SAR/ROL/ROR/RCL/RCR with imm or
CL count. Group 2 (0xD0..0xD3).
- **E.2** String ops: MOVSB/MOVSW, CMPSB/CMPSW, SCASB/SCASW, LODSB/
LODSW, STOSB/STOSW + REP/REPE/REPNE prefix handling.
- **E.3** Multiplication / division: MUL r/m8, MUL r/m16, IMUL r/m8,
IMUL r/m16, DIV r/m8, DIV r/m16, IDIV r/m8, IDIV r/m16. Group 3
(0xF6/0xF7).
- **E.4** BCD adjust: DAA, DAS, AAA, AAS, AAM imm8, AAD imm8.
- **E.5** Port I/O: IN AL,imm8 / IN AX,imm8 / IN AL,DX / IN AX,DX
+ OUT counterparts.
- **E.6** Hardware interrupts: NMI vector 2, INTR + INTA cycle reading
vector byte from data bus, INT imm8, INT 3, INTO, IRET.
- **E.7** LDS/LES (load far pointer), LAHF/SAHF, XCHG, XLAT.
- **E.8** Conditional flag-set: SAHF, LAHF.
- **E.9** Group 4 (0xFE) — INC/DEC r/m8.
- **E.10** Undocumented opcodes: POP CS (0x0F), SALC (0xD6).
### Deliverables
- ~800 LOC additions to `test_8086/8086.c`.
- New tests under `test_8086/` for each instruction class.
### Sources
- Intel iAPX 86,88 User's Manual (in `pdfs/`)
- Cross-check: 8086tiny, MartyPC
---
## Phase F — Real software validation
### Goal
Prove correctness by running historic public-domain test programs.
### Sub-phases
- **F.1** **CPUDIAG** on 8080: load Microcosm Associates CPU diagnostic
(1980, public domain) + minimal CP/M-like BDOS jump table; run until
it prints "CPU IS OPERATIONAL"; integration test asserts expected
output sequence.
- **F.2** **ZEXDOC** on Z80: load Frank Cringle's ZEXDOC (subset of
ZEXALL — documented flags only); run for ~minutes of simulated time
(it's a many-CRC test); assert all 67 sub-tests pass.
- **F.3** **8088 V2 SingleStepTests subset** on 8086: load JSON test
cases (initial state + bus trace + final state) for selected
opcodes; verify our chip matches.
- **F.4** **Busicom 141-PF** on 4004: load the original Busicom
calculator firmware; verify display sequence for a known
calculation. (Requires 4001/4002 chips from Phase C.)
### Deliverables
- Integration test files under `test_<chip>/` that wire the CPU + ROM
+ RAM and run the test ROM to completion.
- Vendored public-domain ROMs under `test/test_intel/roms/`:
- `cpudiag.bin` (~2 KB)
- `zexdoc.bin` (~9 KB)
- `busicom_141pf.bin` (~1 KB)
- Test result expectations documented in autosearch/.
### Sources
- CPUDIAG: widely mirrored on Altair-related sites; license is
effectively public-domain (Microcosm Associates, 1980).
- ZEXDOC/ZEXALL: Frank Cringle 1994; public domain.
- Busicom firmware: Intel released to public domain in 2009.
- 8088 V2 SingleStepTests: Daniel Balsom's MartyPC project,
MIT-licensed.
---
## Phase G — Cycle accuracy (optional, deferred)
### Goal
Move from instruction-per-tick to cycle-accurate timing. Necessary
for emulating cycle-counting retro games (Spectrum games, Lotus
Esprit, etc.).
### Sub-phases
- **G.1** Per-opcode cycle counts for all 5 CPUs.
- **G.2** 8086 prefetch queue (4 bytes). Affects self-modifying
code observable behavior.
- **G.3** Z80 contended memory model (Spectrum 16K..32K cycles).
- **G.4** Wait-state insertion via WAIT̅ + READY pin sampling.
This is HUGE work and only valuable for niche use-cases. Skipped
until user asks for it.
---
## Documentation conventions for completed phases
Each completed phase appends a section titled `## Phase X — completed
(YYYY-MM-DD)` with:
- **Delivered**: bullet list of what shipped
- **Deferred**: bullet list of what was originally planned but moved
out of scope
- **Tests delta**: +N passing, +M todo, etc.
- **Files touched**: key paths
- **Lessons / surprises**: notable discoveries during implementation
- **Sources cited**: PDFs / repos / docs actually consulted
Commits made during the phase reference the phase letter in the
subject line (e.g. "test_intel: phase A — 8080 INTA bus protocol").
---
## Phase A — completed (2026-04-30)
### Delivered
- `test_8080/8080.c`: replaced the synthesised-RST-7 stub with a real
INTA bus cycle. When `int_pending && ime`, the chip clears IME +
INTE pin, runs `bus_read(PC, ST_INTA)` to emit status byte 0x23
(M1+INTA+WO̅) on the data bus during T1, then samples the opcode
external hardware (e.g. an 8259 PIC) jams onto D0..D7 during DBIN.
RST n opcodes (0xC7..0xFF, mask 0xC7==0xC7) are decoded and
push+vector executed.
- `test_8080/8080.test.js`: rewrote the INT test to install a
test-fixture INTA driver that snoops SYNC + the status byte to
detect INTA cycles, then drives RST 5 (0xEF) on the data bus during
DBIN. Driver registered AFTER bootCpu's fake_rom so the late drive
overrides the fake_rom's program-byte drive.
### Deferred
- Multi-byte opcodes during INTA (CALL nnn, JMP nnn) — would require
the chip to issue further INTA cycles for operand bytes. Spec
permits but rarely used in practice. The chip currently treats
non-RST INTA opcodes as NOP.
- EI delayed-effect: real 8080 enables INT acknowledge on the
*instruction after* EI so `EI; RET` is atomic. Mine enables
immediately. Minor fidelity gap, no current test exercises it.
### Tests delta
- `test_8080`: 17 passing → **18 passing** (+1, the INT test
promoted from pending-broken to passing).
- Total `test_intel`: 63 → **64 passing**, 16 todo.
### Files touched
- `test/test_intel/test_8080/8080.c`
- `test/test_intel/test_8080/8080.test.js`
### Lessons
- Listener registration order matters when multiple listeners drive
the same pin. fake_rom registers a DBIN listener; an INTA fixture
must register its own DBIN listener LATER so the late drive
overrides. Documented in test comments.
- Two-stage SYNC→DBIN handoff (latch a flag at SYNC, act on DBIN)
works cleanly; the alternative of doing everything in the SYNC
callback fails because fake_rom's later DBIN drive wins.
### Sources cited
- `pdfs/mcs80_users.pdf` (Intel 1975) — INTA cycle status word + bus
protocol
- Cross-checked behavior against `superzazu/8080`'s `i8080.c` lines
on its `interrupt()` function (no code copied).
---
## Phase B — Z80 ISA polish for ZEXDOC — STARTING
(Updates appended as work proceeds.)

View File

@ -323,16 +323,32 @@ static bool cond_met(uint8_t cc) {
/* ─── One-instruction step ──────────────────────────────────────────────── */
static void step(void) {
/* Service interrupt if pending and IME — simple model: synthesise an
implicit RST 7 (vector 0x0038). Real 8080 reads the RST opcode from
the data bus during INTA; we approximate. */
/* Service interrupt if pending and IME. Real 8080 INT acknowledge:
run an INTA bus cycle (status byte 0x23 = M1+INTA+WO̅), read the
opcode that external hardware (8259 PIC or hard-wired logic)
jams onto the data bus, and execute it. The opcode is typically
a RST n (0xC7..0xFF); we support that fully. Other opcodes
during INTA are documented to work too (e.g. CALL nnn) but
require multi-byte fetches with INTA status deferred. */
if (G.int_pending && G.ime) {
G.ime = false;
G.int_pending = false;
G.halted = false;
vx_pin_write(G.inte, 0);
push16(G.pc);
G.pc = 0x0038;
/* Address driven on A0..A15 during INTA is undefined per
datasheet; we drive PC for clarity. */
uint8_t opcode = bus_read(G.pc, ST_INTA);
if ((opcode & 0xC7) == 0xC7) {
/* RST n */
push16(G.pc);
G.pc = (uint16_t)((opcode >> 3) & 7) * 8;
}
/* If opcode is a non-RST (e.g. CALL nnn = 0xCD), full fidelity
would require additional INTA bus cycles to fetch the
operand bytes not implemented yet. Treat as a NOP. */
return;
}
if (G.halted) {

View File

@ -414,31 +414,50 @@ describe('Intel 8080 chip', () => {
});
describe('interrupts', () => {
it.skipIf(skip)('INT pin triggers RST-vector when EI was executed', async () => {
it.skipIf(skip)('INT pin + INTA bus cycle vectors via RST opcode jammed on bus', async () => {
// EI ; loop: NOP ; JMP loop
// ISR at 0x0038 (RST 7): MVI A, 0x55 ; STA 0x8000 ; HLT
// ISR at 0x0028 (RST 5): MVI A, 0x55 ; STA 0x8000 ; HLT
const program = new Uint8Array(0x40);
program.fill(I8080.NOP);
program[0x00] = I8080.EI;
program[0x01] = I8080.JMP; program[0x02] = 0x01; program[0x03] = 0x00;
program[0x38] = I8080.MVI_A; program[0x39] = 0x55;
program[0x3A] = I8080.STA; program[0x3B] = 0x00; program[0x3C] = 0x80;
program[0x3D] = I8080.HLT;
program[0x28] = I8080.MVI_A; program[0x29] = 0x55;
program[0x2A] = I8080.STA; program[0x2B] = 0x00; program[0x2C] = 0x80;
program[0x2D] = I8080.HLT;
const { board, ram } = await bootCpu(program);
// Run a few cycles to let EI take effect.
// INTA bus driver. Two-stage:
// 1. Watch SYNC. When high, sample the status byte. If INTA bit
// is set (status 0x23 = M1 + INTA + WO̅), latch a flag.
// 2. Watch DBIN AFTER bootCpu (so we register last and our drive
// overrides the fake_rom's drive on the same DBIN edge).
// When DBIN rises during a latched INTA cycle, drive the RST
// opcode on D — the chip will read it.
let intaPending = false;
board.watchNet('SYNC', (high) => {
if (!high) return;
const status = board.readBus('D', 8);
intaPending = (status & 0x01) !== 0;
});
board.watchNet('DBIN', (high) => {
if (!high || !intaPending) return;
intaPending = false;
const RST5 = 0xEF;
for (let i = 0; i < 8; i++) {
board.setNet(`D${i}`, ((RST5 >> i) & 1) === 1);
}
});
// Let EI + a few NOPs run.
board.advanceNanos(CLOCK_NS * 20);
// Pulse INT high. The 8080 expects an RST opcode on the data bus
// during interrupt acknowledge; our fake ROM doesn't model INTA.
// For a first-cut test we mark this todo until we extend the fake
// ROM with an INTA hook.
// Pulse INT high.
board.setNet('INT', true);
board.advanceNanos(CLOCK_NS * 50);
board.advanceNanos(CLOCK_NS * 5);
board.setNet('INT', false);
// Let the ISR run to HLT.
board.advanceNanos(CLOCK_NS * 200);
// This assertion will only pass once the chip + harness understand
// the INTA bus protocol. Until then it's expected to fail/skip.
expect(ram.peek(0x8000)).toBe(0x55);
board.dispose();
});