test_intel: complete Intel 4040 new instructions

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) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-04-30 03:44:01 +02:00
parent ab90bd7618
commit 148c56d6dc
2 changed files with 495 additions and 68 deletions

View File

@ -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 <stdint.h>
@ -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);
}

View File

@ -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();
});
});
});