test(chipbus): Phase 3 core - Z80 + ROM + RAM + address decode over the bus
The architectural heart of the retro computer, proven on real chips. A Z80, a 32K ROM, a 64K RAM and an inverter (address-decode glue) are wired chip-to-chip over a shared address + data bus, no board: ROM at 0x0000-0x7FFF rom.CE = A15 RAM at 0x8000-0xFFFF ram.CE = NOT A15 (the inverter chip) RD -> both OE ; WR -> RAM WE The ROM program writes 0x5A to RAM at 0x8000, clears A, reads it back, and HALTs only if the byte survived. HALT going low proves the full core works: the Z80 runs from ROM, the inverter decodes A15 to select RAM (the settle kernel drives the combinational glue across hops), and the RAM latches a write and returns it on a read over the shared tri-state bus, all within synchronous bus cycles. Adds z80-ram-rom.c (boot image) + ram-64k/inverter fixtures. 37 chipbus tests across 7 files pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
89a5298f47
commit
0cd2dc2062
|
|
@ -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<string, number> =>
|
||||
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();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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();
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
Loading…
Reference in New Issue