test_intel: phase D-3 — 4040 SRC + I/O bus wiring (4004 parity)

Apply the same xact_t pattern from the 4004 (phase D-2) to the 4040,
so SRC and the I/O group (WRM/WMP/WRR/WPM/WR0..3/SBM/RDM/RDR/ADM/
RD0..3) drive or sample the multiplexed nibble bus during X2/X3 with
CM-RAM (or CM-ROM for ROM-port ops) strobed.

The 4040's two CM-ROM lines (selected by rom_bank) and its STP/INT
control flow are unchanged — the bus action is staged at M2 and
acted on at X2/X3, fitting cleanly inside the existing PHASE_X3
control-flow block.

Two new integration tests under "4040 + 4002 RAM integration" mirror
the 4004's: SRC + WMP drives the output port, and SRC + WRM/RDM
round-trips a nibble through 4002 storage.

Total test_intel: 117 passing, 11 todo, 0 failed (was 115).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-05-01 03:07:11 +02:00
parent 076bb78b26
commit adc99a8035
2 changed files with 201 additions and 9 deletions

View File

@ -50,6 +50,19 @@ typedef enum {
FETCH_OPERAND,
} fetch_t;
/* X2/X3 bus action selected at end of M2 from the decoded opcode.
Same set as the 4004 (the 4040 inherits MCS-4 I/O semantics). */
typedef enum {
XACT_NONE = 0,
XACT_SRC, /* drive pair_hi at X2, pair_lo at X3, CM-RAM strobe */
XACT_WRM_WMP, /* drive ACC at X2, CM-RAM (or CM-ROM for WRR/WPM) */
XACT_RDM, /* release D at X2, sample → io_data_in (4002 drives) */
XACT_RDS, /* RDR — release D at X2, sample (CM-ROM strobed) */
XACT_ADM_SBM, /* like RDM but feeds ADD/SUB */
XACT_WR_STATUS, /* WR0..WR3 — drive ACC at X2 */
XACT_RD_STATUS, /* RD0..RD3 — release at X2, sample */
} xact_t;
typedef struct {
/* Pin handles — names from [M40] pp. 1-5/1-6 */
vx_pin dpin[4];
@ -94,6 +107,12 @@ typedef struct {
bool stop_ff;
bool halt_ff;
bool inta_ff;
/* X2/X3 staging — populated at M2 from the decoded opcode. */
xact_t xact;
uint8_t xact_pair;
uint8_t xact_status_idx;
uint8_t io_data_in;
} cpu_t;
static cpu_t G;
@ -162,6 +181,8 @@ static void reset_state(void) {
G.stop_ff = false;
G.halt_ff = false;
G.inta_ff = false;
G.xact = XACT_NONE;
G.io_data_in = 0;
vx_pin_write(G.sync, 0);
vx_pin_write(G.cmrom[0], 0);
@ -336,24 +357,31 @@ static void exec_1byte(uint8_t op) {
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) */
case 0xE: /* I/O / RAM group — bus heavy lifting happened during
X2/X3; here we only update ACC/flags from io_data_in
for read ops. Writes have no further effect on CPU
state (output side of the 4002/4001 was driven by
the X2 bus action). */
switch (lo) {
case 0x8: { /* SBM */
uint8_t r = G.acc + 0xF + (G.cy ? 0 : 1);
case 0x8: { /* SBM — A ← A + ~RAM + ~CY */
uint8_t r = G.acc + ((~G.io_data_in) & 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);
case 0x9: G.acc = G.io_data_in; break; /* RDM */
case 0xA: G.acc = G.io_data_in; break; /* RDR */
case 0xB: { /* ADM — A ← A + RAM + CY */
uint8_t r = G.acc + G.io_data_in + (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 */
case 0xC: case 0xD: case 0xE: case 0xF: /* RD0..RD3 */
G.acc = G.io_data_in;
break;
/* 0,1,2,3,4,5,6,7 = WRM/WMP/WRR/WPM/WR0..3 — bus drives
ACC at X2; nothing more for the CPU side. */
default: break;
}
break;
@ -428,6 +456,7 @@ static void on_phase(void* user_data) {
if (G.phase == PHASE_A1) {
vx_pin_write(G.cmrom[0], 0);
vx_pin_write(G.cmrom[1], 0);
for (int i = 0; i < 4; i++) vx_pin_write(G.cmram[i], 0);
}
switch (G.phase) {
@ -460,13 +489,93 @@ static void on_phase(void* user_data) {
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;
/* Decode opcode → set up X2/X3 bus action (mirrors 4004). */
G.xact = XACT_NONE;
if (G.fetch_state == FETCH_OPCODE) {
uint8_t op = G.opcode;
if ((op & 0xF1) == 0x21) {
G.xact = XACT_SRC;
G.xact_pair = (op >> 1) & 7;
} else if ((op & 0xF0) == 0xE0) {
uint8_t lo = op & 0xF;
switch (lo) {
case 0x0: case 0x1:
case 0x2: case 0x3:
G.xact = XACT_WRM_WMP; break;
case 0x4: case 0x5: case 0x6: case 0x7:
G.xact = XACT_WR_STATUS;
G.xact_status_idx = lo - 4;
break;
case 0x8: case 0xB: G.xact = XACT_ADM_SBM; break;
case 0x9: G.xact = XACT_RDM; break;
case 0xA: G.xact = XACT_RDS; break;
case 0xC: case 0xD: case 0xE: case 0xF:
G.xact = XACT_RD_STATUS;
G.xact_status_idx = lo - 0xC;
break;
}
}
}
break;
case PHASE_X1:
vx_pin_write(G.cy_pin, G.cy ? 1 : 0);
break;
case PHASE_X2:
switch (G.xact) {
case XACT_SRC:
drive_d((pair_read(G.xact_pair) >> 4) & 0xF);
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
break;
case XACT_WRM_WMP: {
drive_d(G.acc & 0xF);
uint8_t lo = G.opcode & 0xF;
if (lo == 0x2 || lo == 0x3) {
vx_pin_write(active_cmrom(), 1);
} else {
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
}
break;
}
case XACT_WR_STATUS:
drive_d(G.acc & 0xF);
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
break;
case XACT_RDM:
case XACT_ADM_SBM:
case XACT_RD_STATUS:
release_d();
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
G.io_data_in = read_d() & 0xF;
break;
case XACT_RDS:
release_d();
vx_pin_write(active_cmrom(), 1);
G.io_data_in = read_d() & 0xF;
break;
default:
break;
}
break;
case PHASE_X3: {
switch (G.xact) {
case XACT_SRC:
drive_d(pair_read(G.xact_pair) & 0xF);
break;
case XACT_WRM_WMP:
case XACT_WR_STATUS:
/* drive held from X2 */
break;
case XACT_RDM:
case XACT_ADM_SBM:
case XACT_RD_STATUS:
case XACT_RDS:
vx_pin_write(G.cmram[G.cmram_select & 3], 0);
vx_pin_write(active_cmrom(), 0);
release_d();
break;
default:
break;
}
G.pc_overridden = false;
if (G.stp_latched) {

View File

@ -236,4 +236,87 @@ describe('Intel 4040 chip', () => {
board.dispose();
});
});
describe('4040 + 4002 RAM integration', () => {
const RAM = '4002-ram';
const skipIntegration = skip || !chipWasmExists(RAM);
it.skipIf(skipIntegration)(
'SRC + WMP drives the 4002 output port from ACC',
async () => {
// PC=0x00: 0xD3 LDM 3 → ACC=3
// PC=0x01: 0x21 SRC P0 → drive R0:R1=0:0 → chip-pair=0
// PC=0x02: 0xE1 WMP → 4002.O0..O3 = 3
const PROG = new Uint8Array(0x40);
PROG[0] = 0xD3;
PROG[1] = 0x21;
PROG[2] = 0xE1;
const board = new BoardHarness();
// Register the 4002 BEFORE the 4040 (same ordering trick as
// 4004/4002 integration). 4040.CMRAM0 → 4002.CM.
await board.addChip(RAM, {
SYNC: 'SYNC', CL: 'CLK1', RESET: 'RESET', CM: 'CMRAM0',
VDD: 'VDD', VSS: 'VSS',
D0: 'D0', D1: 'D1', D2: 'D2', D3: 'D3',
O0: 'O0', O1: 'O1', O2: 'O2', O3: 'O3',
});
await board.addChip(CHIP, fullPinMap());
board.setNet('STP', false);
board.setNet('INT', false);
board.setNet('TEST', false);
board.setNet('RESET', true);
board.advanceNanos(CLOCK_NS * 12);
board.setNet('RESET', false);
const bus = new Bus4040(board, PROG);
for (let cyc = 0; cyc < 8; cyc++) bus.runCycle();
let out = 0;
for (let i = 0; i < 4; i++) if (board.getNet(`O${i}`)) out |= (1 << i);
expect(out, '4002 output port after WMP must equal ACC (= 3)').toBe(3);
board.dispose();
}
);
it.skipIf(skipIntegration)(
'WRM stores into RAM and RDM reads it back through the bus',
async () => {
// 0xD5 LDM 5 ; 0x21 SRC P0 ; 0xE0 WRM ; 0xF0 CLB
// 0xE9 RDM ; 0xE1 WMP ; 0x00 NOP
const PROG = new Uint8Array(0x40);
PROG[0] = 0xD5;
PROG[1] = 0x21;
PROG[2] = 0xE0;
PROG[3] = 0xF0;
PROG[4] = 0xE9;
PROG[5] = 0xE1;
const board = new BoardHarness();
await board.addChip(RAM, {
SYNC: 'SYNC', CL: 'CLK1', RESET: 'RESET', CM: 'CMRAM0',
VDD: 'VDD', VSS: 'VSS',
D0: 'D0', D1: 'D1', D2: 'D2', D3: 'D3',
O0: 'O0', O1: 'O1', O2: 'O2', O3: 'O3',
});
await board.addChip(CHIP, fullPinMap());
board.setNet('STP', false);
board.setNet('INT', false);
board.setNet('TEST', false);
board.setNet('RESET', true);
board.advanceNanos(CLOCK_NS * 12);
board.setNet('RESET', false);
const bus = new Bus4040(board, PROG);
for (let cyc = 0; cyc < 12; cyc++) bus.runCycle();
let out = 0;
for (let i = 0; i < 4; i++) if (board.getNet(`O${i}`)) out |= (1 << i);
expect(out, 'WMP after RDM must surface the mem-stored 5').toBe(5);
board.dispose();
}
);
});
});