test(chipbus): live proof - a real Z80 boots from a ROM over the bus
End-to-end validation of Phases 0-2 on an actual CPU. The real Z80 (examples/intel/z80.c) and a 32K EPROM (z80-boot-rom.c, a rom-32k variant holding JP 0x0006 / HALT) are wired chip-to-chip over a shared address + data bus with no board. RD drives the ROM's OE; CE is left enabled. Booting exercises all three phases at once: the Z80 drives the address -> the ROM reacts on the shared net key (Phase 0); asserts RD -> the ROM tri-state-drives the data bus while the Z80 released it (Phase 1); and reads the data bus in the SAME tickTimers step, getting the settled byte (Phase 2 settle-before-read). The Z80 fetches C3,06,00, jumps to 0x0006, fetches 76, and HALTs -> drives HALT low, which the test observes. z80.wasm is compiled from the committed examples/intel/z80.c; the boot ROM source + chip.json live in test_custom_chips/sdk/examples. All 36 chipbus tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3137c40a90
commit
89a5298f47
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* Phase 0-2 LIVE end-to-end proof on a REAL CPU (project/multichip-bus/).
|
||||
*
|
||||
* A real Z80 (examples/intel/z80.c) and a 32K EPROM (z80-boot-rom.c, a rom-32k
|
||||
* variant) are wired chip-to-chip over a shared address + data bus, NO board.
|
||||
* The ROM holds `JP 0x0006 / HALT`. Booting it requires the Z80 to:
|
||||
* - drive the address bus -> ROM reacts (shared net key, Phase 0);
|
||||
* - assert RD -> ROM tri-state-drives the data bus while the Z80 released it
|
||||
* (Phase 1 drive resolution);
|
||||
* - read the data bus IN THE SAME tickTimers step and get the settled byte
|
||||
* (Phase 2 settle-before-read);
|
||||
* fetch C3,06,00 (the JP + target), jump, fetch 76 at 0x0006, and HALT — which
|
||||
* drives HALT low. Observing HALT go low is the proof the whole bus works.
|
||||
*
|
||||
* Fixtures compiled with wasi-sdk (see test_intel/scripts/compile-chip.sh flags);
|
||||
* skips cleanly if absent. z80.wasm from examples/intel/z80.c; z80-boot-rom.wasm
|
||||
* from project/multichip-bus research (rom-32k with a boot image).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { PinManager } from '../simulation/PinManager';
|
||||
import { ChipInstance } from '../simulation/customChips/ChipRuntime';
|
||||
import {
|
||||
resolveChipNetKey,
|
||||
setChipBusEnabledForTest,
|
||||
resetChipNetIndexForTest,
|
||||
type ChipNetState,
|
||||
} from '../simulation/customChips/chipNets';
|
||||
import { syntheticChipPin } from '../simulation/customChips/syntheticPins';
|
||||
import { resetBusNets } from '../simulation/customChips/busNets';
|
||||
|
||||
const z80Path = fileURLToPath(new URL('./fixtures/chipbus/z80.wasm', import.meta.url));
|
||||
const romPath = fileURLToPath(new URL('./fixtures/chipbus/z80-boot-rom.wasm', import.meta.url));
|
||||
const haveFixtures = existsSync(z80Path) && existsSync(romPath);
|
||||
|
||||
const range = (n: number, from = 0) => Array.from({ length: n }, (_, i) => i + from);
|
||||
|
||||
const Z80_PINS = [
|
||||
...range(16).map((i) => `A${i}`),
|
||||
...range(8).map((i) => `D${i}`),
|
||||
'M1', 'MREQ', 'IORQ', 'RD', 'WR', 'RFSH', 'HALT', 'WAIT',
|
||||
'INT', 'NMI', 'RESET', 'BUSREQ', 'BUSACK', 'CLK', 'VCC', 'GND',
|
||||
];
|
||||
const ROM_PINS = [
|
||||
...range(15).map((i) => `A${i}`),
|
||||
...range(8).map((i) => `D${i}`),
|
||||
'CE', 'OE', 'VCC', 'GND',
|
||||
];
|
||||
|
||||
// The schematic: address A0..A14 and data D0..D7 straight across, plus the Z80's
|
||||
// RD strobe into the ROM's OE so the ROM only drives during reads. CE is left
|
||||
// unwired (reads 0 = always enabled).
|
||||
const STATE: ChipNetState = {
|
||||
wires: [
|
||||
...range(15).map((i) => ({
|
||||
start: { componentId: 'z80', pinName: `A${i}` },
|
||||
end: { componentId: 'rom', pinName: `A${i}` },
|
||||
})),
|
||||
...range(8).map((i) => ({
|
||||
start: { componentId: 'z80', pinName: `D${i}` },
|
||||
end: { componentId: 'rom', pinName: `D${i}` },
|
||||
})),
|
||||
{ start: { componentId: 'z80', pinName: 'RD' }, end: { componentId: 'rom', pinName: 'OE' } },
|
||||
],
|
||||
components: [
|
||||
{ id: 'z80', metadataId: 'custom-chip' },
|
||||
{ id: 'rom', metadataId: 'custom-chip' },
|
||||
],
|
||||
boards: [],
|
||||
};
|
||||
|
||||
function pinKey(chipId: string, pin: string): number {
|
||||
return resolveChipNetKey(STATE, chipId, pin) ?? syntheticChipPin(chipId, pin);
|
||||
}
|
||||
function wiresFor(chipId: string, pins: string[]): Map<string, number> {
|
||||
return new Map(pins.map((p) => [p, pinKey(chipId, p)] as [string, number]));
|
||||
}
|
||||
|
||||
describe.skipIf(!haveFixtures)('chipbus Phase 0-2 — a real Z80 boots from a ROM over the bus', () => {
|
||||
beforeEach(() => {
|
||||
setChipBusEnabledForTest(true);
|
||||
resetChipNetIndexForTest();
|
||||
resetBusNets();
|
||||
});
|
||||
afterEach(() => {
|
||||
setChipBusEnabledForTest(null);
|
||||
resetChipNetIndexForTest();
|
||||
resetBusNets();
|
||||
});
|
||||
|
||||
it('fetches JP+HALT from the ROM across the bus and drives HALT low', async () => {
|
||||
const z80Wasm = new Uint8Array(readFileSync(z80Path));
|
||||
const romWasm = new Uint8Array(readFileSync(romPath));
|
||||
const pm = new PinManager();
|
||||
|
||||
const z80 = await ChipInstance.create({
|
||||
wasm: z80Wasm, componentId: 'z80', pinManager: pm, wires: wiresFor('z80', Z80_PINS),
|
||||
});
|
||||
z80.start();
|
||||
const rom = await ChipInstance.create({
|
||||
wasm: romWasm, componentId: 'rom', pinManager: pm, wires: wiresFor('rom', ROM_PINS),
|
||||
});
|
||||
rom.start();
|
||||
|
||||
const halt = pinKey('z80', 'HALT');
|
||||
// At power-on the Z80 holds HALT high (running, registered OUTPUT_HIGH).
|
||||
expect(pm.getPinState(halt)).toBe(true);
|
||||
|
||||
// on_clock bails while BUSREQ/WAIT read low (unwired inputs default 0), so
|
||||
// deassert them, then release RESET (rising edge) to start the CPU.
|
||||
pm.triggerPinChange(pinKey('z80', 'BUSREQ'), true);
|
||||
pm.triggerPinChange(pinKey('z80', 'WAIT'), true);
|
||||
pm.triggerPinChange(pinKey('z80', 'RESET'), true);
|
||||
|
||||
// Run the 4 MHz pseudo-clock (250 ns period) for ~200 ticks — far more than
|
||||
// the 2 instructions to reach HALT.
|
||||
z80.tickTimers(50_000n);
|
||||
|
||||
// HALT went low: the Z80 executed JP 0x0006 then HALT, having correctly read
|
||||
// every byte from the ROM over the shared chip-to-chip bus.
|
||||
expect(pm.getPinState(halt)).toBe(false);
|
||||
|
||||
z80.dispose();
|
||||
rom.dispose();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* z80-boot-rom — a rom-32k variant whose image is a tiny Z80 boot program,
|
||||
* for the Phase 0-2 live proof (project/multichip-bus/). Identical to
|
||||
* examples/intel/rom-32k.c except for rom_image, which holds:
|
||||
*
|
||||
* 0000: C3 06 00 JP 0x0006 ; requires reading C3,06,00 over the bus
|
||||
* 0003: 00 00 00 NOP x3 ; jumped over
|
||||
* 0006: 76 HALT ; Z80 drives HALT low here -> observable
|
||||
*
|
||||
* Reaching HALT proves the Z80 fetched the multi-byte JP and its target from
|
||||
* this ROM across the shared data bus and executed it.
|
||||
*/
|
||||
#include "velxio-chip.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define ROM_SIZE 0x8000 /* 32 KB */
|
||||
|
||||
static const uint8_t rom_image[ROM_SIZE] = {
|
||||
[0x0000] = 0xC3, [0x0001] = 0x06, [0x0002] = 0x00, /* JP 0x0006 */
|
||||
[0x0003] = 0x00, [0x0004] = 0x00, [0x0005] = 0x00, /* NOP NOP NOP */
|
||||
[0x0006] = 0x76, /* HALT */
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
vx_pin a[15];
|
||||
vx_pin d[8];
|
||||
vx_pin ce;
|
||||
vx_pin oe;
|
||||
vx_pin vcc;
|
||||
vx_pin gnd;
|
||||
bool driving;
|
||||
} chip_t;
|
||||
|
||||
static chip_t G;
|
||||
|
||||
static uint16_t read_addr(void) {
|
||||
uint16_t v = 0;
|
||||
for (int i = 0; i < 15; i++) if (vx_pin_read(G.a[i])) v |= (1u << i);
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint8_t image_byte(uint16_t addr) {
|
||||
if (addr >= ROM_SIZE) return 0xFF;
|
||||
/* honour the programmed low bytes; an erased EPROM reads 0xFF elsewhere */
|
||||
if (addr >= 0x10) return 0xFF;
|
||||
return rom_image[addr];
|
||||
}
|
||||
|
||||
static void drive_data(uint8_t v) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
vx_pin_set_mode(G.d[i], VX_OUTPUT);
|
||||
vx_pin_write(G.d[i], (v >> i) & 1);
|
||||
}
|
||||
G.driving = true;
|
||||
}
|
||||
|
||||
static void release_data(void) {
|
||||
if (!G.driving) return;
|
||||
for (int i = 0; i < 8; i++) vx_pin_set_mode(G.d[i], VX_INPUT);
|
||||
G.driving = false;
|
||||
}
|
||||
|
||||
static void update_outputs(void) {
|
||||
int ce_low = (vx_pin_read(G.ce) == 0);
|
||||
int oe_low = (vx_pin_read(G.oe) == 0);
|
||||
if (ce_low && oe_low) {
|
||||
drive_data(image_byte(read_addr()));
|
||||
} else {
|
||||
release_data();
|
||||
}
|
||||
}
|
||||
|
||||
static void on_pin_change(void* user_data, vx_pin pin, int value) {
|
||||
(void)user_data; (void)pin; (void)value;
|
||||
update_outputs();
|
||||
}
|
||||
|
||||
void chip_setup(void) {
|
||||
char name[4];
|
||||
for (int i = 0; i < 15; i++) {
|
||||
name[0]='A';
|
||||
if (i<10) { name[1]='0'+i; name[2]=0; }
|
||||
else { name[1]='1'; name[2]='0'+(i-10); name[3]=0; }
|
||||
G.a[i] = vx_pin_register(name, VX_INPUT);
|
||||
}
|
||||
for (int i = 0; i < 8; i++) {
|
||||
name[0]='D'; name[1]='0'+i; name[2]=0;
|
||||
G.d[i] = vx_pin_register(name, VX_INPUT);
|
||||
}
|
||||
G.ce = vx_pin_register("CE", VX_INPUT);
|
||||
G.oe = vx_pin_register("OE", VX_INPUT);
|
||||
G.vcc = vx_pin_register("VCC", VX_INPUT);
|
||||
G.gnd = vx_pin_register("GND", VX_INPUT);
|
||||
G.driving = false;
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
vx_pin_watch(G.a[i], VX_EDGE_BOTH, on_pin_change, 0);
|
||||
}
|
||||
vx_pin_watch(G.ce, VX_EDGE_BOTH, on_pin_change, 0);
|
||||
vx_pin_watch(G.oe, VX_EDGE_BOTH, on_pin_change, 0);
|
||||
|
||||
update_outputs();
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schema": "velxio-chip/v1",
|
||||
"name": "Z80 Boot ROM (JP+HALT)",
|
||||
"author": "Velxio",
|
||||
"license": "MIT",
|
||||
"description": "A rom-32k variant holding a tiny Z80 boot program (JP 0x0006; HALT). Fixture for the Phase 0-2 live proof that a real Z80 boots from a ROM over a chip-to-chip bus.",
|
||||
"pins": ["A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "A11", "A12", "A13", "A14", "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "CE", "OE", "VCC", "GND"],
|
||||
"attributes": []
|
||||
}
|
||||
Loading…
Reference in New Issue