velxio/test/test_intel/test_4040/4040.test.js

323 lines
12 KiB
JavaScript
Raw Normal View History

/**
* Intel 4040 emulator chip TDD spec.
*
* The 4040 is a strict superset of the 4004. It adds:
* - Interrupts (INT pin, fixed vector verify exact addr from datasheet)
* - Single-step / STOP / STOP-ACK
* - Expanded register file (16 24 4-bit registers)
* - Deeper PC stack (3 7)
* - 14 new opcodes (interrupt enable/disable, return-from-interrupt,
* stop, additional register-pair ops)
* - 24-pin DIP, 2 CM-ROM lines (vs 1 on 4004)
*
* Tests focus on the deltas from 4004. The shared 4004-subset behavior
* should be exercised by a parametrised re-run of test_4004's suite once
* both chips are implemented (deferred).
*/
import { describe, it, expect } from 'vitest';
import { BoardHarness } from '../src/BoardHarness.js';
import { chipWasmExists } from '../src/helpers.js';
const CHIP = '4040';
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
* C identifiers); the three 15 V supply pins (Vdd, Vdd1, Vdd2) are kept
* separate even though velxio is digital and treats them all as power.
*/
function fullPinMap() {
const m = {
SYNC: 'SYNC', RESET: 'RESET', TEST: 'TEST',
CMROM0: 'CMROM0', CMROM1: 'CMROM1',
CMRAM0: 'CMRAM0', CMRAM1: 'CMRAM1', CMRAM2: 'CMRAM2', CMRAM3: 'CMRAM3',
CLK1: 'CLK1', CLK2: 'CLK2',
STP: 'STP', STPA: 'STPA', // Stop input + Stop-acknowledge output
INT: 'INT', INTA: 'INTA', // Interrupt input + ack output
CY: 'CY', // Carry output buffer (open drain)
VDD: 'VDD', VDD1: 'VDD1', VDD2: 'VDD2', VSS: 'VSS',
};
for (let i = 0; i < 4; i++) m[`D${i}`] = `D${i}`;
return m;
}
describe('Intel 4040 chip', () => {
describe('pin contract', () => {
it.skipIf(skip)('registers the 24-pin contract (4004 superset)', async () => {
const board = new BoardHarness();
await expect(board.addChip(CHIP, fullPinMap())).resolves.toBeDefined();
board.dispose();
});
});
describe('STP / STPA', () => {
it.skipIf(skip)('asserting STP causes STPA to assert within one cycle', async () => {
// Per MCS-40 manual p. 1-10: when STP is latched at M2, the STOP FF
// sets at X3; the CPU then executes NOPs in a loop (clock and SYNC
// KEEP RUNNING) and STPA asserts. So the assertion here is that
// STPA goes high — we deliberately do NOT assert that SYNC stops.
const board = new BoardHarness();
await board.addChip(CHIP, fullPinMap());
// Reset and run a few cycles freely.
board.setNet('RESET', true);
board.advanceNanos(CLOCK_NS * 12); // ≥96 clk per p. 1-5 RESET min
board.setNet('RESET', false);
for (let i = 0; i < 16; i++) board.advanceNanos(CLOCK_NS);
// Now assert STP (active high per pin description, p. 1-5) and watch.
let acked = false;
board.watchNet('STPA', (high) => { if (high) acked = true; });
board.setNet('STP', true);
// Allow up to 2 instruction cycles for the chip to latch STP at M2
// and assert STPA at X3.
for (let i = 0; i < 24; i++) board.advanceNanos(CLOCK_NS);
expect(acked, 'STPA must rise within ~two instruction cycles').toBe(true);
board.dispose();
});
});
describe('interrupts', () => {
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.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();
});
});
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();
}
);
});
});