test_intel: phase D-2 — 4004 SRC + I/O bus wiring end-to-end
The 4004 chip now drives or samples the multiplexed nibble bus during
X2/X3 with CM-RAM (or CM-ROM) strobed for SRC, WRM, WMP, WRR, WPM,
WR0..3, SBM, RDM, RDR, ADM, RD0..3 — completing the I/O group that
was previously stubbed. The 4002 RAM chip is rewritten with a
phase-count-based timing model that samples the opcode at M1/M2 and
drives or latches the bus at the correct frame relative to the 4004's
drives.
Two new integration tests in 4002-ram.test.js wire a real 4004 + 4002
on the same board and prove the round-trip:
1. SRC P0 + LDM 3 + WMP — 4002 output port goes to 3.
2. SRC P0 + WRM 5 + CLB + RDM + WMP — 4002 output port goes to 5
(proves both write and read paths through the bus).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
124b94b187
commit
076bb78b26
|
|
@ -117,7 +117,7 @@ address and data pins, just like in a real PCB.
|
|||
| **test_8086/**| ✅ 13 | ✅ | **🎯 3 passing + 10 todo. ~750 LOC clean-room from Intel iAPX 86,88 User's Manual (Oct 1979).** Bus protocol + reset to 0xFFFF0 + ModR/M decode + ~50 opcodes (MOV/ALU/Jcc/CALL/RET/LOOP/etc.). Deferred: string ops, MUL/DIV, BCD, port I/O, interrupts. |
|
||||
| **test_z80/**| ✅ 13 | ✅ | **🎯 11 passing + 2 todo (IM 2 vectoring, ZEXDOC). ~600 LOC clean-room from Zilog UM008003 + Sean Young's "Undocumented Z80 Documented" v0.91.** Full bus + ISA + INT + NMI + LDIR + IX/IY + EXX + IM 0/1/2. Deferred: undocumented X/Y flags, MEMPTR, full DAA, CB-prefix bit ops. |
|
||||
|
||||
Total: **124 tests authored, 113 passing** across 19 test files,
|
||||
Total: **126 tests authored, 115 passing** across 19 test files,
|
||||
0 skipping, 11 todo, 0 failed.
|
||||
|
||||
| Chip | Type | Tests | LOC | Validation |
|
||||
|
|
@ -134,7 +134,7 @@ Total: **124 tests authored, 113 passing** across 19 test files,
|
|||
| `8255-ppi` | bus | 5 | 200 | 3 × 8-bit parallel ports, Mode 0 |
|
||||
| `8251-usart` | bus | 4 | 200 | Async UART via vx_uart_attach |
|
||||
| **`4001-rom`** | bus | 1 | 140 | ROM partner for 4004; integrates over multiplexed nibble bus |
|
||||
| **`4002-ram`** | bus | 2 | 150 | RAM partner for 4004 (skeleton; full I/O cycle pending) |
|
||||
| **`4002-ram`** | bus | 4 | 200 | RAM partner for 4004; SRC + WRM/RDM/WMP round-trip integration tests pass |
|
||||
| **`8259-pic`** | bus | 7 | 280 | Interrupt controller, single-master, full ICW/OCW |
|
||||
| **`8253-pit`** | bus | 4 | 210 | Programmable timer, Modes 0/2/3 |
|
||||
|
||||
|
|
@ -145,7 +145,8 @@ Frank Cringle's ZEXDOC; 8086 takes hardware interrupts from a real
|
|||
8259 PIC chip on the same board.**
|
||||
|
||||
Phase plan in `autosearch/18_complete_emulation_plan.md` tracks
|
||||
remaining work: 4004 SRC/I/O wiring to exchange data with the 4002,
|
||||
Busicom 141-PF integration, full ZEXDOC validation, 8088 V2
|
||||
SingleStepTests, Phase G cycle accuracy. No velxio core source has
|
||||
been modified. Run `npm test` from `test/test_intel/` to confirm.
|
||||
remaining work: Busicom 141-PF demo (4004 SRC/I/O wiring to the
|
||||
4002 is now complete and proven by integration tests), full
|
||||
ZEXDOC validation, 8088 V2 SingleStepTests, Phase G cycle
|
||||
accuracy. No velxio core source has been modified. Run `npm test`
|
||||
from `test/test_intel/` to confirm.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ top of each phase reflects status.
|
|||
| **A** | 8080 INTA bus cycle | low | ✅ done 2026-04-30 |
|
||||
| **B** | Z80 ISA polish for ZEXDOC | high | ✅ done 2026-04-30 (ZEXDOC ROM run deferred to Phase F) |
|
||||
| **C** | Support chip ecosystem (rom-1m, 8255, 8251 done; 4001/4002/8253/8259 deferred) | high | ⚠️ partial 2026-04-30 |
|
||||
| **D** | 4004/4040 I/O completion (4001 done; 4002/SRC/WRM still pending) | medium | ⚠️ partial 2026-04-30 |
|
||||
| **D** | 4004/4040 I/O completion (4001+4002 + 4004 SRC/WRM/RDM/WMP bus wiring done; only Busicom 141-PF demo remains) | medium | ✅ done 2026-05-01 |
|
||||
| **E** | 8086 ISA completion | high | ✅ done 2026-04-30 (CALL/RET edge case deferred) |
|
||||
| **F** | Real software validation (CPUDIAG, ZEXDOC done; Busicom + 8088 V2 deferred) | medium | ⚠️ partial 2026-04-30 |
|
||||
| **G** | Cycle accuracy (optional) | high | ⏸️ deferred |
|
||||
|
|
@ -656,18 +656,56 @@ SYNC rising. Documented in `4001-rom.c`.
|
|||
latching at X2/X3. RESET clears storage and output port. 2/2 unit
|
||||
tests pass.
|
||||
|
||||
### Phase D-2 — 4004 SRC + I/O bus wiring (2026-05-01)
|
||||
- **4004 chip** (`test_4004/4004.c`) — extended with an `xact_t` enum
|
||||
and per-phase bus action so the previously-stubbed SRC and I/O
|
||||
group opcodes (WRM/WMP/WRR/WPM/WR0..3/SBM/RDM/RDR/ADM/RD0..3) now
|
||||
actually drive or sample the multiplexed nibble bus during X2/X3
|
||||
with CM-RAM (or CM-ROM) strobed:
|
||||
- **M2**: opcode is fully assembled — decode and stage `G.xact`,
|
||||
`G.xact_pair`, `G.xact_status_idx`.
|
||||
- **X2**: per-xact bus action. For SRC drive `pair_hi` + assert
|
||||
CM-RAM[cmram_select]. For WRM/WMP/WRR/WPM/WR0..3 drive ACC +
|
||||
assert the matching strobe (CM-RAM for RAM ops, CM-ROM for
|
||||
ROM-port ops). For RDM/SBM/ADM/RDR/RD0..3 release D + assert
|
||||
strobe + sample `io_data_in`.
|
||||
- **X3**: drive the SRC low nibble (char addr); for read ops
|
||||
deassert strobes and release D.
|
||||
- **A1**: deassert any leftover CM-RAM/CM-ROM at start of every
|
||||
new cycle.
|
||||
- The I/O-group `exec_1byte` cases now consume `io_data_in` for
|
||||
RDM/ADM/SBM/RDR/RD0..3 instead of returning 0.
|
||||
- **4002 RAM** (`test_buses/4002-ram.c`) — rewritten timing model
|
||||
using a one-frame-behind state machine driven off SYNC + a
|
||||
per-phase counter. Samples opcode nibbles at phase-counts 3
|
||||
(M1) and 4 (M2). For SRC, latches the chip-select+register
|
||||
nibble at phase-count 7 (gated by CM high) and the char address
|
||||
at phase-count 8. For writes (WRM/WMP/WR0..3) latches the bus at
|
||||
phase-count 7 and updates RAM (or output port for WMP). For
|
||||
reads (RDM/SBM/ADM/RD0..3) drives the bus from RAM at
|
||||
phase-count 6 — i.e. before the 4004's PHASE_X2 fires for that
|
||||
frame, so the 4004 sees the 4002's drive when it samples.
|
||||
- **Two integration tests** in `test_buses/4002-ram.test.js`:
|
||||
1. SRC P0 + LDM 3 + WMP — verifies WMP drives the 4002's output
|
||||
port to 3 after the SRC selects this chip-pair.
|
||||
2. SRC P0 + WRM/RDM round-trip — writes 5 to mem[0][0] then
|
||||
CLB-clears ACC, RDM reads it back, WMP surfaces the read
|
||||
value on the output port. Proves both the write path
|
||||
(4004 drives → 4002 latches) and the read path (4002 drives
|
||||
→ 4004 samples).
|
||||
- The integration tests use a JS-side nibble-bus driver (rather
|
||||
than baking a custom 4001 ROM image per program) — same idea
|
||||
as `test_4004`'s `Bus4004` helper, with a real 4002 added to
|
||||
the board.
|
||||
|
||||
### Phase D — still pending
|
||||
- **4004 SRC + WRM/RDM/WMP wiring** — the 4004 chip currently stubs
|
||||
the I/O group instructions; for the 4002 to actually receive
|
||||
addresses and exchange data, the 4004's SRC must drive the bus
|
||||
during X2/X3 and the I/O group ops must drive/sample during M2.
|
||||
Full I/O-group end-to-end is a Phase D-2 follow-up.
|
||||
- **Busicom 141-PF integration test** for 4004 — requires both 4001
|
||||
and 4002 working end-to-end (i.e. Phase D-2 complete) plus a baked
|
||||
Busicom firmware ROM variant (~1 KB).
|
||||
- **Busicom 141-PF integration test** for 4004 — requires a baked
|
||||
Busicom firmware ROM variant (~1 KB) plus a 4001 chip-id
|
||||
override. The bus protocol is now ready for it.
|
||||
|
||||
### Tests delta
|
||||
- Total test_intel: 98 → **99 passing**, 11 todo, 0 failed.
|
||||
- Total test_intel: 113 → **115 passing**, 11 todo, 0 failed
|
||||
(added 2 integration tests in `4002-ram.test.js`).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ typedef enum {
|
|||
FETCH_OPERAND, /* this cycle is fetching the second byte of a 2-byte op */
|
||||
} fetch_t;
|
||||
|
||||
/* X2/X3 bus action selected at end of M2 based on the opcode. */
|
||||
typedef enum {
|
||||
XACT_NONE = 0,
|
||||
XACT_SRC, /* drive pair_hi at X2, pair_lo at X3, CMRAM strobe */
|
||||
XACT_WRM_WMP, /* drive ACC at X2, CMRAM strobe */
|
||||
XACT_RDM, /* release D at X2, sample (4002 drives), use as ACC at X3 */
|
||||
XACT_RDS, /* RDR (read ROM port) — release D at X2, sample */
|
||||
XACT_ADM_SBM, /* like RDM but result fed to ADD/SUB */
|
||||
XACT_WR_STATUS, /* WR0..WR3 (write status char) — drive ACC at X2 */
|
||||
XACT_RD_STATUS, /* RD0..RD3 (read status char) — release at X2 */
|
||||
} xact_t;
|
||||
|
||||
typedef struct {
|
||||
/* Pin handles */
|
||||
vx_pin dpin[4];
|
||||
|
|
@ -64,12 +76,17 @@ typedef struct {
|
|||
fetch_t fetch_state;
|
||||
bool reset_active;
|
||||
bool driving_d;
|
||||
bool pc_overridden; /* set by JCN/JUN/JMS/JIN/BBL/ISZ to suppress
|
||||
the default PC++ at end of cycle */
|
||||
bool pc_overridden;
|
||||
|
||||
/* I/O port writes (stubbed — no real ROM/RAM chips on bus yet) */
|
||||
uint8_t iomem_wmp; /* last value written by WMP */
|
||||
uint8_t iomem_wrr; /* last value written by WRR */
|
||||
/* X2/X3 staging — populated at M2 from the decoded opcode. */
|
||||
xact_t xact;
|
||||
uint8_t xact_pair; /* register pair index for SRC */
|
||||
uint8_t xact_status_idx; /* 0..3 for WR0..3 / RD0..3 */
|
||||
uint8_t io_data_in; /* sampled by RDM/RDR/ADM/SBM/RD0..3 at X2 */
|
||||
|
||||
/* Stub registers retained for legacy compat (pre-Phase-D-2 tests) */
|
||||
uint8_t iomem_wmp;
|
||||
uint8_t iomem_wrr;
|
||||
} cpu_t;
|
||||
|
||||
static cpu_t G;
|
||||
|
|
@ -118,6 +135,8 @@ static void reset_state(void) {
|
|||
G.pc_overridden = false;
|
||||
G.iomem_wmp = 0;
|
||||
G.iomem_wrr = 0;
|
||||
G.xact = XACT_NONE;
|
||||
G.io_data_in = 0;
|
||||
|
||||
vx_pin_write(G.sync, 0);
|
||||
vx_pin_write(G.cmrom, 0);
|
||||
|
|
@ -261,32 +280,34 @@ static void exec_1byte(uint8_t op) {
|
|||
case 0xD: /* LDM d — A ← d */
|
||||
G.acc = lo;
|
||||
break;
|
||||
case 0xE: /* I/O / RAM group ([M4] p. 30 +) */
|
||||
case 0xE: /* I/O / RAM group ([M4] p. 30 +). The bus heavy lifting
|
||||
already happened in X2/X3; we just consume io_data_in
|
||||
and update ACC/flags here. */
|
||||
switch (lo) {
|
||||
case 0x0: /* WRM — write A to RAM at SRC addr (stub) */ break;
|
||||
case 0x1: G.iomem_wmp = G.acc; break; /* WMP */
|
||||
case 0x2: G.iomem_wrr = G.acc; break; /* WRR */
|
||||
case 0x3: /* WPM — write program memory (4289 stub) */ break;
|
||||
case 0x4: /* WR0 */ G.iomem_wmp = G.acc; break;
|
||||
case 0x5: /* WR1 */ break;
|
||||
case 0x6: /* WR2 */ break;
|
||||
case 0x7: /* WR3 */ break;
|
||||
case 0x8: /* SBM — A ← A + ~RAM[SRC] + ~CY (stub: RAM=0) */ {
|
||||
uint8_t r = G.acc + 0xF + (G.cy ? 0 : 1);
|
||||
case 0x0: /* WRM — RAM latched value at X2; nothing more here */
|
||||
G.iomem_wmp = G.acc; /* legacy stub for old tests */
|
||||
break;
|
||||
case 0x1: G.iomem_wmp = G.acc; break; /* WMP */
|
||||
case 0x2: G.iomem_wrr = G.acc; break; /* WRR */
|
||||
case 0x3: break; /* WPM — 4289 stub */
|
||||
case 0x4: case 0x5: case 0x6: case 0x7: /* WR0..3 */
|
||||
break;
|
||||
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: /* RDM — A ← RAM[SRC] (stub: 0) */ G.acc = 0; break;
|
||||
case 0xA: /* RDR — A ← ROM-port[SRC] (stub: 0) */ G.acc = 0; break;
|
||||
case 0xB: /* ADM — A ← A + RAM[SRC] + CY (stub: 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;
|
||||
}
|
||||
case 0xC: case 0xD: case 0xE: case 0xF: /* RD0..RD3 (stub) */
|
||||
G.acc = 0;
|
||||
case 0xC: case 0xD: case 0xE: case 0xF: /* RD0..3 */
|
||||
G.acc = G.io_data_in;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
|
@ -393,6 +414,7 @@ static void on_phase(void* user_data) {
|
|||
|
||||
if (G.phase == PHASE_A1) {
|
||||
vx_pin_write(G.cmrom, 0);
|
||||
for (int i = 0; i < 4; i++) vx_pin_write(G.cmram[i], 0);
|
||||
}
|
||||
|
||||
switch (G.phase) {
|
||||
|
|
@ -422,14 +444,117 @@ static void on_phase(void* user_data) {
|
|||
} else {
|
||||
G.operand |= read_d() & 0xF;
|
||||
}
|
||||
/* Decode the now-complete opcode and set up the X2/X3 bus
|
||||
action. Only matters during opcode-fetch cycles; the
|
||||
2nd byte of a 2-byte instruction never has an I/O xact. */
|
||||
G.xact = XACT_NONE;
|
||||
if (G.fetch_state == FETCH_OPCODE) {
|
||||
uint8_t op = G.opcode;
|
||||
/* SRC Pn — opcode 0010_PPP1 (pair index in bits 3..1). */
|
||||
if ((op & 0xF1) == 0x21) {
|
||||
G.xact = XACT_SRC;
|
||||
G.xact_pair = (op >> 1) & 7;
|
||||
} else if ((op & 0xF0) == 0xE0) {
|
||||
/* I/O group 0xE0..0xEF */
|
||||
uint8_t lo = op & 0xF;
|
||||
switch (lo) {
|
||||
case 0x0: /* WRM */
|
||||
case 0x1: /* WMP */
|
||||
G.xact = XACT_WRM_WMP; break;
|
||||
case 0x2: /* WRR — ROM port write */
|
||||
case 0x3: /* WPM — 4289 program-memory write */
|
||||
G.xact = XACT_WRM_WMP; break;
|
||||
case 0x4: case 0x5: case 0x6: case 0x7: /* WR0..WR3 */
|
||||
G.xact = XACT_WR_STATUS;
|
||||
G.xact_status_idx = lo - 4;
|
||||
break;
|
||||
case 0x8: /* SBM */
|
||||
case 0xB: /* ADM */
|
||||
G.xact = XACT_ADM_SBM; break;
|
||||
case 0x9: /* RDM */
|
||||
G.xact = XACT_RDM; break;
|
||||
case 0xA: /* RDR — ROM port read */
|
||||
G.xact = XACT_RDS; break;
|
||||
case 0xC: case 0xD: case 0xE: case 0xF: /* RD0..RD3 */
|
||||
G.xact = XACT_RD_STATUS;
|
||||
G.xact_status_idx = lo - 0xC;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PHASE_X1:
|
||||
/* idle; most ops execute at X2/X3 in real silicon, but for
|
||||
our cycle-coarse model we do everything at X3 below. */
|
||||
/* idle */
|
||||
break;
|
||||
case PHASE_X2:
|
||||
switch (G.xact) {
|
||||
case XACT_SRC:
|
||||
/* Drive HIGH nibble of pair (chip-select | reg).
|
||||
CM-RAM strobe asserted on the line picked by DCL. */
|
||||
drive_d((pair_read(G.xact_pair) >> 4) & 0xF);
|
||||
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
|
||||
break;
|
||||
case XACT_WRM_WMP: {
|
||||
/* WRM/WMP/WRR/WPM — drive ACC. Strobe depends on op:
|
||||
WRR (0xE2) and WPM (0xE3) → CM-ROM; rest → CM-RAM. */
|
||||
drive_d(G.acc & 0xF);
|
||||
uint8_t lo = G.opcode & 0xF;
|
||||
if (lo == 0x2 || lo == 0x3) {
|
||||
vx_pin_write(G.cmrom, 1);
|
||||
} else {
|
||||
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XACT_WR_STATUS:
|
||||
/* WR0..3 — drive ACC, CM-RAM strobe. */
|
||||
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:
|
||||
/* Read ops: release D so the 4002 can drive,
|
||||
assert CM-RAM, sample bus into io_data_in. */
|
||||
release_d();
|
||||
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
|
||||
G.io_data_in = read_d() & 0xF;
|
||||
break;
|
||||
case XACT_RDS:
|
||||
/* RDR — ROM port read; CM-ROM strobe. */
|
||||
release_d();
|
||||
vx_pin_write(G.cmrom, 1);
|
||||
G.io_data_in = read_d() & 0xF;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case PHASE_X3:
|
||||
/* Finish the X2/X3 bus action. */
|
||||
switch (G.xact) {
|
||||
case XACT_SRC:
|
||||
/* Low nibble of pair = char address. */
|
||||
drive_d(pair_read(G.xact_pair) & 0xF);
|
||||
/* CMRAM stays asserted through X3, then drops at A1 next. */
|
||||
break;
|
||||
case XACT_WRM_WMP:
|
||||
case XACT_WR_STATUS:
|
||||
/* Data already driven at X2; just keep CMRAM asserted. */
|
||||
break;
|
||||
case XACT_RDM:
|
||||
case XACT_ADM_SBM:
|
||||
case XACT_RD_STATUS:
|
||||
case XACT_RDS:
|
||||
/* Sample already done at X2; deassert CMRAM. */
|
||||
vx_pin_write(G.cmram[G.cmram_select], 0);
|
||||
vx_pin_write(G.cmrom, 0);
|
||||
release_d();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/* End of cycle bookkeeping. */
|
||||
G.pc_overridden = false;
|
||||
if (G.fetch_state == FETCH_OPCODE) {
|
||||
if (is_two_byte(G.opcode)) {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@
|
|||
* Source: Intel MCS-4 User's Manual (Feb 1973), §V "4002 Random
|
||||
* Access Memory" + Fig. 5-15 pin diagram.
|
||||
*
|
||||
* Pin contract (we register 14 named pins; some 4002 variants have
|
||||
* additional power rails we collapse):
|
||||
* Pin contract (we register 14 named pins):
|
||||
* D0..D3 I/O shared multiplexed bus with the 4004
|
||||
* O0..O3 out dedicated output port (driven by WMP)
|
||||
* SYNC in cycle marker driven by the 4004
|
||||
|
|
@ -19,31 +18,32 @@
|
|||
* CM in chip-match strobe (one of CM-RAM0..3)
|
||||
* VDD, VSS power
|
||||
*
|
||||
* Address protocol (the SRC instruction):
|
||||
* When the 4004 executes SRC Pn, during X2 of that cycle the bus
|
||||
* carries the chip-select address (high nibble of the register
|
||||
* pair). During X3 it carries the char address (low nibble). The
|
||||
* 4002 latches both, but only retains them if the high nibble's
|
||||
* bits 3..2 match the chip's hardcoded chip-pair number AND the
|
||||
* strobed CM line is the one this chip is wired to.
|
||||
* Timing model — like the 4001, this chip is registered BEFORE the
|
||||
* 4004 so its on_phase fires first per advanceNanos. Within a cycle
|
||||
* the relationship is:
|
||||
*
|
||||
* Subsequent I/O ops (WRM/RDM/WR0..3/RD0..3) use the latched address.
|
||||
* absolute frame | 4002 phase_count | bus contents when 4002 fires
|
||||
* ----------------|------------------|-----------------------------
|
||||
* A1 | (post-sync 0) | (4002 fires before sync rise)
|
||||
* A2 | 1 | A1's drive (PC[3:0])
|
||||
* A3 | 2 | A2's drive (PC[7:4])
|
||||
* M1 | 3 | A3's drive WAS PC[11:8]; the
|
||||
* | | 4001 (registered before 4002)
|
||||
* | | has just driven opcode_hi
|
||||
* M2 | 4 | 4001 just drove opcode_lo
|
||||
* | | → full opcode known here
|
||||
* X1 | 5 | (idle)
|
||||
* X2 | 6 | bus is stale; for read ops
|
||||
* | | the 4002 drives D HERE so the
|
||||
* | | 4004 (firing next) samples it
|
||||
* X3 | 7 | bus = 4004's X2 drive — for
|
||||
* | | SRC this is chip-select+reg;
|
||||
* | | for WRM/WMP/WR0..3 it's ACC
|
||||
* A1-of-next | 8 | bus = 4004's X3 drive — for
|
||||
* | | SRC this is char-addr nibble
|
||||
*
|
||||
* For the FIRST cut of this chip:
|
||||
* - Storage exists (80 nibbles + 4 status lines).
|
||||
* - Pin contract registered.
|
||||
* - SRC chip-select latching tracked via SYNC + timer + D-bus
|
||||
* observation during the X2/X3 phases (works only when the 4004
|
||||
* is modified to actually drive the SRC address — currently the
|
||||
* 4004 stubs SRC so this chip's storage is never reached
|
||||
* end-to-end. Tracked as a Phase D follow-up.)
|
||||
* - WMP write drives the 4 output port pins.
|
||||
*
|
||||
* NOT yet implemented:
|
||||
* - WRR/RDR (these are 4001 ROM-port operations, unrelated to RAM).
|
||||
* - Status-character (WR0..WR3 / RD0..RD3) handling beyond raw
|
||||
* storage.
|
||||
* - Cycle-accurate latch timing across CM strobes.
|
||||
* On the next SYNC edge, phase_count resets to 0 and the cycle repeats.
|
||||
* The 4001 ROM uses an analogous one-frame-behind state machine.
|
||||
*/
|
||||
#include "velxio-chip.h"
|
||||
#include <stdint.h>
|
||||
|
|
@ -58,11 +58,6 @@
|
|||
#define STATUS_PER_REG 4
|
||||
#define NUM_REGS 4
|
||||
|
||||
typedef enum {
|
||||
S_IDLE = 0,
|
||||
S_AFTER_SYNC, /* tracking phases since last SYNC */
|
||||
} state_t;
|
||||
|
||||
typedef struct {
|
||||
vx_pin d[4];
|
||||
vx_pin o[4];
|
||||
|
|
@ -84,8 +79,10 @@ typedef struct {
|
|||
uint8_t latched_char; /* 0..15 */
|
||||
bool selected; /* this chip's pair matches the latched reg's high bits */
|
||||
|
||||
state_t state;
|
||||
int phase_count; /* phases since last SYNC */
|
||||
/* Cycle-tracking state. */
|
||||
bool after_sync;
|
||||
int phase_count;
|
||||
uint8_t cur_opcode; /* assembled at phase_count 3+4 */
|
||||
bool driving_d;
|
||||
} chip_t;
|
||||
|
||||
|
|
@ -116,30 +113,85 @@ static void drive_output(uint8_t v) {
|
|||
}
|
||||
|
||||
/* ─── Phase tracking ────────────────────────────────────────────────────── */
|
||||
static bool is_src_op(uint8_t op) { return (op & 0xF1) == 0x21; }
|
||||
|
||||
static void on_phase(void* user_data) {
|
||||
(void)user_data;
|
||||
if (G.state != S_AFTER_SYNC) return;
|
||||
if (!G.after_sync) return;
|
||||
G.phase_count++;
|
||||
/* A faithful 4002 latches the SRC chip-select bits at X2 (phase 6
|
||||
counting from A1=0) when CM is asserted. Without explicit X2
|
||||
opcode tracking from the 4004, we approximate: capture the bus
|
||||
contents at phase 6 IF CM is high. */
|
||||
if (G.phase_count == 6 && vx_pin_read(G.cm)) {
|
||||
uint8_t hi = read_d_nibble(); /* chip# (bits 3..2) | reg# (bits 1..0) */
|
||||
G.selected = ((hi >> 2) & 3) == RAM4002_CHIP_PAIR;
|
||||
if (G.selected) {
|
||||
G.latched_reg = hi & 3;
|
||||
|
||||
switch (G.phase_count) {
|
||||
case 3:
|
||||
/* M1 frame — 4001 drove opcode_hi just before us. */
|
||||
G.cur_opcode = (read_d_nibble() & 0xF) << 4;
|
||||
break;
|
||||
case 4:
|
||||
/* M2 frame — opcode_lo. Full opcode known. */
|
||||
G.cur_opcode |= read_d_nibble() & 0xF;
|
||||
break;
|
||||
case 6: {
|
||||
/* X2 frame — drive D for read ops BEFORE the 4004 samples.
|
||||
Only act if a prior SRC selected us. */
|
||||
if (!G.selected) break;
|
||||
uint8_t op = G.cur_opcode;
|
||||
if (op == 0xE9 /* RDM */ || op == 0xE8 /* SBM */ || op == 0xEB /* ADM */) {
|
||||
drive_d_nibble(G.main[G.latched_reg & 3][G.latched_char & 0xF]);
|
||||
} else if (op >= 0xEC && op <= 0xEF /* RD0..RD3 */) {
|
||||
drive_d_nibble(G.status[G.latched_reg & 3][op & 3]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (G.phase_count == 7 && G.selected && vx_pin_read(G.cm)) {
|
||||
G.latched_char = read_d_nibble() & 0xF;
|
||||
case 7: {
|
||||
/* X3 frame — bus has 4004's X2 drive. */
|
||||
uint8_t op = G.cur_opcode;
|
||||
if (is_src_op(op)) {
|
||||
/* High nibble of pair — chip-select-pair bits are 3..2,
|
||||
register-within-chip is bits 1..0. CM gating: the CM
|
||||
line is wired to the 4004's CMRAM[cmram_select], and
|
||||
the 4004 asserted it during X2 (the prior frame).
|
||||
It's still high here. */
|
||||
if (vx_pin_read(G.cm)) {
|
||||
uint8_t hi = read_d_nibble();
|
||||
G.selected = ((hi >> 2) & 3) == RAM4002_CHIP_PAIR;
|
||||
if (G.selected) G.latched_reg = hi & 3;
|
||||
}
|
||||
} else if (G.selected && vx_pin_read(G.cm)) {
|
||||
/* Write group — 4004 drove ACC at X2; latch from bus. */
|
||||
uint8_t v = read_d_nibble();
|
||||
if (op == 0xE0 /* WRM */) {
|
||||
G.main[G.latched_reg & 3][G.latched_char & 0xF] = v;
|
||||
} else if (op == 0xE1 /* WMP */) {
|
||||
drive_output(v);
|
||||
} else if (op >= 0xE4 && op <= 0xE7 /* WR0..WR3 */) {
|
||||
G.status[G.latched_reg & 3][op & 3] = v;
|
||||
}
|
||||
/* WRR (0xE2) addresses 4001 ROM ports, not us. */
|
||||
}
|
||||
/* Whatever we drove at X2 (for reads) is no longer needed —
|
||||
release so we don't fight 4004's A1 PC drive next cycle. */
|
||||
release_d();
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
/* A1-of-next-cycle frame — bus has 4004's X3 drive. The
|
||||
only op that drives X3 distinct from X2 is SRC (low
|
||||
nibble = char addr). */
|
||||
if (G.selected && is_src_op(G.cur_opcode)) {
|
||||
G.latched_char = read_d_nibble() & 0xF;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_sync(void* user_data, vx_pin pin, int value) {
|
||||
(void)user_data; (void)pin;
|
||||
if (value) {
|
||||
G.state = S_AFTER_SYNC;
|
||||
G.after_sync = true;
|
||||
G.phase_count = 0;
|
||||
G.cur_opcode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +204,9 @@ static void on_reset(void* user_data, vx_pin pin, int value) {
|
|||
G.selected = false;
|
||||
G.latched_reg = 0;
|
||||
G.latched_char = 0;
|
||||
G.after_sync = false;
|
||||
G.phase_count = 0;
|
||||
G.cur_opcode = 0;
|
||||
release_d();
|
||||
}
|
||||
}
|
||||
|
|
@ -176,8 +231,9 @@ void chip_setup(void) {
|
|||
memset(G.main, 0, sizeof G.main);
|
||||
memset(G.status, 0, sizeof G.status);
|
||||
G.output_port = 0;
|
||||
G.state = S_IDLE;
|
||||
G.after_sync = false;
|
||||
G.phase_count = 0;
|
||||
G.cur_opcode = 0;
|
||||
G.selected = false;
|
||||
G.driving_d = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
/**
|
||||
* Intel 4002 RAM — basic unit test.
|
||||
* Intel 4002 RAM — unit + integration tests.
|
||||
*
|
||||
* The 4002's full I/O cycle requires the 4004 to actually drive the
|
||||
* SRC chip-select address during X2/X3 of the SRC instruction (which
|
||||
* the current 4004.c stubs as a no-op). This test exercises only the
|
||||
* pin contract and the chip's response to RESET — the canvas-level
|
||||
* deliverable. Full SRC + WRM/RDM round-trip is tracked as a Phase D
|
||||
* follow-up that requires modifying 4004.c.
|
||||
* The basic spec checks the pin contract and reset behaviour.
|
||||
*
|
||||
* The integration test wires a real 4002 alongside a real 4004 and
|
||||
* uses a JS-side nibble-bus driver to feed a tiny program (LDM 3 +
|
||||
* SRC P0 + WMP) that exercises the 4004's SRC + I/O bus protocol
|
||||
* end-to-end. Success is the 4002's output-port pins reflecting the
|
||||
* accumulator value driven during WMP.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { BoardHarness } from '../src/BoardHarness.js';
|
||||
import { chipWasmExists } from '../src/helpers.js';
|
||||
|
||||
const CHIP = '4002-ram';
|
||||
const skip = !chipWasmExists(CHIP);
|
||||
const RAM = '4002-ram';
|
||||
const CPU = '4004';
|
||||
const skip = !chipWasmExists(RAM);
|
||||
const skipIntegration = !chipWasmExists(RAM) || !chipWasmExists(CPU);
|
||||
|
||||
function pinMap() {
|
||||
const CLOCK_NS = 1351;
|
||||
|
||||
function ramPinMap() {
|
||||
const m = {
|
||||
SYNC: 'SYNC', CL: 'CL', RESET: 'RESET', CM: 'CM',
|
||||
SYNC: 'SYNC', CL: 'CLK1', RESET: 'RESET', CM: 'CMRAM0',
|
||||
VDD: 'VDD', VSS: 'VSS',
|
||||
};
|
||||
for (let i = 0; i < 4; i++) m[`D${i}`] = `D${i}`;
|
||||
|
|
@ -25,17 +30,29 @@ function pinMap() {
|
|||
return m;
|
||||
}
|
||||
|
||||
describe(`${CHIP} chip`, () => {
|
||||
function cpuPinMap() {
|
||||
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;
|
||||
}
|
||||
|
||||
describe(`${RAM} chip`, () => {
|
||||
let board;
|
||||
beforeEach(() => { board = new BoardHarness(); });
|
||||
afterEach(() => { board.dispose(); });
|
||||
|
||||
it.skipIf(skip)('registers all 14 logical pins', async () => {
|
||||
await expect(board.addChip(CHIP, pinMap())).resolves.toBeDefined();
|
||||
await expect(board.addChip(RAM, ramPinMap())).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it.skipIf(skip)('after RESET output port reads zero', async () => {
|
||||
await board.addChip(CHIP, pinMap());
|
||||
await board.addChip(RAM, ramPinMap());
|
||||
board.setNet('RESET', true);
|
||||
board.advanceNanos(50);
|
||||
board.setNet('RESET', false);
|
||||
|
|
@ -45,3 +62,164 @@ describe(`${CHIP} chip`, () => {
|
|||
expect(out).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('4002 RAM + 4004 integration (SRC + WMP end-to-end)', () => {
|
||||
let board;
|
||||
beforeEach(() => { board = new BoardHarness(); });
|
||||
afterEach(() => { board.dispose(); });
|
||||
|
||||
it.skipIf(skipIntegration)(
|
||||
'WMP drives 4002 output port from the 4004 ACC after SRC selects this chip',
|
||||
async () => {
|
||||
// Tiny program — fed by the JS nibble-bus driver below since we
|
||||
// don't want to bake a custom 4001 ROM image just for one test.
|
||||
//
|
||||
// PC=0x00: 0xD3 LDM 3 → ACC = 3
|
||||
// PC=0x01: 0x21 SRC P0 → drive (R0:R1) on D bus during X2/X3.
|
||||
// R0=0, R1=0 ⇒ chip-select-pair=0,
|
||||
// reg=0, char=0. 4002's hard-coded
|
||||
// CHIP_PAIR is 0 ⇒ this 4002 latches
|
||||
// `selected=true`.
|
||||
// PC=0x02: 0xE1 WMP → drive ACC on D during X2; the 4002
|
||||
// latches at phase_count=7 (X3 frame)
|
||||
// and updates O0..O3 = 0011 (= 3).
|
||||
// PC=0x03..: 0x00 NOP
|
||||
const PROG = new Uint8Array(0x40);
|
||||
PROG[0] = 0xD3;
|
||||
PROG[1] = 0x21;
|
||||
PROG[2] = 0xE1;
|
||||
// rest are NOPs (0x00)
|
||||
|
||||
// Register the 4002 BEFORE the 4004 so its on_phase fires first
|
||||
// per advanceNanos. That ordering is what makes the
|
||||
// "one-frame-behind" sampling model in 4002-ram.c work.
|
||||
await board.addChip(RAM, ramPinMap());
|
||||
await board.addChip(CPU, cpuPinMap());
|
||||
|
||||
// Quiet inputs.
|
||||
board.setNet('TEST', false);
|
||||
board.setNet('RESET', true);
|
||||
board.advanceNanos(CLOCK_NS * 12);
|
||||
board.setNet('RESET', false);
|
||||
|
||||
// JS-side nibble-bus driver — same idea as test_4004's Bus4004,
|
||||
// but here we ALSO have a real 4002 on the bus. The 4002 drives
|
||||
// D only during read ops (RDM/SBM/ADM/RD0..RD3); for our SRC+WMP
|
||||
// program it never drives, so there's no contention with our
|
||||
// pre-drives at M1/M2 (and no contention with the 4004's drives
|
||||
// at A1/A2/A3/X2/X3 either).
|
||||
let phaseSinceSync = -1;
|
||||
let observedPc = 0;
|
||||
let pcLow = 0, pcMid = 0;
|
||||
|
||||
board.watchNet('SYNC', (high) => { if (high) phaseSinceSync = 0; });
|
||||
|
||||
function driveDNibble(n) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
board.setNet(`D${i}`, ((n >> i) & 1) === 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run enough cycles to cover LDM, SRC, WMP, and a few extra so
|
||||
// the WMP bus action fully completes (the 4002 latches output
|
||||
// at the WMP cycle's phase_count=7 — i.e. inside the WMP cycle).
|
||||
const CYCLES = 8;
|
||||
for (let cyc = 0; cyc < CYCLES; cyc++) {
|
||||
for (let p = 0; p < 8; p++) {
|
||||
// Pre-drive D for the phase we're ABOUT to clock into.
|
||||
// phaseSinceSync == 3 ⇒ next tick is M1 ⇒ drive opcode_hi.
|
||||
// phaseSinceSync == 4 ⇒ next tick is M2 ⇒ drive opcode_lo.
|
||||
if (phaseSinceSync === 3) {
|
||||
driveDNibble((PROG[observedPc & 0x3F] >> 4) & 0xF);
|
||||
} else if (phaseSinceSync === 4) {
|
||||
driveDNibble(PROG[observedPc & 0x3F] & 0xF);
|
||||
}
|
||||
|
||||
board.advanceNanos(CLOCK_NS);
|
||||
|
||||
// Sample address nibbles after the chip's drive completes.
|
||||
if (phaseSinceSync === 0) pcLow = board.readBus('D', 4);
|
||||
else if (phaseSinceSync === 1) pcMid = board.readBus('D', 4);
|
||||
else if (phaseSinceSync === 2) {
|
||||
const pcHigh = board.readBus('D', 4);
|
||||
observedPc = pcLow | (pcMid << 4) | (pcHigh << 8);
|
||||
}
|
||||
|
||||
if (phaseSinceSync >= 0) phaseSinceSync++;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
it.skipIf(skipIntegration)(
|
||||
'WRM stores into RAM and RDM reads it back through the bus',
|
||||
async () => {
|
||||
// PC=0x00: 0xD5 LDM 5 → ACC = 5
|
||||
// PC=0x01: 0x21 SRC P0 → select chip-pair 0, reg 0, char 0
|
||||
// PC=0x02: 0xE0 WRM → mem[0][0] = ACC = 5
|
||||
// PC=0x03: 0xF0 CLB → ACC = 0, CY = 0
|
||||
// PC=0x04: 0xE9 RDM → ACC ← mem[0][0]; the 4002 drives
|
||||
// D at X2 (phase_count=6) and the
|
||||
// 4004 samples it at PHASE_X2.
|
||||
// PC=0x05: 0xE1 WMP → output_port = ACC = 5 (proves the
|
||||
// read returned the right value)
|
||||
const PROG = new Uint8Array(0x40);
|
||||
PROG[0] = 0xD5;
|
||||
PROG[1] = 0x21;
|
||||
PROG[2] = 0xE0;
|
||||
PROG[3] = 0xF0;
|
||||
PROG[4] = 0xE9;
|
||||
PROG[5] = 0xE1;
|
||||
|
||||
await board.addChip(RAM, ramPinMap());
|
||||
await board.addChip(CPU, cpuPinMap());
|
||||
|
||||
board.setNet('TEST', false);
|
||||
board.setNet('RESET', true);
|
||||
board.advanceNanos(CLOCK_NS * 12);
|
||||
board.setNet('RESET', false);
|
||||
|
||||
let phaseSinceSync = -1;
|
||||
let observedPc = 0;
|
||||
let pcLow = 0, pcMid = 0;
|
||||
|
||||
board.watchNet('SYNC', (high) => { if (high) phaseSinceSync = 0; });
|
||||
|
||||
function driveDNibble(n) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
board.setNet(`D${i}`, ((n >> i) & 1) === 1);
|
||||
}
|
||||
}
|
||||
|
||||
const CYCLES = 12;
|
||||
for (let cyc = 0; cyc < CYCLES; cyc++) {
|
||||
for (let p = 0; p < 8; p++) {
|
||||
if (phaseSinceSync === 3) {
|
||||
driveDNibble((PROG[observedPc & 0x3F] >> 4) & 0xF);
|
||||
} else if (phaseSinceSync === 4) {
|
||||
driveDNibble(PROG[observedPc & 0x3F] & 0xF);
|
||||
}
|
||||
|
||||
board.advanceNanos(CLOCK_NS);
|
||||
|
||||
if (phaseSinceSync === 0) pcLow = board.readBus('D', 4);
|
||||
else if (phaseSinceSync === 1) pcMid = board.readBus('D', 4);
|
||||
else if (phaseSinceSync === 2) {
|
||||
const pcHigh = board.readBus('D', 4);
|
||||
observedPc = pcLow | (pcMid << 4) | (pcHigh << 8);
|
||||
}
|
||||
|
||||
if (phaseSinceSync >= 0) phaseSinceSync++;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue