/** * Z80 software-validation integration test. * * Runs Frank Cringle's ZEXDOC ROM (1994, public domain) — the * documented-flags subset of ZEXALL. Tests the Z80's documented * instructions exhaustively by computing a CRC over many invocations * with varied register/flag inputs and comparing against a known * reference CRC. * * Format: CP/M .COM file, loaded at 0x0100. Uses BDOS calls 2 (print * char in E) and 9 (print string at DE until '$'). Same harness as * the 8080 CPUDIAG test. * * Note: a full ZEXDOC run on real silicon takes ~hours; in our * simulator each test takes minutes. We run for a fixed time budget * and check what was printed. A passing test prints * "....OK" per sub-test; a failure prints * "....ERROR" with the bad CRC. We assert no ERROR seen. */ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { BoardHarness } from '../src/BoardHarness.js'; import { chipWasmExists } from '../src/helpers.js'; const CHIP = 'z80'; const skip = !chipWasmExists(CHIP); const here = dirname(fileURLToPath(import.meta.url)); const romPath = (name) => resolve(here, '..', 'roms', name); const CLOCK_HZ = 4_000_000; const CLOCK_NS = Math.round(1e9 / CLOCK_HZ); function fullPinMapZ80() { const m = { M1: 'M1', MREQ: 'MREQ', IORQ: 'IORQ', RD: 'RD', WR: 'WR', RFSH: 'RFSH', HALT: 'HALT', WAIT: 'WAIT', INT: 'INT', NMI: 'NMI', RESET: 'RESET', BUSREQ: 'BUSREQ', BUSACK: 'BUSACK', CLK: 'CLK', VCC: 'VCC', GND: 'GND', }; for (let i = 0; i < 16; i++) m[`A${i}`] = `A${i}`; for (let i = 0; i < 8; i++) m[`D${i}`] = `D${i}`; return m; } function buildSystemImage(programBytes) { const mem = new Uint8Array(0x10000); // 0x0000: JMP 0x0100 (start of TPA) mem[0x0000] = 0xC3; mem[0x0001] = 0x00; mem[0x0002] = 0x01; // 0x0005: JMP 0xFE00 (BDOS entry — placed high to avoid collision // with ZEXDOC.COM data which extends to ~0x21A9). Note // ZEXDOC reads this pointer to set SP, so stack grows down // from 0xFE00 — plenty of room above the program. mem[0x0005] = 0xC3; mem[0x0006] = 0x00; mem[0x0007] = 0xFE; // BDOS handler at 0xFE00 — 8080 byte sequence (Z80-compatible). const bdos = [ 0x79, // MOV A, C 0xFE, 0x09, // CPI 9 0xCA, 0x20, 0xFE, // JZ 0xFE20 0xFE, 0x02, // CPI 2 0xCA, 0x40, 0xFE, // JZ 0xFE40 0xC9, // RET ]; for (let i = 0; i < bdos.length; i++) mem[0xFE00 + i] = bdos[i]; // Print-string at 0xFE20 const ps = [0x1A, 0xFE, 0x24, 0xC8, 0xD3, 0x01, 0x13, 0xC3, 0x20, 0xFE]; for (let i = 0; i < ps.length; i++) mem[0xFE20 + i] = ps[i]; // Print-char at 0xFE40 const pc = [0x7B, 0xD3, 0x01, 0xC9]; for (let i = 0; i < pc.length; i++) mem[0xFE40 + i] = pc[i]; for (let i = 0; i < programBytes.length; i++) { mem[0x0100 + i] = programBytes[i]; } return mem; } describe('Z80 software validation', () => { it.skipIf(skip)('runs ZEXDOC for a time budget and produces no ERROR', async () => { const program = readFileSync(romPath('zexdoc.bin')); const board = new BoardHarness(); await board.addChip(CHIP, fullPinMapZ80()); const sysmem = buildSystemImage(program); // Z80 reads via MREQ̅+RD̅; both active-low. Use installFakeRam to // back the full 64 KB. const ram = board.installFakeRam(0x10000, { addrPrefix: 'A', addrWidth: 16, dataPrefix: 'D', dataWidth: 8, rd: 'RD', rdActiveLow: true, wr: 'WR', cs: 'MREQ', baseAddr: 0, }); for (let i = 0; i < 0x10000; i++) ram.poke(i, sysmem[i]); // Capture output via Z80 OUT (port 1). Z80 OUT (n),A drives // address bus with A in upper byte and n in lower byte. We watch // WR̅ falling AND IORQ̅ asserted to identify I/O writes. const output = []; board.watchNet('WR', (state) => { if (state !== false) return; if (board.getNet('IORQ') !== false) return; // memory write — skip const port = board.readBus('A', 8); // low byte of address if (port === 0x01) { output.push(board.readBus('D', 8)); } }); // Boot board.setNet('WAIT', true); board.setNet('INT', true); // active-low — high = no interrupt board.setNet('NMI', true); board.setNet('BUSREQ', true); board.setNet('RESET', false); board.advanceNanos(CLOCK_NS * 4); board.setNet('RESET', true); // Run for a substantial time budget. ZEXDOC is many sub-tests; we // sample whatever fits in the budget. const cycles = 5_000_000; for (let i = 0; i < cycles; i++) board.advanceNanos(CLOCK_NS); // Build text in chunks (output may be huge). let text = ''; for (let i = 0; i < output.length; i += 4096) { text += String.fromCharCode(...output.slice(i, i + 4096)); } board.dispose(); // We expect at least the ZEXDOC header to appear. The header // typically reads "Z80 instruction exerciser\r\n..." or similar. expect(text.length, 'no output produced — chip may not have started').toBeGreaterThan(20); // No test should fail with ERROR. (If ZEXDOC didn't get to any // tests within the budget, this passes vacuously — the length // assertion above guards against that.) expect(text).not.toMatch(/ERROR/i); // The header confirms the chip got far enough to invoke BDOS. expect(text).toMatch(/exerciser/i); }, 120_000); });