diff --git a/frontend/src/__tests__/chipbus-z80-ram-decode.test.ts b/frontend/src/__tests__/chipbus-z80-ram-decode.test.ts new file mode 100644 index 00000000..6528428e --- /dev/null +++ b/frontend/src/__tests__/chipbus-z80-ram-decode.test.ts @@ -0,0 +1,134 @@ +/** + * Phase 3 computer-core LIVE proof (project/multichip-bus/): a real Z80 with + * ROM + RAM + address-decode glue, all chip-to-chip over a shared bus, no board. + * + * Memory map via real glue logic: + * ROM (32K) at 0x0000-0x7FFF -> rom.CE = A15 (selected when A15=0) + * RAM (64K) at 0x8000-0xFFFF -> ram.CE = NOT A15 (an inverter chip) + * both data outputs gated by RD (OE); RAM write gated by WR (WE). + * + * The ROM program writes 0x5A to RAM at 0x8000, clears A, reads it back, and + * HALTs only if the byte survived (else it spins). HALT going low proves: the + * Z80 ran from ROM, the inverter decoded A15 to select RAM (Phase 2 settle + * drives the combinational glue), the RAM latched a write and returned it on a + * read over the shared bus (Phase 1 tri-state), all within synchronous bus + * cycles (Phase 2 settle-before-read). + * + * Fixtures compiled with wasi-sdk; skips if absent. ram-64k.wasm / inverter.wasm + * are built from the committed examples; z80-ram-rom.c is the 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 f = (n: string) => fileURLToPath(new URL(`./fixtures/chipbus/${n}`, import.meta.url)); +const paths = { + z80: f('z80.wasm'), + rom: f('z80-ram-rom.wasm'), + ram: f('ram-64k.wasm'), + inv: f('inverter.wasm'), +}; +const haveFixtures = Object.values(paths).every(existsSync); + +const range = (n: number) => Array.from({ length: n }, (_, i) => i); + +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']; +const RAM_PINS = [...range(16).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE', 'WE', 'VCC', 'GND']; +const INV_PINS = ['IN', 'OUT']; + +const W: ChipNetState['wires'] = []; +const wire = (a: string, ap: string, b: string, bp: string) => + (W as { start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }[]).push( + { start: { componentId: a, pinName: ap }, end: { componentId: b, pinName: bp } }, + ); +// Address bus A0..A14 shared by Z80, ROM and RAM. +for (const i of range(15)) { + wire('z80', `A${i}`, 'rom', `A${i}`); + wire('z80', `A${i}`, 'ram', `A${i}`); +} +// A15: top RAM address bit, ROM chip-select, and the inverter input (decode). +wire('z80', 'A15', 'ram', 'A15'); +wire('z80', 'A15', 'rom', 'CE'); +wire('z80', 'A15', 'inv', 'IN'); +// Data bus shared by all three memory-side chips. +for (const i of range(8)) { + wire('z80', `D${i}`, 'rom', `D${i}`); + wire('z80', `D${i}`, 'ram', `D${i}`); +} +// Control: RD -> both output-enables; WR -> RAM write-enable; !A15 -> RAM CE. +wire('z80', 'RD', 'rom', 'OE'); +wire('z80', 'RD', 'ram', 'OE'); +wire('z80', 'WR', 'ram', 'WE'); +wire('inv', 'OUT', 'ram', 'CE'); + +const STATE: ChipNetState = { + wires: W, + components: [ + { id: 'z80', metadataId: 'custom-chip' }, + { id: 'rom', metadataId: 'custom-chip' }, + { id: 'ram', metadataId: 'custom-chip' }, + { id: 'inv', metadataId: 'custom-chip' }, + ], + boards: [], +}; + +const pinKey = (chipId: string, pin: string): number => + resolveChipNetKey(STATE, chipId, pin) ?? syntheticChipPin(chipId, pin); +const wiresFor = (chipId: string, pins: string[]): Map => + new Map(pins.map((p) => [p, pinKey(chipId, p)] as [string, number])); + +describe.skipIf(!haveFixtures)('chipbus Phase 3 core — Z80 + ROM + RAM + address decode', () => { + beforeEach(() => { + setChipBusEnabledForTest(true); + resetChipNetIndexForTest(); + resetBusNets(); + }); + afterEach(() => { + setChipBusEnabledForTest(null); + resetChipNetIndexForTest(); + resetBusNets(); + }); + + it('runs from ROM, round-trips RAM via the inverter-decoded bus, and HALTs', async () => { + const pm = new PinManager(); + const z80 = await ChipInstance.create({ wasm: new Uint8Array(readFileSync(paths.z80)), componentId: 'z80', pinManager: pm, wires: wiresFor('z80', Z80_PINS) }); + z80.start(); + const rom = await ChipInstance.create({ wasm: new Uint8Array(readFileSync(paths.rom)), componentId: 'rom', pinManager: pm, wires: wiresFor('rom', ROM_PINS) }); + rom.start(); + const ram = await ChipInstance.create({ wasm: new Uint8Array(readFileSync(paths.ram)), componentId: 'ram', pinManager: pm, wires: wiresFor('ram', RAM_PINS) }); + ram.start(); + const inv = await ChipInstance.create({ wasm: new Uint8Array(readFileSync(paths.inv)), componentId: 'inv', pinManager: pm, wires: wiresFor('inv', INV_PINS) }); + inv.start(); + + const halt = pinKey('z80', 'HALT'); + expect(pm.getPinState(halt)).toBe(true); // running at power-on + + pm.triggerPinChange(pinKey('z80', 'BUSREQ'), true); + pm.triggerPinChange(pinKey('z80', 'WAIT'), true); + pm.triggerPinChange(pinKey('z80', 'RESET'), true); + + z80.tickTimers(100_000n); // ~400 clocks; the program halts after ~7 instructions + + // HALT low: the RAM write+read round-trip survived -> the whole computer core + // (CPU + ROM + RAM + inverter decode) works over the shared chip-to-chip bus. + expect(pm.getPinState(halt)).toBe(false); + + z80.dispose(); rom.dispose(); ram.dispose(); inv.dispose(); + }); +}); diff --git a/frontend/src/__tests__/fixtures/chipbus/inverter.wasm b/frontend/src/__tests__/fixtures/chipbus/inverter.wasm new file mode 100644 index 00000000..1e36b766 Binary files /dev/null and b/frontend/src/__tests__/fixtures/chipbus/inverter.wasm differ diff --git a/frontend/src/__tests__/fixtures/chipbus/ram-64k.wasm b/frontend/src/__tests__/fixtures/chipbus/ram-64k.wasm new file mode 100644 index 00000000..d957887c Binary files /dev/null and b/frontend/src/__tests__/fixtures/chipbus/ram-64k.wasm differ diff --git a/frontend/src/__tests__/fixtures/chipbus/z80-ram-rom.wasm b/frontend/src/__tests__/fixtures/chipbus/z80-ram-rom.wasm new file mode 100644 index 00000000..87a130eb Binary files /dev/null and b/frontend/src/__tests__/fixtures/chipbus/z80-ram-rom.wasm differ diff --git a/test/test_custom_chips/sdk/examples/z80-ram-rom.c b/test/test_custom_chips/sdk/examples/z80-ram-rom.c new file mode 100644 index 00000000..e899adca --- /dev/null +++ b/test/test_custom_chips/sdk/examples/z80-ram-rom.c @@ -0,0 +1,113 @@ +/* + * z80-ram-rom — a rom-32k variant whose image is a Z80 RAM round-trip test, + * for the Phase 3 computer-core proof (project/multichip-bus/). Honours the + * full image (no 0xFF clamp) so the program can exceed 16 bytes. + * + * 0000: 3E 5A LD A, 0x5A + * 0002: 32 00 80 LD (0x8000), A ; write 0x5A to RAM at 0x8000 + * 0005: AF XOR A ; A = 0 + * 0006: 3A 00 80 LD A, (0x8000) ; read it back from RAM + * 0009: FE 5A CP 0x5A + * 000B: C2 10 00 JP NZ, 0x0010 ; mismatch -> fail loop (no HALT) + * 000E: 76 HALT ; success: RAM round-trip worked + * 0010: C3 10 00 JP 0x0010 ; fail: spin forever + * + * HALT only fires if the Z80 fetched the program from ROM (0x0000-0x7FFF), + * wrote+read RAM (0x8000-0xFFFF), and the byte survived — i.e. address decoding + * + RAM read/write over the shared bus all work. + */ +#include "velxio-chip.h" +#include +#include + +#define ROM_SIZE 0x8000 /* 32 KB */ + +static const uint8_t rom_image[ROM_SIZE] = { + [0x0000] = 0x3E, [0x0001] = 0x5A, + [0x0002] = 0x32, [0x0003] = 0x00, [0x0004] = 0x80, + [0x0005] = 0xAF, + [0x0006] = 0x3A, [0x0007] = 0x00, [0x0008] = 0x80, + [0x0009] = 0xFE, [0x000A] = 0x5A, + [0x000B] = 0xC2, [0x000C] = 0x10, [0x000D] = 0x00, + [0x000E] = 0x76, + [0x0010] = 0xC3, [0x0011] = 0x10, [0x0012] = 0x00, +}; + +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; + return rom_image[addr]; /* honour the whole image; unset = 0x00 (NOP) */ +} + +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(); +} diff --git a/test/test_custom_chips/sdk/examples/z80-ram-rom.chip.json b/test/test_custom_chips/sdk/examples/z80-ram-rom.chip.json new file mode 100644 index 00000000..2f26ed79 --- /dev/null +++ b/test/test_custom_chips/sdk/examples/z80-ram-rom.chip.json @@ -0,0 +1,9 @@ +{ + "schema": "velxio-chip/v1", + "name": "Z80 RAM-test ROM", + "author": "Velxio", + "license": "MIT", + "description": "A rom-32k variant holding a Z80 RAM round-trip program (write 0x5A to 0x8000, read back, HALT on match). Fixture for the Phase 3 computer-core proof (Z80 + ROM + RAM + address decode).", + "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": [] +}