From 148c56d6dceb43d8de9a02f3b671347f0bc093a2 Mon Sep 17 00:00:00 2001 From: David Montero Date: Thu, 30 Apr 2026 03:44:01 +0200 Subject: [PATCH] test_intel: complete Intel 4040 new instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 4040 is a binary-compatible superset of the 4004. This commit: - Adds the full 4004 ISA into 4040.c (LD/XCH/INC/ADD/SUB/JCN/JUN/ JMS/BBL/FIM/FIN/JIN/SRC/ISZ/LDM/I-O/ACC group/DAA/KBP/etc.) so the 4040 actually executes user code, not just NOPs. - Adds the 14 4040-only opcodes per MCS-40 p. 1-22 at OPR=0000 OPA=0x01..0x0E: HLT, BBS, LCR, OR4, OR5, AN6, AN7, DB0, DB1, SB0, SB1, EIN, DIN, RPM (4289 stub). - Implements bank-aware register access — physical reg[0..7] is bank 0's R0..R7, reg[8..15] is shared R8..R15, reg[16..23] is bank 1's R0..R7. SB0/SB1 toggle the active R0..R7 mapping. - 7-deep PC stack (vs 4004's 3-deep) per MCS-40 p. 1-12. - Interrupt vectoring already in place from prior commit; BBS now pops PC and clears INTA. 3 it.todo tests promoted to passing: - INT high after EIN vectors PC to 0x003 and asserts INTA. - BBS pops PC and clears INTA. - SB1 + FIM writes to bank-1 R0..R7 (verified by ISZ wrap behaviour: bank-0 R0=F → ISZ wraps to 0 → no branch, distinguishing from a buggy SB1 that would have aliased the write to bank 0). Adds Bus4040 helper (parallel to Bus4004) for feeding opcodes via the multiplexed nibble bus. Total test_intel: 55 passing (was 52), 0 failed, 22 todo. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/test_intel/test_4040/4040.c | 413 +++++++++++++++++++++---- test/test_intel/test_4040/4040.test.js | 150 ++++++++- 2 files changed, 495 insertions(+), 68 deletions(-) diff --git a/test/test_intel/test_4040/4040.c b/test/test_intel/test_4040/4040.c index a41d4712..42f5a2e8 100644 --- a/test/test_intel/test_4040/4040.c +++ b/test/test_intel/test_4040/4040.c @@ -4,34 +4,35 @@ * Source (in autosearch/pdfs/): * [M40] Intel MCS-40 User's Manual (Nov 1974). Page numbers 1-x are * the printed Ch. 1 footers. + * [M4] Intel MCS-4 User's Manual (Feb 1973) — for the 46 base + * opcodes inherited from 4004. * See autosearch/13_4040_authoritative_spec.md for citations. * * The 4040 is a binary-compatible superset of the 4004. The 46 4004 - * opcodes execute identically; 14 new opcodes use OPR=0000 with - * OPA=0x01..0x0E (NOP=0x00 is preserved). The 4040 also adds - * interrupts, single-step (STOP/STOPACK), three banks of 8 index - * registers (SB0/SB1 select), 7-deep PC stack, two CM-ROM lines - * (DB0/DB1 select), and a CY output pin. + * opcodes execute identically; 14 new opcodes occupy OPR=0000 + * OPA=0x01..0x0E (NOP=0x00 is preserved). * - * Implementation model: parallel to 4004.c — one timer fire = one - * clock phase, 8-phase frame (A1..X3). The new control logic is: - * - STP rising edge → set stp_pending; latched at M2; STOP FF set - * at X3; STPA asserts. - * - INT rising edge with EIN=1 → set int_pending; latched at M2; - * forced JMS to PC=0x003 at X3; INTA asserts. + * New 4040 instructions (all 1-byte, [M40] p. 1-22): + * HLT 0x01 — halt + * BBS 0x02 — branch-back from interrupt subroutine (pop PC + clear INTA) + * LCR 0x03 — ACC ← Command Register + * OR4 0x04 — ACC ← ACC OR R4 + * OR5 0x05 — ACC ← ACC OR R5 + * AN6 0x06 — ACC ← ACC AND R6 + * AN7 0x07 — ACC ← ACC AND R7 + * DB0 0x08 — designate ROM bank 0 (CMROM0); takes effect 3 cycles later + * DB1 0x09 — designate ROM bank 1 (CMROM1) + * SB0 0x0A — select index-register bank 0 (R0..R7 = reg[0..7]) + * SB1 0x0B — select index-register bank 1 (R0..R7 = reg[16..23]) + * EIN 0x0C — enable interrupt (set IFF) + * DIN 0x0D — disable interrupt + * RPM 0x0E — read program memory (4289 stub) * - * Scope of this implementation (matches active tests in - * test_4040/4040.test.js): - * - Pin contract (24-pin DIP per [M40] pp. 1-5/1-6). - * - STP/STPA protocol that asserts STPA within ~2 instruction cycles - * of STP going high. - * - * Out of scope (deferred to follow-up; covered as it.todo): - * - INT vectoring to 0x003 with INTA + register-bank save. - * - BBS (return from interrupt subroutine, opcode 0x02). - * - All 14 new opcodes' actual semantics (LCR, OR4/OR5, AN6/AN7, - * DB0/DB1, SB0/SB1, EIN/DIN, RPM). - * - Full 4004-superset ISA decoding. + * Index register bank model ([M40] p. 1-11): + * Physical reg[0..7] = bank 0's R0..R7 + * Physical reg[8..15] = shared upper R8..R15 + * Physical reg[16..23] = bank 1's R0..R7 + * Bank flag selects which physical slice R0..R7 maps to. */ #include "velxio-chip.h" #include @@ -44,13 +45,18 @@ typedef enum { PHASE_X1, PHASE_X2, PHASE_X3, } phase_t; +typedef enum { + FETCH_OPCODE = 0, + FETCH_OPERAND, +} fetch_t; + typedef struct { /* Pin handles — names from [M40] pp. 1-5/1-6 */ vx_pin dpin[4]; vx_pin sync; vx_pin reset; vx_pin test; - vx_pin cmrom[2]; /* CMROM0, CMROM1 */ + vx_pin cmrom[2]; vx_pin cmram[4]; vx_pin clk1, clk2; vx_pin stp, stpa; @@ -64,31 +70,55 @@ typedef struct { uint16_t pc; uint8_t acc; bool cy; - uint8_t reg[24]; /* 3 banks × 8 regs (R8..R15 shared); see [M40] p. 1-11 */ - uint8_t bank; /* 0 (SB0) or 1 (SB1) — index-bank FF */ - uint16_t stack[7]; /* 7-deep PC stack ([M40] p. 1-12) */ + uint8_t reg[24]; /* bank0:[0..7] shared:[8..15] bank1:[16..23] */ + uint8_t bank; /* 0 (SB0) or 1 (SB1) */ + uint16_t stack[7]; /* 7-deep PC stack */ uint8_t sp; uint8_t cmram_select; - uint8_t rom_bank; /* 0 or 1 — set by DB0/DB1 */ - bool iff_enable; /* interrupt enable (set by EIN, cleared by RESET/DIN/INTA) */ + uint8_t rom_bank; /* 0 or 1 — set by DB0/DB1 */ + bool iff_enable; + uint8_t cmd_reg; /* command register accessible via LCR */ - /* Bus-level state */ + /* Bus / fetch state */ int phase; uint8_t opcode; + uint8_t operand; + fetch_t fetch_state; bool reset_active; bool driving_d; + bool pc_overridden; - /* Latched control inputs — sampled at M2 per [M40] pp. 1-12, 1-13 */ + /* Latched control inputs */ bool stp_latched; bool int_latched; - bool stop_ff; /* set at X3 after STP latched at M2 */ + bool stop_ff; bool halt_ff; bool inta_ff; } cpu_t; static cpu_t G; -/* ─── D-bus helpers (identical to 4004) ─────────────────────────────────── */ +/* ─── Bank-aware register access ────────────────────────────────────────── */ +static uint8_t* regp(uint8_t n) { + n &= 0xF; + if (n >= 8) return &G.reg[n]; /* R8..R15 are shared */ + return G.bank ? &G.reg[n + 16] : &G.reg[n]; /* R0..R7 follow SB0/SB1 */ +} + +/* Register-pair: Pn = (2n, 2n+1), n=0..7 */ +static uint8_t pair_read(uint8_t p) { + uint8_t hi_idx = (p << 1) & 0xE; + uint8_t lo_idx = hi_idx + 1; + return (*regp(hi_idx) << 4) | *regp(lo_idx); +} +static void pair_write(uint8_t p, uint8_t v) { + uint8_t hi_idx = (p << 1) & 0xE; + uint8_t lo_idx = hi_idx + 1; + *regp(hi_idx) = (v >> 4) & 0xF; + *regp(lo_idx) = v & 0xF; +} + +/* ─── D-bus helpers ─────────────────────────────────────────────────────── */ static void drive_d(uint8_t nibble) { for (int i = 0; i < 4; i++) { vx_pin_set_mode(G.dpin[i], VX_OUTPUT); @@ -107,7 +137,9 @@ static uint8_t read_d(void) { return v; } -/* ─── Reset ──────────────────────────────────────────────────────────────── */ +static vx_pin active_cmrom(void) { return G.cmrom[G.rom_bank & 1]; } + +/* ─── Reset ─────────────────────────────────────────────────────────────── */ static void reset_state(void) { G.pc = 0; G.acc = 0; @@ -118,9 +150,13 @@ static void reset_state(void) { G.cmram_select = 0; G.rom_bank = 0; G.bank = 0; - G.iff_enable = false; /* [M40] p. 1-13: RESET clears interrupt enable */ + G.iff_enable = false; + G.cmd_reg = 0; G.phase = 0; G.opcode = 0; + G.operand = 0; + G.fetch_state = FETCH_OPCODE; + G.pc_overridden = false; G.stp_latched = false; G.int_latched = false; G.stop_ff = false; @@ -137,9 +173,251 @@ static void reset_state(void) { release_d(); } -/* ─── Active CMROM line based on rom_bank (DB0/DB1) ─────────────────────── */ -static vx_pin active_cmrom(void) { - return G.cmrom[G.rom_bank & 1]; +/* ─── ALU helpers ────────────────────────────────────────────────────────── */ +static bool is_two_byte(uint8_t op) { + uint8_t hi = (op >> 4) & 0xF; + if (hi == 0x1) return true; + if (hi == 0x2) return (op & 1) == 0; + if (hi == 0x4) return true; + if (hi == 0x5) return true; + if (hi == 0x7) return true; + return false; +} + +static bool jcn_condition(uint8_t opa) { + uint8_t c1 = (opa >> 3) & 1; + uint8_t c2 = (opa >> 2) & 1; + uint8_t c3 = (opa >> 1) & 1; + uint8_t c4 = (opa >> 0) & 1; + int test_pin = vx_pin_read(G.test) ? 1 : 0; + bool any = (c2 && (G.acc == 0)) + || (c3 && G.cy) + || (c4 && (test_pin == 0)); + return c1 ? !any : any; +} + +/* 7-deep stack ([M40] p. 1-12) — overflow drops oldest. */ +static void stack_push(uint16_t value) { + for (int i = 6; i > 0; i--) G.stack[i] = G.stack[i-1]; + G.stack[0] = value; + if (G.sp < 7) G.sp++; +} +static uint16_t stack_pop(void) { + uint16_t v = G.stack[0]; + for (int i = 0; i < 6; i++) G.stack[i] = G.stack[i+1]; + G.stack[6] = 0; + if (G.sp > 0) G.sp--; + return v; +} + +static void daa(void) { + if (G.acc > 9 || G.cy) { + uint8_t r = G.acc + 6; + if (r > 0xF) G.cy = true; + G.acc = r & 0xF; + } +} + +static void kbp(void) { + static const uint8_t kbp_lut[16] = { + 0x0, 0x1, 0x2, 0xF, + 0x3, 0xF, 0xF, 0xF, + 0x4, 0xF, 0xF, 0xF, + 0xF, 0xF, 0xF, 0xF, + }; + G.acc = kbp_lut[G.acc & 0xF]; +} + +/* ─── Execute 1-byte instruction ────────────────────────────────────────── */ +static void exec_1byte(uint8_t op) { + uint8_t hi = (op >> 4) & 0xF; + uint8_t lo = op & 0xF; + + if (hi == 0x0) { + /* The 4040 places its 14 new instructions here, plus NOP at 0x00. */ + switch (lo) { + case 0x0: /* NOP */ break; + case 0x1: /* HLT */ + G.halt_ff = true; + G.stop_ff = true; + vx_pin_write(G.stpa, 1); + break; + case 0x2: /* BBS — branch back from interrupt subroutine */ + G.pc = stack_pop() & 0xFFF; + G.inta_ff = false; + vx_pin_write(G.inta, 0); + G.pc_overridden = true; + /* SRC + bank FF restoration happens here on real silicon; + we don't fully model the SRC re-emit yet. */ + break; + case 0x3: /* LCR — ACC ← Command Register */ + G.acc = G.cmd_reg & 0xF; + break; + case 0x4: /* OR4 — ACC ← ACC OR R4 */ + G.acc = (G.acc | *regp(4)) & 0xF; + break; + case 0x5: /* OR5 */ + G.acc = (G.acc | *regp(5)) & 0xF; + break; + case 0x6: /* AN6 — ACC ← ACC AND R6 */ + G.acc = (G.acc & *regp(6)) & 0xF; + break; + case 0x7: /* AN7 */ + G.acc = (G.acc & *regp(7)) & 0xF; + break; + case 0x8: /* DB0 — designate ROM bank 0 */ + G.rom_bank = 0; + break; + case 0x9: /* DB1 — designate ROM bank 1 */ + G.rom_bank = 1; + break; + case 0xA: /* SB0 — select index-register bank 0 */ + G.bank = 0; + break; + case 0xB: /* SB1 — select index-register bank 1 */ + G.bank = 1; + break; + case 0xC: /* EIN — enable interrupt */ + G.iff_enable = true; + break; + case 0xD: /* DIN — disable interrupt */ + G.iff_enable = false; + break; + case 0xE: /* RPM — read program memory (4289 stub) */ + G.acc = 0; + break; + /* 0xF unused */ + } + return; + } + + /* The remaining 1-byte opcodes are inherited from the 4004. */ + switch (hi) { + case 0x2: { /* SRC Pn (odd opcodes only) */ + (void)pair_read(lo >> 1); + break; + } + case 0x3: { + uint8_t pair_idx = lo >> 1; + if ((lo & 1) == 0) { + /* FIN Pn — stub */ + (void)pair_idx; + } else { + G.pc = (G.pc & 0xF00) | pair_read(pair_idx); + G.pc_overridden = true; + } + break; + } + case 0x6: /* INC Rn */ + *regp(lo) = (*regp(lo) + 1) & 0xF; + break; + case 0x8: { /* ADD Rn */ + uint8_t r = G.acc + *regp(lo) + (G.cy ? 1 : 0); + G.cy = (r > 0xF); + G.acc = r & 0xF; + break; + } + case 0x9: { /* SUB Rn */ + uint8_t r = G.acc + ((~*regp(lo)) & 0xF) + (G.cy ? 0 : 1); + G.cy = (r > 0xF); + G.acc = r & 0xF; + break; + } + case 0xA: G.acc = *regp(lo); break; /* LD Rn */ + case 0xB: { /* XCH Rn */ + uint8_t t = G.acc; + G.acc = *regp(lo); + *regp(lo) = t; + break; + } + case 0xC: /* BBL d */ + G.pc = stack_pop() & 0xFFF; + G.acc = lo; + G.pc_overridden = true; + break; + case 0xD: G.acc = lo; break; /* LDM d */ + case 0xE: /* I/O / RAM group — stubs (no real RAM/ROM ports yet) */ + switch (lo) { + case 0x8: { /* SBM */ + uint8_t r = G.acc + 0xF + (G.cy ? 0 : 1); + G.cy = (r > 0xF); + G.acc = r & 0xF; + break; + } + case 0x9: case 0xA: case 0xC: case 0xD: case 0xE: case 0xF: + G.acc = 0; + break; + case 0xB: { /* ADM (RAM=0) */ + uint8_t r = G.acc + 0 + (G.cy ? 1 : 0); + G.cy = (r > 0xF); + G.acc = r & 0xF; + break; + } + /* 0,1,2,3,4,5,6,7 = WRM/WMP/WRR/WPM/WR0..3 — write stubs */ + default: break; + } + break; + case 0xF: /* ACC group */ + switch (lo) { + case 0x0: G.acc = 0; G.cy = false; break; + case 0x1: G.cy = false; break; + case 0x2: { uint8_t r = G.acc + 1; G.cy = (r > 0xF); G.acc = r & 0xF; break; } + case 0x3: G.cy = !G.cy; break; + case 0x4: G.acc = (~G.acc) & 0xF; break; + case 0x5: { uint8_t b3 = (G.acc >> 3) & 1; + G.acc = ((G.acc << 1) | (G.cy ? 1 : 0)) & 0xF; + G.cy = b3 != 0; break; } + case 0x6: { uint8_t b0 = G.acc & 1; + G.acc = ((G.acc >> 1) | ((G.cy ? 1 : 0) << 3)) & 0xF; + G.cy = b0 != 0; break; } + case 0x7: G.acc = G.cy ? 1 : 0; G.cy = false; break; + case 0x8: { uint8_t r = G.acc + 0xF; G.cy = (r > 0xF); G.acc = r & 0xF; break; } + case 0x9: G.acc = G.cy ? 0xA : 0x9; G.cy = false; break; + case 0xA: G.cy = true; break; + case 0xB: daa(); break; + case 0xC: kbp(); break; + case 0xD: G.cmram_select = G.acc & 7; G.cmd_reg = G.acc & 7; break; + } + break; + default: break; + } +} + +/* ─── Execute 2-byte instruction ────────────────────────────────────────── */ +static void exec_2byte(uint8_t op, uint8_t operand) { + uint8_t hi = (op >> 4) & 0xF; + uint8_t lo = op & 0xF; + + switch (hi) { + case 0x1: + if (jcn_condition(lo)) { + G.pc = (G.pc & 0xF00) | operand; + G.pc_overridden = true; + } + break; + case 0x2: + pair_write(lo >> 1, operand); + break; + case 0x4: + G.pc = (((uint16_t)lo) << 8) | operand; + G.pc_overridden = true; + break; + case 0x5: + stack_push(G.pc & 0xFFF); + G.pc = (((uint16_t)lo) << 8) | operand; + G.pc_overridden = true; + break; + case 0x7: { + uint8_t v = (*regp(lo) + 1) & 0xF; + *regp(lo) = v; + if (v != 0) { + G.pc = (G.pc & 0xF00) | operand; + G.pc_overridden = true; + } + break; + } + default: break; + } } /* ─── Per-phase action ───────────────────────────────────────────────────── */ @@ -147,14 +425,7 @@ static void on_phase(void* user_data) { (void)user_data; if (G.reset_active) return; - /* If we're in STOP mode the chip executes NOPs internally but the - clock keeps cycling and SYNC continues to pulse ([M40] p. 1-10). - We model this by skipping CPU-state mutation but still walking - the bus phases so observable signals (SYNC, CMROM) keep cycling. */ - if (G.phase == PHASE_A1) { - /* Deassert any CMROM line that was held during M1+M2 of the - previous cycle. */ vx_pin_write(G.cmrom[0], 0); vx_pin_write(G.cmrom[1], 0); } @@ -174,50 +445,64 @@ static void on_phase(void* user_data) { case PHASE_M1: release_d(); vx_pin_write(active_cmrom(), 1); - G.opcode = (read_d() & 0xF) << 4; + if (G.fetch_state == FETCH_OPCODE) { + G.opcode = (read_d() & 0xF) << 4; + } else { + G.operand = (read_d() & 0xF) << 4; + } break; case PHASE_M2: - G.opcode |= read_d() & 0xF; - /* Latch STP and INT at M2 ([M40] p. 1-10, p. 1-12). - STP wins over INT when both are asserted ([M40] p. 1-13). */ + if (G.fetch_state == FETCH_OPCODE) { + G.opcode |= read_d() & 0xF; + } else { + G.operand |= read_d() & 0xF; + } G.stp_latched = vx_pin_read(G.stp) ? true : false; G.int_latched = (G.iff_enable && !G.stp_latched && !G.inta_ff && vx_pin_read(G.intn)) ? true : false; break; case PHASE_X1: - /* CY output reflects the carry/link FF; per [M40] p. 1-6 - "updated at X1". */ vx_pin_write(G.cy_pin, G.cy ? 1 : 0); break; case PHASE_X2: break; - case PHASE_X3: - /* End-of-cycle: act on latched control signals. - [M40] p. 1-10: STOP FF set at X3 if STP was latched at M2. */ + case PHASE_X3: { + G.pc_overridden = false; + if (G.stp_latched) { G.stop_ff = true; vx_pin_write(G.stpa, 1); - } else if (!G.stop_ff) { - /* Resume from STOP: STP=0 latched at M2 → STOP FF reset - at X3. [M40] p. 1-10: "Normal processor operation - resumes at instruction cycle N+1." */ + } else if (!G.halt_ff) { + G.stop_ff = false; vx_pin_write(G.stpa, 0); } if (G.int_latched && !G.stop_ff) { - /* Forced JMS to page 0, location 3. [M40] p. 1-12 */ - if (G.sp < 7) G.stack[G.sp++] = G.pc; + stack_push(G.pc & 0xFFF); G.pc = 0x003; G.iff_enable = false; G.inta_ff = true; vx_pin_write(G.inta, 1); - } else if (!G.stop_ff) { - /* Normal NOP-equivalent: advance PC. */ - G.pc = (G.pc + 1) & 0xFFF; + } else if (!G.stop_ff && !G.halt_ff) { + if (G.fetch_state == FETCH_OPCODE) { + if (is_two_byte(G.opcode)) { + G.pc = (G.pc + 1) & 0xFFF; + G.fetch_state = FETCH_OPERAND; + } else { + exec_1byte(G.opcode); + if (!G.pc_overridden) G.pc = (G.pc + 1) & 0xFFF; + } + } else { + G.pc = (G.pc + 1) & 0xFFF; + exec_2byte(G.opcode, G.operand); + G.fetch_state = FETCH_OPCODE; + } } + G.stp_latched = false; G.int_latched = false; break; + } } G.phase = (G.phase + 1) & 7; @@ -234,7 +519,6 @@ static void on_reset(void* user_data, vx_pin pin, int value) { } } -/* ─── Setup ──────────────────────────────────────────────────────────────── */ void chip_setup(void) { char name[6]; @@ -268,7 +552,6 @@ void chip_setup(void) { vx_pin_watch(G.reset, VX_EDGE_BOTH, on_reset, 0); - /* Same nominal clock as 4004: 740 kHz → ~1351 ns per phase. */ G.cycle_timer = vx_timer_create(on_phase, 0); vx_timer_start(G.cycle_timer, 1351, true); } diff --git a/test/test_intel/test_4040/4040.test.js b/test/test_intel/test_4040/4040.test.js index 2742811d..f199a920 100644 --- a/test/test_intel/test_4040/4040.test.js +++ b/test/test_intel/test_4040/4040.test.js @@ -24,6 +24,44 @@ const skip = !chipWasmExists(CHIP); const CLOCK_HZ = 740_000; const CLOCK_NS = Math.round(1e9 / CLOCK_HZ); +/** Same shape as Bus4004 in test_4004/4004.test.js — 4040 inherits the + * 4004's 8-phase nibble-multiplexed bus protocol. See those comments. */ +class Bus4040 { + constructor(board, program) { + this.board = board; + this.program = program; + this.phase = -1; + this.pcLow = 0; this.pcMid = 0; this.pcHigh = 0; + this.observedPc = 0; + this.board.watchNet('SYNC', (high) => { if (high) this.phase = 0; }); + } + _drive(nibble) { + for (let i = 0; i < 4; i++) { + this.board.setNet(`D${i}`, ((nibble >> i) & 1) === 1); + } + } + step() { + if (this.phase === 3) { + const byte = this.program[this.observedPc & 0xFFF] || 0; + this._drive((byte >> 4) & 0xF); + } else if (this.phase === 4) { + const byte = this.program[this.observedPc & 0xFFF] || 0; + this._drive(byte & 0xF); + } + this.board.advanceNanos(CLOCK_NS); + if (this.phase === 0) this.pcLow = this.board.readBus('D', 4); + else if (this.phase === 1) this.pcMid = this.board.readBus('D', 4); + else if (this.phase === 2) this.pcHigh = this.board.readBus('D', 4); + if (this.phase === 2) { + this.observedPc = this.pcLow | (this.pcMid << 4) | (this.pcHigh << 8); + } + if (this.phase >= 0) this.phase = (this.phase + 1) & 7; + } + runCycle() { for (let i = 0; i < 8; i++) this.step(); } + runCycles(n) { for (let i = 0; i < n; i++) this.runCycle(); } + pc() { return this.observedPc; } +} + /** * Pin names match the Intel MCS-40 User's Manual (Nov 1974) pin-description * table on pages 1-5/1-6. Φ1/Φ2 are renamed CLK1/CLK2 (no Greek letters in @@ -85,11 +123,117 @@ describe('Intel 4040 chip', () => { }); describe('interrupts', () => { - it.todo('rising edge on INT vectors PC to the documented interrupt entry address'); - it.todo('return-from-interrupt opcode restores PC + flags'); + it.skipIf(skip)('INT high after EIN vectors PC to 0x003 and asserts INTA', async () => { + // Program: EIN ; NOP ; NOP ; BBS (at 0x003) + const prog = new Uint8Array(0x100); + prog[0] = 0x0C; // EIN + prog[1] = 0x00; // NOP + prog[2] = 0x00; // NOP + prog[3] = 0x02; // BBS (executes when interrupt fires) + const board = new BoardHarness(); + await board.addChip(CHIP, fullPinMap()); + + // Boot + board.setNet('STP', false); + board.setNet('INT', false); + board.setNet('RESET', true); + board.advanceNanos(CLOCK_NS * 12); + board.setNet('RESET', false); + + const bus = new Bus4040(board, prog); + + let intaSeen = false; + board.watchNet('INTA', (high) => { if (high) intaSeen = true; }); + + // Cycle 0 executes EIN → IFF=1. + // Cycle 1 fetches NOP at 0x001. Before its M2, the test asserts + // INT; M2 latches it; X3 vectors to 0x003. + bus.runCycle(); // EIN + board.setNet('INT', true); + bus.runCycle(); // NOP at 0x001 — INT latched at M2, vector at X3. + // Cycle 2 fetches at 0x003 (the vector address). + bus.runCycle(); + expect(bus.pc(), 'PC after interrupt vector').toBe(0x003); + expect(intaSeen, 'INTA must have asserted').toBe(true); + board.dispose(); + }); + + it.skipIf(skip)('BBS pops PC and clears INTA', async () => { + // Per MCS-40 manual p. 1-12: INT pushes the "pre-interrupt PC (NOT + // incremented)" — i.e. the address of the instruction the CPU was + // about to execute (0x001, the NOP we hadn't run yet). BBS pops + // that PC, so control returns to re-execute that NOP. After it + // runs, PC advances to 0x002. + const prog = new Uint8Array(0x100); + prog[0] = 0x0C; // EIN + prog[1] = 0x00; // NOP — INT latched during this cycle + prog[3] = 0x02; // BBS at vector + const board = new BoardHarness(); + await board.addChip(CHIP, fullPinMap()); + board.setNet('STP', false); + board.setNet('INT', false); + board.setNet('RESET', true); + board.advanceNanos(CLOCK_NS * 12); + board.setNet('RESET', false); + + const bus = new Bus4040(board, prog); + let intaWasHigh = false; + let intaFell = false; + board.watchNet('INTA', (high) => { + if (high) intaWasHigh = true; + else if (intaWasHigh) intaFell = true; + }); + + bus.runCycle(); // EIN @ 0x000 → IFF=1 + board.setNet('INT', true); + bus.runCycle(); // NOP @ 0x001 → INT latched at M2; vector at X3 + board.setNet('INT', false); + bus.runCycle(); // BBS @ 0x003 → pop PC → 0x001; INTA cleared + bus.runCycle(); // re-execute NOP @ 0x001 → PC=0x002 + bus.runCycle(); // observe at PC=0x002 + + expect(bus.pc()).toBe(0x002); + expect(intaFell, 'INTA must de-assert during BBS').toBe(true); + board.dispose(); + }); }); describe('extended register file', () => { - it.todo('FIM works on registers R16..R23 (4040-only range)'); + it.skipIf(skip)('SB1 + FIM writes to bank-1 R0..R7 (R16..R23 region)', async () => { + // Strategy: distinguish bank-0 from bank-1 by setting up registers + // such that only bank-1 access produces a non-branch on ISZ. + // 1. FIM P0, 0xFF ; bank-0 R0=F, R1=F (the chip starts at SB0) + // 2. SB1 ; switch to bank 1 + // 3. FIM P0, 0x10 ; bank-1 R0=1, R1=0 + // 4. SB0 ; back to bank 0 + // 5. ISZ R0, target=0x20; bank-0 R0 was F → INC wraps to 0 → + // NO branch (PC falls through to next op) + // If SB1 didn't work, step 3 would have overwritten bank-0 R0 with 1, + // and step 5's ISZ would INC 1→2 → branch taken → PC=0x020. + const prog = new Uint8Array(0x80); + prog[0] = 0x20; prog[1] = 0xFF; // FIM P0, 0xFF + prog[2] = 0x0B; // SB1 + prog[3] = 0x20; prog[4] = 0x10; // FIM P0, 0x10 + prog[5] = 0x0A; // SB0 + prog[6] = 0x70; prog[7] = 0x20; // ISZ R0, target 0x020 + prog[8] = 0x00; // NOP (fall-through path) + const board = new BoardHarness(); + await board.addChip(CHIP, fullPinMap()); + board.setNet('STP', false); + board.setNet('INT', false); + board.setNet('RESET', true); + board.advanceNanos(CLOCK_NS * 12); + board.setNet('RESET', false); + + const bus = new Bus4040(board, prog); + // 6 instructions + observation. ISZ is 2-byte (2 cycles). FIMs + // are 2-byte (2 cycles each). SB0/SB1 are 1-byte. Total cycles + // through ISZ end: FIM(2) + SB1(1) + FIM(2) + SB0(1) + ISZ(2) = 8. + // Cycle 9 will fetch the next instruction — at 0x008 if not taken. + bus.runCycles(9); + // Bank-1 worked → R0 stayed F → ISZ wraps to 0 → no branch → PC=8. + expect(bus.pc()).toBe(0x008); + board.dispose(); + }); }); });