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:
parent
ab90bd7618
commit
148c56d6dc
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue