130 lines
4.4 KiB
JavaScript
130 lines
4.4 KiB
JavaScript
/**
|
|
* Intel 4004 emulator chip — TDD spec.
|
|
*
|
|
* The 4004 is electrically the most exotic chip on the list:
|
|
* - 4-bit data bus on D0..D3 multiplexed with addresses across an
|
|
* 8-cycle instruction frame (A1, A2, A3, M1, M2, X1, X2, X3).
|
|
* - SYNC pulses to mark the start of each instruction frame.
|
|
* - Two-phase clock (CLK1, CLK2).
|
|
* - 16 pins total.
|
|
*
|
|
* Because the bus is so different from the 8080/Z80, we don't reuse the
|
|
* fake-ROM helper. Tests here observe the bus phase-by-phase.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { BoardHarness } from '../src/BoardHarness.js';
|
|
import { chipWasmExists } from '../src/helpers.js';
|
|
|
|
const CHIP = '4004';
|
|
const skip = !chipWasmExists(CHIP);
|
|
|
|
const CLOCK_HZ = 740_000;
|
|
const CLOCK_NS = Math.round(1e9 / CLOCK_HZ);
|
|
|
|
function fullPinMap() {
|
|
const m = {
|
|
SYNC: 'SYNC', RESET: 'RESET', TEST: 'TEST',
|
|
CMROM: 'CMROM',
|
|
CMRAM0: 'CMRAM0', CMRAM1: 'CMRAM1', CMRAM2: 'CMRAM2', CMRAM3: 'CMRAM3',
|
|
CLK1: 'CLK1', CLK2: 'CLK2',
|
|
VDD: 'VDD', VSS: 'VSS',
|
|
};
|
|
for (let i = 0; i < 4; i++) m[`D${i}`] = `D${i}`;
|
|
return m;
|
|
}
|
|
|
|
async function bootChip(board) {
|
|
await board.addChip(CHIP, fullPinMap());
|
|
board.setNet('TEST', false);
|
|
// Pulse RESET high then low. Do NOT advance time after RESET goes
|
|
// low — caller does that so the first observed cycle starts at
|
|
// phase A1 with PC = 0. (Same lesson as bootCpu in the 8080 tests.)
|
|
board.setNet('RESET', true);
|
|
board.advanceNanos(CLOCK_NS * 10);
|
|
board.setNet('RESET', false);
|
|
}
|
|
|
|
describe('Intel 4004 chip', () => {
|
|
|
|
describe('pin contract', () => {
|
|
it.skipIf(skip)('registers all 16 logical pins', async () => {
|
|
const board = new BoardHarness();
|
|
await expect(board.addChip(CHIP, fullPinMap())).resolves.toBeDefined();
|
|
board.dispose();
|
|
});
|
|
});
|
|
|
|
describe('instruction-cycle frame', () => {
|
|
it.skipIf(skip)('asserts SYNC once every 8 clock cycles', async () => {
|
|
const board = new BoardHarness();
|
|
await bootChip(board);
|
|
|
|
const syncTimes = [];
|
|
board.watchNet('SYNC', (high) => {
|
|
if (high) syncTimes.push(board.nowNanos);
|
|
});
|
|
|
|
// Run 24 clock cycles → expect ≈ 3 SYNC pulses.
|
|
for (let i = 0; i < 24; i++) board.advanceNanos(CLOCK_NS);
|
|
|
|
expect(syncTimes.length, 'SYNC pulses in 24 cycles').toBeGreaterThanOrEqual(2);
|
|
// Spacing should be ~8 cycles between pulses.
|
|
if (syncTimes.length >= 2) {
|
|
const gap = Number(syncTimes[1] - syncTimes[0]);
|
|
expect(gap).toBeGreaterThan(CLOCK_NS * 6);
|
|
expect(gap).toBeLessThan(CLOCK_NS * 10);
|
|
}
|
|
board.dispose();
|
|
});
|
|
|
|
it.skipIf(skip)('drives D0..D3 with 12-bit address across A1, A2, A3 phases', async () => {
|
|
const board = new BoardHarness();
|
|
await bootChip(board);
|
|
|
|
// After RESET the PC is 0. The first three nibbles after SYNC
|
|
// should all be 0 (low addr nibble first, by 4004 convention).
|
|
const samples = [];
|
|
let sinceSync = -1;
|
|
// Latch on the FIRST SYNC only — a second pulse in the window
|
|
// would otherwise re-arm the sampler and over-collect.
|
|
board.watchNet('SYNC', (high) => { if (high && sinceSync === -1) sinceSync = 0; });
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
board.advanceNanos(CLOCK_NS);
|
|
if (sinceSync >= 0 && sinceSync < 3) {
|
|
samples.push(board.readBus('D', 4));
|
|
sinceSync++;
|
|
}
|
|
}
|
|
expect(samples.length).toBe(3);
|
|
// For PC = 0 all three nibbles are 0.
|
|
expect(samples).toEqual([0, 0, 0]);
|
|
board.dispose();
|
|
});
|
|
|
|
it.skipIf(skip)('CM-ROM strobes during M1 phase of an instruction cycle', async () => {
|
|
const board = new BoardHarness();
|
|
await bootChip(board);
|
|
|
|
let cmRomSeen = false;
|
|
board.watchNet('CMROM', (high) => { if (high) cmRomSeen = true; });
|
|
for (let i = 0; i < 16; i++) board.advanceNanos(CLOCK_NS);
|
|
expect(cmRomSeen, 'CM-ROM must pulse high during M1').toBe(true);
|
|
board.dispose();
|
|
});
|
|
});
|
|
|
|
describe('instruction set', () => {
|
|
it.todo('NOP advances PC by 1');
|
|
it.todo('LDM loads the immediate nibble into the accumulator');
|
|
it.todo('JCN conditionally jumps based on TEST/CY/ACC zero');
|
|
it.todo('FIM loads an 8-bit immediate into a register pair');
|
|
it.todo('JMS pushes return address and jumps');
|
|
it.todo('BBL pops return address into PC');
|
|
});
|
|
|
|
describe('integration', () => {
|
|
it.todo('runs a Busicom-style decrement-and-blink program');
|
|
});
|
|
});
|