fix(chipbus): Galaksija boots + displays + types live in the browser
The gallery example loaded but the Z80 never visibly ran: the screen stayed frozen on garbage. Two multi-chip async-load races, neither caught by the existing headless tests (which drive RESET manually and attach the display before boot): 1. RESET edge-vs-level race. The Z80 only left reset on the RISING edge of RESET (a pin watch). In the browser the 7 chips instantiate asynchronously, so the small power-on-reset chip releases RESET before the larger Z80 has registered its watch -> the edge is lost and the CPU stays in reset forever. Fix: on_clock samples the RESET level (hardware-accurate; RESET is level-sensitive) so a missed edge self-corrects. An undriven RESET reads low, so the CPU safely stays in reset until something drives it high. Repro/guard: chipbus-galaksija-reset-race (race ordering must still boot). 2. Display-snoop load-order race. galaksija-display was a passive write-snoop; the ROM paints the screen ONCE at boot then idles, so a display that comes up late misses every write and shows stale content forever. A snoop cannot recover writes it never saw. Fix: fold the screen into the RAM chip (galaksija-ram-display) and render from the ACTUAL video RAM (0x2800-0x2BFF, internal 0x0800 with A0-A12 wiring) on a ~30 fps timer - correct regardless of load order, exactly how the real machine scans video RAM. Repro/guard: chipbus-galaksija-display-snoop-race (late snoop shows nothing) + chipbus-galaksija-ram-display (renders even when first paint is post-boot). The example now has 6 chips (RAM+display merged, gdisp dropped), 76 wires. Verified live in the browser: boots to "@'READY", shows the ">" prompt, and pressing A echoes ">A_" through keyboard -> Z80 -> video RAM -> display. The full chipbus suite is 45/45. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
94627b99d2
commit
47adb0b1c8
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Phase 3 — the display-snoop load-order race (project/multichip-bus/).
|
||||
*
|
||||
* galaksija-display is a PASSIVE bus snoop: it renders a cell only when it
|
||||
* catches a WR rising edge into video RAM (0x2800-0x2BFF). The Galaksija ROM
|
||||
* writes the whole screen ONCE during boot, then idles polling the keyboard. So
|
||||
* if the display chip is instantiated AFTER the CPU has already written the
|
||||
* screen — which happens in the browser, where the 7 chips load asynchronously
|
||||
* and the display can come up after the (smaller, faster) reset path has already
|
||||
* let the CPU run — the display misses every write and shows stale/blank content
|
||||
* forever. A write-snoop cannot recover writes it never saw.
|
||||
*
|
||||
* This test demonstrates the failure: boot the machine, THEN attach the display,
|
||||
* and confirm it never shows the ">" prompt. (The fix is to render from actual
|
||||
* video RAM instead of snooping writes — see galaksija-vram-display.)
|
||||
*/
|
||||
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 P = { z80: f('z80.wasm'), rom: f('galaksija-rom.wasm'), ram: f('galaksija-ram.wasm'), inv: f('inverter.wasm'), disp: f('galaksija-display.wasm') };
|
||||
const have = Object.values(P).every(existsSync);
|
||||
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
|
||||
|
||||
const Z80 = [...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 = [...range(13).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE'];
|
||||
const RAM = [...range(16).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE', 'WE', 'VCC', 'GND'];
|
||||
const INV = ['IN', 'OUT'];
|
||||
const DISP = [...range(14).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'WR'];
|
||||
|
||||
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 } });
|
||||
for (const i of range(13)) for (const c of ['rom', 'ram', 'disp']) wire('z80', `A${i}`, c, `A${i}`);
|
||||
wire('z80', 'A13', 'rom', 'CE'); wire('z80', 'A13', 'inv', 'IN'); wire('inv', 'OUT', 'ram', 'CE'); wire('z80', 'A13', 'disp', 'A13');
|
||||
for (const i of range(8)) for (const c of ['rom', 'ram', 'disp']) wire('z80', `D${i}`, c, `D${i}`);
|
||||
wire('z80', 'RD', 'rom', 'OE'); wire('z80', 'RD', 'ram', 'OE');
|
||||
wire('z80', 'WR', 'ram', 'WE'); wire('z80', 'WR', 'disp', 'WR');
|
||||
|
||||
const STATE: ChipNetState = { wires: W, components: ['z80', 'rom', 'ram', 'inv', 'disp'].map((id) => ({ id, metadataId: 'custom-chip' })), boards: [] };
|
||||
const pk = (c: string, p: string): number => resolveChipNetKey(STATE, c, p) ?? syntheticChipPin(c, p);
|
||||
const wf = (c: string, pins: string[]) => new Map(pins.map((p) => [p, pk(c, p)] as [string, number]));
|
||||
const cellLit = (fb: Uint8Array, col: number, row: number): number => { let n = 0; for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (fb[((row * 8 + y) * 256 + (col * 8 + x)) * 4 + 1] > 0x80) n++; return n; };
|
||||
|
||||
describe.skipIf(!have)('chipbus Phase 3 — display-snoop load-order race', () => {
|
||||
beforeEach(() => { setChipBusEnabledForTest(true); resetChipNetIndexForTest(); resetBusNets(); });
|
||||
afterEach(() => { setChipBusEnabledForTest(null); resetChipNetIndexForTest(); resetBusNets(); });
|
||||
|
||||
const mk = async (pm: PinManager, k: keyof typeof P, id: string, pins: string[], display?: { width: number; height: number }) =>
|
||||
ChipInstance.create({ wasm: new Uint8Array(readFileSync(P[k])), componentId: id, pinManager: pm, wires: wf(id, pins), display });
|
||||
|
||||
it('a display attached AFTER boot misses the screen (write-snoop limitation)', async () => {
|
||||
const pm = new PinManager();
|
||||
const z80 = await mk(pm, 'z80', 'z80', Z80); z80.start();
|
||||
(await mk(pm, 'rom', 'rom', ROM)).start();
|
||||
(await mk(pm, 'ram', 'ram', RAM)).start();
|
||||
(await mk(pm, 'inv', 'inv', INV)).start();
|
||||
|
||||
for (const p of ['WAIT', 'INT', 'NMI', 'BUSREQ']) pm.triggerPinChange(pk('z80', p), true);
|
||||
pm.triggerPinChange(pk('z80', 'RESET'), false); pm.triggerPinChange(pk('z80', 'RESET'), true);
|
||||
z80.tickTimers(BigInt(120000 * 250)); // CPU writes the whole screen during boot
|
||||
|
||||
// Display arrives late — every screen write already happened.
|
||||
const disp = await mk(pm, 'disp', 'disp', DISP, { width: 256, height: 128 });
|
||||
let fb: Uint8Array | null = null; disp.onFramebufferUpdate((r) => { fb = r as Uint8Array; }); disp.start();
|
||||
z80.tickTimers(BigInt(240000 * 250)); // CPU now idles in the keyboard loop, no screen writes
|
||||
disp.tickTimers(50_000_000n);
|
||||
|
||||
expect(fb).not.toBeNull();
|
||||
// The prompt never appears: the snoop saw none of the boot writes.
|
||||
expect(cellLit(fb!, 0, 1), 'a late-attached snoop display shows no prompt').toBe(0);
|
||||
z80.dispose(); disp.dispose();
|
||||
}, 60_000);
|
||||
});
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Phase 3 — Galaksija RAM+display chip renders from real video RAM
|
||||
* (project/multichip-bus/).
|
||||
*
|
||||
* galaksija-ram-display is the 64 KB RAM with the screen folded in: it renders
|
||||
* the 32x16 text screen from its OWN video RAM (0x2800-0x2BFF) on a ~30 fps
|
||||
* timer, instead of snooping bus writes. That makes the picture correct
|
||||
* regardless of when its paint timer first fires — the failure mode of the old
|
||||
* passive-snoop display (which lost the boot screen if it came up late). Here
|
||||
* the CPU boots and writes the whole screen FIRST, and the display's very first
|
||||
* paint happens only AFTER all writes are done; it must still show the ">"
|
||||
* prompt because it reads the memory it owns.
|
||||
*/
|
||||
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 P = { z80: f('z80.wasm'), rom: f('galaksija-rom.wasm'), ramdisp: f('galaksija-ram-display.wasm'), inv: f('inverter.wasm') };
|
||||
const have = Object.values(P).every(existsSync);
|
||||
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
|
||||
|
||||
const Z80 = [...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 = [...range(13).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE'];
|
||||
const RD = [...range(16).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE', 'WE', 'VCC', 'GND'];
|
||||
const INV = ['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 } });
|
||||
// A0-A12 to the RAM (A13 selects it via CE), exactly like the gallery example.
|
||||
for (const i of range(13)) wire('z80', `A${i}`, 'rd', `A${i}`);
|
||||
for (const i of range(13)) wire('z80', `A${i}`, 'rom', `A${i}`);
|
||||
wire('z80', 'A13', 'rom', 'CE'); wire('z80', 'A13', 'inv', 'IN'); wire('inv', 'OUT', 'rd', 'CE');
|
||||
for (const i of range(8)) { wire('z80', `D${i}`, 'rom', `D${i}`); wire('z80', `D${i}`, 'rd', `D${i}`); }
|
||||
wire('z80', 'RD', 'rom', 'OE'); wire('z80', 'RD', 'rd', 'OE'); wire('z80', 'WR', 'rd', 'WE');
|
||||
|
||||
const STATE: ChipNetState = { wires: W, components: ['z80', 'rom', 'rd', 'inv'].map((id) => ({ id, metadataId: 'custom-chip' })), boards: [] };
|
||||
const pk = (c: string, p: string): number => resolveChipNetKey(STATE, c, p) ?? syntheticChipPin(c, p);
|
||||
const wf = (c: string, pins: string[]) => new Map(pins.map((p) => [p, pk(c, p)] as [string, number]));
|
||||
const cellLit = (fb: Uint8Array, col: number, row: number): number => { let n = 0; for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (fb[((row * 8 + y) * 256 + (col * 8 + x)) * 4 + 1] > 0x80) n++; return n; };
|
||||
|
||||
describe.skipIf(!have)('chipbus Phase 3 — Galaksija RAM+display renders from video RAM', () => {
|
||||
beforeEach(() => { setChipBusEnabledForTest(true); resetChipNetIndexForTest(); resetBusNets(); });
|
||||
afterEach(() => { setChipBusEnabledForTest(null); resetChipNetIndexForTest(); resetBusNets(); });
|
||||
|
||||
const mk = async (pm: PinManager, k: keyof typeof P, id: string, pins: string[], display?: { width: number; height: number }) =>
|
||||
ChipInstance.create({ wasm: new Uint8Array(readFileSync(P[k])), componentId: id, pinManager: pm, wires: wf(id, pins), display });
|
||||
|
||||
it('shows the ">" prompt even when the first paint happens after the boot writes', async () => {
|
||||
const pm = new PinManager();
|
||||
const z80 = await mk(pm, 'z80', 'z80', Z80); z80.start();
|
||||
(await mk(pm, 'rom', 'rom', ROM)).start();
|
||||
const rd = await mk(pm, 'ramdisp', 'rd', RD, { width: 256, height: 128 });
|
||||
let fb: Uint8Array | null = null; rd.onFramebufferUpdate((r) => { fb = r as Uint8Array; }); rd.start();
|
||||
(await mk(pm, 'inv', 'inv', INV)).start();
|
||||
|
||||
for (const p of ['WAIT', 'INT', 'NMI', 'BUSREQ']) pm.triggerPinChange(pk('z80', p), true);
|
||||
pm.triggerPinChange(pk('z80', 'RESET'), false); pm.triggerPinChange(pk('z80', 'RESET'), true);
|
||||
|
||||
// Boot fully FIRST — the CPU writes the whole screen into RAM. (chip_setup
|
||||
// pushed one initial blank framebuffer; the paint timer has not run yet.)
|
||||
z80.tickTimers(BigInt(240000 * 250));
|
||||
|
||||
// The first real paint happens only now, long after every screen write.
|
||||
// Reading real video RAM, it still renders the prompt.
|
||||
fb = null;
|
||||
rd.tickTimers(50_000_000n);
|
||||
expect(fb, 'the paint timer pushed a framebuffer').not.toBeNull();
|
||||
expect(cellLit(fb!, 0, 1), 'the BASIC ">" prompt rendered from video RAM').toBeGreaterThan(4);
|
||||
|
||||
z80.dispose(); rd.dispose();
|
||||
}, 60_000);
|
||||
});
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* Phase 3 — multi-chip RESET ordering race (project/multichip-bus/).
|
||||
*
|
||||
* The Galaksija example boots through a power-on-reset chip (reset-gen) wired to
|
||||
* the Z80's RESET line, NOT a manual RESET pulse like the other tests. reset-gen
|
||||
* holds RESET low, then a one-shot timer drives it high to release the CPU.
|
||||
*
|
||||
* The Z80 chip releases from reset on the RISING EDGE of RESET (vx_pin_watch).
|
||||
* In the browser the 7 chips instantiate ASYNCHRONOUSLY and the host feeds a
|
||||
* wall-clock now, so reset-gen (small, loads first) fires its release timer on
|
||||
* its first tick — potentially BEFORE the larger Z80 finishes registering its
|
||||
* RESET watch. If the Z80 misses that one rising edge it stays in reset forever
|
||||
* and the machine never boots (the live symptom: a frozen, garbled display).
|
||||
*
|
||||
* This reproduces both orderings:
|
||||
* - race: reset-gen releases RESET, THEN the Z80 is created -> must still boot
|
||||
* - safe: the Z80 is created first, THEN reset-gen releases -> boots
|
||||
* Both must boot once the Z80 samples the RESET level (not just the edge).
|
||||
*/
|
||||
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 P = {
|
||||
z80: f('z80.wasm'), rom: f('galaksija-rom.wasm'), ram: f('galaksija-ram.wasm'),
|
||||
inv: f('inverter.wasm'), disp: f('galaksija-display.wasm'), rst: f('reset-gen.wasm'),
|
||||
};
|
||||
const have = Object.values(P).every(existsSync);
|
||||
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
|
||||
|
||||
const Z80 = [...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 = [...range(13).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE'];
|
||||
const RAM = [...range(16).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE', 'WE', 'VCC', 'GND'];
|
||||
const INV = ['IN', 'OUT'];
|
||||
const DISP = [...range(14).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'WR'];
|
||||
const RST = ['RESET', 'WAIT', 'BUSREQ', 'INT', 'NMI'];
|
||||
|
||||
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 } });
|
||||
for (const i of range(13)) for (const c of ['rom', 'ram', 'disp']) wire('z80', `A${i}`, c, `A${i}`);
|
||||
wire('z80', 'A13', 'rom', 'CE'); wire('z80', 'A13', 'inv', 'IN'); wire('inv', 'OUT', 'ram', 'CE');
|
||||
wire('z80', 'A13', 'disp', 'A13');
|
||||
for (const i of range(8)) for (const c of ['rom', 'ram', 'disp']) wire('z80', `D${i}`, c, `D${i}`);
|
||||
wire('z80', 'RD', 'rom', 'OE'); wire('z80', 'RD', 'ram', 'OE');
|
||||
wire('z80', 'WR', 'ram', 'WE'); wire('z80', 'WR', 'disp', 'WR');
|
||||
// reset-gen drives the Z80 control lines (the example wiring).
|
||||
wire('rst', 'RESET', 'z80', 'RESET'); wire('rst', 'WAIT', 'z80', 'WAIT');
|
||||
wire('rst', 'BUSREQ', 'z80', 'BUSREQ'); wire('rst', 'INT', 'z80', 'INT'); wire('rst', 'NMI', 'z80', 'NMI');
|
||||
|
||||
const STATE: ChipNetState = { wires: W, components: ['z80', 'rom', 'ram', 'inv', 'disp', 'rst'].map((id) => ({ id, metadataId: 'custom-chip' })), boards: [] };
|
||||
const pk = (c: string, p: string): number => resolveChipNetKey(STATE, c, p) ?? syntheticChipPin(c, p);
|
||||
const wf = (c: string, pins: string[]) => new Map(pins.map((p) => [p, pk(c, p)] as [string, number]));
|
||||
|
||||
const cellLit = (fb: Uint8Array, col: number, row: number): number => {
|
||||
let n = 0;
|
||||
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (fb[((row * 8 + y) * 256 + (col * 8 + x)) * 4 + 1] > 0x80) n++;
|
||||
return n;
|
||||
};
|
||||
|
||||
describe.skipIf(!have)('chipbus Phase 3 — RESET ordering race via reset-gen', () => {
|
||||
beforeEach(() => { setChipBusEnabledForTest(true); resetChipNetIndexForTest(); resetBusNets(); });
|
||||
afterEach(() => { setChipBusEnabledForTest(null); resetChipNetIndexForTest(); resetBusNets(); });
|
||||
|
||||
const mk = async (pm: PinManager, k: keyof typeof P, id: string, pins: string[], display?: { width: number; height: number }) =>
|
||||
ChipInstance.create({ wasm: new Uint8Array(readFileSync(P[k])), componentId: id, pinManager: pm, wires: wf(id, pins), display });
|
||||
|
||||
// Wall-clock-ish: a huge `now` so reset-gen's 2 ms one-shot is already due on
|
||||
// the first tick (exactly what the browser feeds via performance.now()*1e6).
|
||||
const NOW = BigInt(120_000 * 250);
|
||||
|
||||
it('RACE: reset-gen releases RESET before the Z80 exists — must still boot', async () => {
|
||||
const pm = new PinManager();
|
||||
// reset-gen loads and ticks FIRST: it drives RESET low at setup then high on
|
||||
// this tick — the rising edge happens with no Z80 watching yet.
|
||||
const rst = await mk(pm, 'rst', 'rst', RST); rst.start();
|
||||
rst.tickTimers(NOW); // release RESET high (edge lost — nothing is listening)
|
||||
|
||||
// Only now does the (larger) Z80 finish instantiating + register its watch.
|
||||
const z80 = await mk(pm, 'z80', 'z80', Z80); z80.start();
|
||||
(await mk(pm, 'rom', 'rom', ROM)).start();
|
||||
(await mk(pm, 'ram', 'ram', RAM)).start();
|
||||
(await mk(pm, 'inv', 'inv', INV)).start();
|
||||
const disp = await mk(pm, 'disp', 'disp', DISP, { width: 256, height: 128 });
|
||||
let fb: Uint8Array | null = null; disp.onFramebufferUpdate((r) => { fb = r as Uint8Array; }); disp.start();
|
||||
|
||||
rst.tickTimers(NOW); // re-assert reset-gen outputs (WAIT/INT/NMI high)
|
||||
z80.tickTimers(NOW); // boot
|
||||
disp.tickTimers(50_000_000n);
|
||||
|
||||
expect(fb).not.toBeNull();
|
||||
// The ">" prompt at col 0 of row 1 proves the CPU left reset and ran the ROM.
|
||||
expect(cellLit(fb!, 0, 1), 'the BASIC ">" prompt rendered — the Z80 left reset').toBeGreaterThan(4);
|
||||
|
||||
z80.dispose(); rst.dispose(); disp.dispose();
|
||||
}, 60_000);
|
||||
|
||||
it('SAFE: Z80 created first, then reset-gen releases RESET — boots', async () => {
|
||||
const pm = new PinManager();
|
||||
const z80 = await mk(pm, 'z80', 'z80', Z80); z80.start();
|
||||
(await mk(pm, 'rom', 'rom', ROM)).start();
|
||||
(await mk(pm, 'ram', 'ram', RAM)).start();
|
||||
(await mk(pm, 'inv', 'inv', INV)).start();
|
||||
const disp = await mk(pm, 'disp', 'disp', DISP, { width: 256, height: 128 });
|
||||
let fb: Uint8Array | null = null; disp.onFramebufferUpdate((r) => { fb = r as Uint8Array; }); disp.start();
|
||||
const rst = await mk(pm, 'rst', 'rst', RST); rst.start();
|
||||
|
||||
rst.tickTimers(NOW); // NOW the rising edge is delivered to a live watch
|
||||
z80.tickTimers(NOW);
|
||||
disp.tickTimers(50_000_000n);
|
||||
|
||||
expect(fb).not.toBeNull();
|
||||
expect(cellLit(fb!, 0, 1), 'the BASIC ">" prompt rendered').toBeGreaterThan(4);
|
||||
|
||||
z80.dispose(); rst.dispose(); disp.dispose();
|
||||
}, 60_000);
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* galaksija-ram-display - 64 KB SRAM that ALSO renders the Galaksija text
|
||||
* screen from its own video RAM (Phase 3, project/multichip-bus/).
|
||||
*
|
||||
* This is galaksija-ram (64 KB, yields 0x2000-0x203F reads to the keyboard)
|
||||
* with the display folded in. Crucially it renders from the ACTUAL contents of
|
||||
* its video RAM (0x2800-0x2BFF) on a ~30 fps timer, NOT from snooped bus writes.
|
||||
*
|
||||
* The earlier split (separate galaksija-ram + galaksija-display) made the
|
||||
* display a passive write-snoop. That loses the picture whenever it misses a
|
||||
* write: the Galaksija ROM paints the whole screen ONCE at boot then idles
|
||||
* polling the keyboard, so a snoop that comes up late (the chips load
|
||||
* asynchronously in the browser) shows stale/blank content forever. Reading the
|
||||
* memory it already owns makes the display correct regardless of load order -
|
||||
* exactly how the real machine generates its picture by scanning video RAM.
|
||||
*
|
||||
* Pin contract: idealised 64 KB byte-wide SRAM.
|
||||
* A0..A15 input 16-bit address
|
||||
* D0..D7 bidirectional 8-bit data (output on read, input on write)
|
||||
* CE/OE/WE input active-low chip/output/write enables
|
||||
* VCC, GND power
|
||||
* Read 0x2000-0x203F is released (the memory-mapped keyboard drives it).
|
||||
*
|
||||
* Video RAM: the ROM stores ASCII codes at 0x2800-0x2BFF (32x16). Rendered with
|
||||
* the public-domain IBM/VGA 8x8 font (font8x8 by Daniel Hepper / Marcel
|
||||
* Sondaar), green-on-black phosphor; 1 byte/row, bit 0 (LSB) = leftmost pixel.
|
||||
*/
|
||||
#include "velxio-chip.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define RAM_SIZE 0x10000 /* 64 KB */
|
||||
/* Video RAM as THIS chip sees it. The board wires A0-A12 (A13 selects the chip
|
||||
via CE), so the CPU's 0x2000-0x3FFF window maps to internal mem[0x0000-0x1FFF]
|
||||
and the 0x2800 video RAM lands at internal 0x0800. The memory-mapped keyboard
|
||||
(0x2000-0x203F real -> 0x00-0x3F here) is yielded in update_outputs(). */
|
||||
#define VRAM_BASE 0x0800
|
||||
#define COLS 32
|
||||
#define ROWS 16
|
||||
#define FB_W (COLS*8)
|
||||
#define FB_H (ROWS*8)
|
||||
|
||||
static const uint8_t font8x8[1024] = {
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x3c,0x3c,0x18,0x18,0x00,0x18,0x00,
|
||||
0x36,0x36,0x00,0x00,0x00,0x00,0x00,0x00,0x36,0x36,0x7f,0x36,0x7f,0x36,0x36,0x00,
|
||||
0x0c,0x3e,0x03,0x1e,0x30,0x1f,0x0c,0x00,0x00,0x63,0x33,0x18,0x0c,0x66,0x63,0x00,
|
||||
0x1c,0x36,0x1c,0x6e,0x3b,0x33,0x6e,0x00,0x06,0x06,0x03,0x00,0x00,0x00,0x00,0x00,
|
||||
0x18,0x0c,0x06,0x06,0x06,0x0c,0x18,0x00,0x06,0x0c,0x18,0x18,0x18,0x0c,0x06,0x00,
|
||||
0x00,0x66,0x3c,0xff,0x3c,0x66,0x00,0x00,0x00,0x0c,0x0c,0x3f,0x0c,0x0c,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x06,0x00,0x00,0x00,0x3f,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x00,0x60,0x30,0x18,0x0c,0x06,0x03,0x01,0x00,
|
||||
0x3e,0x63,0x73,0x7b,0x6f,0x67,0x3e,0x00,0x0c,0x0e,0x0c,0x0c,0x0c,0x0c,0x3f,0x00,
|
||||
0x1e,0x33,0x30,0x1c,0x06,0x33,0x3f,0x00,0x1e,0x33,0x30,0x1c,0x30,0x33,0x1e,0x00,
|
||||
0x38,0x3c,0x36,0x33,0x7f,0x30,0x78,0x00,0x3f,0x03,0x1f,0x30,0x30,0x33,0x1e,0x00,
|
||||
0x1c,0x06,0x03,0x1f,0x33,0x33,0x1e,0x00,0x3f,0x33,0x30,0x18,0x0c,0x0c,0x0c,0x00,
|
||||
0x1e,0x33,0x33,0x1e,0x33,0x33,0x1e,0x00,0x1e,0x33,0x33,0x3e,0x30,0x18,0x0e,0x00,
|
||||
0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x06,
|
||||
0x18,0x0c,0x06,0x03,0x06,0x0c,0x18,0x00,0x00,0x00,0x3f,0x00,0x00,0x3f,0x00,0x00,
|
||||
0x06,0x0c,0x18,0x30,0x18,0x0c,0x06,0x00,0x1e,0x33,0x30,0x18,0x0c,0x00,0x0c,0x00,
|
||||
0x3e,0x63,0x7b,0x7b,0x7b,0x03,0x1e,0x00,0x0c,0x1e,0x33,0x33,0x3f,0x33,0x33,0x00,
|
||||
0x3f,0x66,0x66,0x3e,0x66,0x66,0x3f,0x00,0x3c,0x66,0x03,0x03,0x03,0x66,0x3c,0x00,
|
||||
0x1f,0x36,0x66,0x66,0x66,0x36,0x1f,0x00,0x7f,0x46,0x16,0x1e,0x16,0x46,0x7f,0x00,
|
||||
0x7f,0x46,0x16,0x1e,0x16,0x06,0x0f,0x00,0x3c,0x66,0x03,0x03,0x73,0x66,0x7c,0x00,
|
||||
0x33,0x33,0x33,0x3f,0x33,0x33,0x33,0x00,0x1e,0x0c,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,
|
||||
0x78,0x30,0x30,0x30,0x33,0x33,0x1e,0x00,0x67,0x66,0x36,0x1e,0x36,0x66,0x67,0x00,
|
||||
0x0f,0x06,0x06,0x06,0x46,0x66,0x7f,0x00,0x63,0x77,0x7f,0x7f,0x6b,0x63,0x63,0x00,
|
||||
0x63,0x67,0x6f,0x7b,0x73,0x63,0x63,0x00,0x1c,0x36,0x63,0x63,0x63,0x36,0x1c,0x00,
|
||||
0x3f,0x66,0x66,0x3e,0x06,0x06,0x0f,0x00,0x1e,0x33,0x33,0x33,0x3b,0x1e,0x38,0x00,
|
||||
0x3f,0x66,0x66,0x3e,0x36,0x66,0x67,0x00,0x1e,0x33,0x07,0x0e,0x38,0x33,0x1e,0x00,
|
||||
0x3f,0x2d,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,0x33,0x33,0x33,0x33,0x33,0x33,0x3f,0x00,
|
||||
0x33,0x33,0x33,0x33,0x33,0x1e,0x0c,0x00,0x63,0x63,0x63,0x6b,0x7f,0x77,0x63,0x00,
|
||||
0x63,0x63,0x36,0x1c,0x1c,0x36,0x63,0x00,0x33,0x33,0x33,0x1e,0x0c,0x0c,0x1e,0x00,
|
||||
0x7f,0x63,0x31,0x18,0x4c,0x66,0x7f,0x00,0x1e,0x06,0x06,0x06,0x06,0x06,0x1e,0x00,
|
||||
0x03,0x06,0x0c,0x18,0x30,0x60,0x40,0x00,0x1e,0x18,0x18,0x18,0x18,0x18,0x1e,0x00,
|
||||
0x08,0x1c,0x36,0x63,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,
|
||||
0x0c,0x0c,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1e,0x30,0x3e,0x33,0x6e,0x00,
|
||||
0x07,0x06,0x06,0x3e,0x66,0x66,0x3b,0x00,0x00,0x00,0x1e,0x33,0x03,0x33,0x1e,0x00,
|
||||
0x38,0x30,0x30,0x3e,0x33,0x33,0x6e,0x00,0x00,0x00,0x1e,0x33,0x3f,0x03,0x1e,0x00,
|
||||
0x1c,0x36,0x06,0x0f,0x06,0x06,0x0f,0x00,0x00,0x00,0x6e,0x33,0x33,0x3e,0x30,0x1f,
|
||||
0x07,0x06,0x36,0x6e,0x66,0x66,0x67,0x00,0x0c,0x00,0x0e,0x0c,0x0c,0x0c,0x1e,0x00,
|
||||
0x30,0x00,0x30,0x30,0x30,0x33,0x33,0x1e,0x07,0x06,0x66,0x36,0x1e,0x36,0x67,0x00,
|
||||
0x0e,0x0c,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,0x00,0x00,0x33,0x7f,0x7f,0x6b,0x63,0x00,
|
||||
0x00,0x00,0x1f,0x33,0x33,0x33,0x33,0x00,0x00,0x00,0x1e,0x33,0x33,0x33,0x1e,0x00,
|
||||
0x00,0x00,0x3b,0x66,0x66,0x3e,0x06,0x0f,0x00,0x00,0x6e,0x33,0x33,0x3e,0x30,0x78,
|
||||
0x00,0x00,0x3b,0x6e,0x66,0x06,0x0f,0x00,0x00,0x00,0x3e,0x03,0x1e,0x30,0x1f,0x00,
|
||||
0x08,0x0c,0x3e,0x0c,0x0c,0x2c,0x18,0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x6e,0x00,
|
||||
0x00,0x00,0x33,0x33,0x33,0x1e,0x0c,0x00,0x00,0x00,0x63,0x6b,0x7f,0x7f,0x36,0x00,
|
||||
0x00,0x00,0x63,0x36,0x1c,0x36,0x63,0x00,0x00,0x00,0x33,0x33,0x33,0x3e,0x30,0x1f,
|
||||
0x00,0x00,0x3f,0x19,0x0c,0x26,0x3f,0x00,0x38,0x0c,0x0c,0x07,0x0c,0x0c,0x38,0x00,
|
||||
0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x00,0x07,0x0c,0x0c,0x38,0x0c,0x0c,0x07,0x00,
|
||||
0x6e,0x3b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
vx_pin a[16];
|
||||
vx_pin d[8];
|
||||
vx_pin ce, oe, we, vcc, gnd;
|
||||
uint8_t* mem;
|
||||
bool driving;
|
||||
int we_last;
|
||||
vx_buffer fb;
|
||||
vx_timer paint;
|
||||
} chip_t;
|
||||
|
||||
static chip_t G;
|
||||
static uint8_t fbpix[FB_W*FB_H*4];
|
||||
|
||||
static uint16_t read_addr(void) {
|
||||
uint16_t v = 0;
|
||||
for (int i = 0; i < 16; i++) if (vx_pin_read(G.a[i])) v |= (1u << i);
|
||||
return v;
|
||||
}
|
||||
static uint8_t read_data_bus(void) {
|
||||
uint8_t v = 0;
|
||||
for (int i = 0; i < 8; i++) if (vx_pin_read(G.d[i])) v |= (1u << i);
|
||||
return v;
|
||||
}
|
||||
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);
|
||||
int we_low = (vx_pin_read(G.we) == 0);
|
||||
if (ce_low && oe_low && !we_low) {
|
||||
uint16_t addr = read_addr();
|
||||
if (addr < 0x40) { release_data(); return; } /* keyboard owns 0x2000-0x203F */
|
||||
drive_data(G.mem[addr]);
|
||||
} else {
|
||||
release_data();
|
||||
}
|
||||
}
|
||||
|
||||
static void on_addr_or_ctrl(void* u, vx_pin p, int v) { (void)u;(void)p;(void)v; update_outputs(); }
|
||||
|
||||
static void on_we(void* u, vx_pin p, int value) {
|
||||
(void)u;(void)p;
|
||||
int ce_low = (vx_pin_read(G.ce) == 0);
|
||||
if (G.we_last == 0 && value == 1 && ce_low) {
|
||||
uint16_t addr = read_addr();
|
||||
G.mem[addr] = read_data_bus();
|
||||
}
|
||||
G.we_last = value;
|
||||
update_outputs();
|
||||
}
|
||||
|
||||
static void put_px(int x, int y, uint8_t lit) {
|
||||
if ((unsigned)x < FB_W && (unsigned)y < FB_H) {
|
||||
int o = (y*FB_W + x)*4;
|
||||
fbpix[o]=lit?0x33:0x00; fbpix[o+1]=lit?0xE0:0x12; fbpix[o+2]=lit?0x33:0x00; fbpix[o+3]=0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* Render the whole 32x16 screen from video RAM, then blit. Reading the memory
|
||||
we own (rather than snooping writes) keeps the picture correct no matter when
|
||||
this chip was instantiated relative to the CPU. */
|
||||
static void on_paint(void* u) {
|
||||
(void)u;
|
||||
for (int row = 0; row < ROWS; row++) {
|
||||
for (int col = 0; col < COLS; col++) {
|
||||
uint8_t ch = G.mem[VRAM_BASE + row*COLS + col] & 0x7f;
|
||||
for (int line = 0; line < 8; line++) {
|
||||
uint8_t bits = font8x8[ch*8 + line];
|
||||
for (int px = 0; px < 8; px++) put_px(col*8+px, row*8+line, (bits >> px) & 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix));
|
||||
}
|
||||
|
||||
void chip_setup(void) {
|
||||
char name[4];
|
||||
for (int i = 0; i < 16; 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.we = vx_pin_register("WE", VX_INPUT);
|
||||
G.vcc = vx_pin_register("VCC", VX_INPUT);
|
||||
G.gnd = vx_pin_register("GND", VX_INPUT);
|
||||
|
||||
G.mem = (uint8_t*)calloc(RAM_SIZE, 1);
|
||||
G.driving = false;
|
||||
G.we_last = vx_pin_read(G.we);
|
||||
|
||||
for (int i = 0; i < 16; i++) vx_pin_watch(G.a[i], VX_EDGE_BOTH, on_addr_or_ctrl, 0);
|
||||
vx_pin_watch(G.ce, VX_EDGE_BOTH, on_addr_or_ctrl, 0);
|
||||
vx_pin_watch(G.oe, VX_EDGE_BOTH, on_addr_or_ctrl, 0);
|
||||
vx_pin_watch(G.we, VX_EDGE_BOTH, on_we, 0);
|
||||
update_outputs();
|
||||
|
||||
uint32_t w, h;
|
||||
G.fb = vx_framebuffer_init(&w, &h);
|
||||
for (int i = 0; i < FB_W*FB_H; i++) { fbpix[i*4]=0; fbpix[i*4+1]=0x12; fbpix[i*4+2]=0; fbpix[i*4+3]=0xFF; }
|
||||
vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix));
|
||||
G.paint = vx_timer_create(on_paint, 0);
|
||||
vx_timer_start(G.paint, 33000000ULL, true); /* ~30 fps */
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schema": "velxio-chip/v1",
|
||||
"name": "RAM 64K + Display (Galaksija)",
|
||||
"author": "Velxio (font8x8: Daniel Hepper / Marcel Sondaar, public domain)",
|
||||
"license": "MIT (chip) / public-domain (font)",
|
||||
"description": "64KB SRAM that also renders the Galaksija 32x16 text screen from its own video RAM (0x2800-0x2BFF) into a 256x128 framebuffer on a ~30fps timer. Reads from real memory (not snooped writes) so the picture is correct regardless of chip load order. Yields reads of 0x2000-0x203F to the memory-mapped keyboard. Phase 3 chip-to-chip bus proof.",
|
||||
"display": { "width": 256, "height": 128 },
|
||||
"pins": ["A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","A14","A15","D0","D1","D2","D3","D4","D5","D6","D7","CE","OE","WE","VCC","GND"]
|
||||
}
|
||||
|
|
@ -966,6 +966,15 @@ static void on_int(void* user_data, vx_pin pin, int value) {
|
|||
|
||||
static void on_clock(void* user_data) {
|
||||
(void)user_data;
|
||||
/* RESET̅ is level-sensitive on real silicon. on_reset() reinitialises the
|
||||
core on the falling edge, but if the releasing RISING edge arrived before
|
||||
this chip registered its watch — which happens on a multi-chip async
|
||||
load, where a power-on-reset generator can drive RESET̅ high before the
|
||||
(larger, slower-loading) CPU's watch is live — that one edge is lost and
|
||||
the CPU would stay in reset forever. Sample the level here so a missed
|
||||
edge self-corrects. An undriven RESET̅ resolves to Z and reads low, so the
|
||||
CPU safely stays in reset until something actually drives it high. */
|
||||
if (G.reset_active && vx_pin_read(G.reset_) != 0) G.reset_active = false;
|
||||
if (G.reset_active) return;
|
||||
if (vx_pin_read(G.busreq) == 0) {
|
||||
vx_pin_write(G.busack, 0);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* galaksija-ram-display - 64 KB SRAM that ALSO renders the Galaksija text
|
||||
* screen from its own video RAM (Phase 3, project/multichip-bus/).
|
||||
*
|
||||
* This is galaksija-ram (64 KB, yields 0x2000-0x203F reads to the keyboard)
|
||||
* with the display folded in. Crucially it renders from the ACTUAL contents of
|
||||
* its video RAM (0x2800-0x2BFF) on a ~30 fps timer, NOT from snooped bus writes.
|
||||
*
|
||||
* The earlier split (separate galaksija-ram + galaksija-display) made the
|
||||
* display a passive write-snoop. That loses the picture whenever it misses a
|
||||
* write: the Galaksija ROM paints the whole screen ONCE at boot then idles
|
||||
* polling the keyboard, so a snoop that comes up late (the chips load
|
||||
* asynchronously in the browser) shows stale/blank content forever. Reading the
|
||||
* memory it already owns makes the display correct regardless of load order -
|
||||
* exactly how the real machine generates its picture by scanning video RAM.
|
||||
*
|
||||
* Pin contract: idealised 64 KB byte-wide SRAM.
|
||||
* A0..A15 input 16-bit address
|
||||
* D0..D7 bidirectional 8-bit data (output on read, input on write)
|
||||
* CE/OE/WE input active-low chip/output/write enables
|
||||
* VCC, GND power
|
||||
* Read 0x2000-0x203F is released (the memory-mapped keyboard drives it).
|
||||
*
|
||||
* Video RAM: the ROM stores ASCII codes at 0x2800-0x2BFF (32x16). Rendered with
|
||||
* the public-domain IBM/VGA 8x8 font (font8x8 by Daniel Hepper / Marcel
|
||||
* Sondaar), green-on-black phosphor; 1 byte/row, bit 0 (LSB) = leftmost pixel.
|
||||
*/
|
||||
#include "velxio-chip.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define RAM_SIZE 0x10000 /* 64 KB */
|
||||
/* Video RAM as THIS chip sees it. The board wires A0-A12 (A13 selects the chip
|
||||
via CE), so the CPU's 0x2000-0x3FFF window maps to internal mem[0x0000-0x1FFF]
|
||||
and the 0x2800 video RAM lands at internal 0x0800. The memory-mapped keyboard
|
||||
(0x2000-0x203F real -> 0x00-0x3F here) is yielded in update_outputs(). */
|
||||
#define VRAM_BASE 0x0800
|
||||
#define COLS 32
|
||||
#define ROWS 16
|
||||
#define FB_W (COLS*8)
|
||||
#define FB_H (ROWS*8)
|
||||
|
||||
static const uint8_t font8x8[1024] = {
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x3c,0x3c,0x18,0x18,0x00,0x18,0x00,
|
||||
0x36,0x36,0x00,0x00,0x00,0x00,0x00,0x00,0x36,0x36,0x7f,0x36,0x7f,0x36,0x36,0x00,
|
||||
0x0c,0x3e,0x03,0x1e,0x30,0x1f,0x0c,0x00,0x00,0x63,0x33,0x18,0x0c,0x66,0x63,0x00,
|
||||
0x1c,0x36,0x1c,0x6e,0x3b,0x33,0x6e,0x00,0x06,0x06,0x03,0x00,0x00,0x00,0x00,0x00,
|
||||
0x18,0x0c,0x06,0x06,0x06,0x0c,0x18,0x00,0x06,0x0c,0x18,0x18,0x18,0x0c,0x06,0x00,
|
||||
0x00,0x66,0x3c,0xff,0x3c,0x66,0x00,0x00,0x00,0x0c,0x0c,0x3f,0x0c,0x0c,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x06,0x00,0x00,0x00,0x3f,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x00,0x60,0x30,0x18,0x0c,0x06,0x03,0x01,0x00,
|
||||
0x3e,0x63,0x73,0x7b,0x6f,0x67,0x3e,0x00,0x0c,0x0e,0x0c,0x0c,0x0c,0x0c,0x3f,0x00,
|
||||
0x1e,0x33,0x30,0x1c,0x06,0x33,0x3f,0x00,0x1e,0x33,0x30,0x1c,0x30,0x33,0x1e,0x00,
|
||||
0x38,0x3c,0x36,0x33,0x7f,0x30,0x78,0x00,0x3f,0x03,0x1f,0x30,0x30,0x33,0x1e,0x00,
|
||||
0x1c,0x06,0x03,0x1f,0x33,0x33,0x1e,0x00,0x3f,0x33,0x30,0x18,0x0c,0x0c,0x0c,0x00,
|
||||
0x1e,0x33,0x33,0x1e,0x33,0x33,0x1e,0x00,0x1e,0x33,0x33,0x3e,0x30,0x18,0x0e,0x00,
|
||||
0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x00,0x00,0x0c,0x0c,0x06,
|
||||
0x18,0x0c,0x06,0x03,0x06,0x0c,0x18,0x00,0x00,0x00,0x3f,0x00,0x00,0x3f,0x00,0x00,
|
||||
0x06,0x0c,0x18,0x30,0x18,0x0c,0x06,0x00,0x1e,0x33,0x30,0x18,0x0c,0x00,0x0c,0x00,
|
||||
0x3e,0x63,0x7b,0x7b,0x7b,0x03,0x1e,0x00,0x0c,0x1e,0x33,0x33,0x3f,0x33,0x33,0x00,
|
||||
0x3f,0x66,0x66,0x3e,0x66,0x66,0x3f,0x00,0x3c,0x66,0x03,0x03,0x03,0x66,0x3c,0x00,
|
||||
0x1f,0x36,0x66,0x66,0x66,0x36,0x1f,0x00,0x7f,0x46,0x16,0x1e,0x16,0x46,0x7f,0x00,
|
||||
0x7f,0x46,0x16,0x1e,0x16,0x06,0x0f,0x00,0x3c,0x66,0x03,0x03,0x73,0x66,0x7c,0x00,
|
||||
0x33,0x33,0x33,0x3f,0x33,0x33,0x33,0x00,0x1e,0x0c,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,
|
||||
0x78,0x30,0x30,0x30,0x33,0x33,0x1e,0x00,0x67,0x66,0x36,0x1e,0x36,0x66,0x67,0x00,
|
||||
0x0f,0x06,0x06,0x06,0x46,0x66,0x7f,0x00,0x63,0x77,0x7f,0x7f,0x6b,0x63,0x63,0x00,
|
||||
0x63,0x67,0x6f,0x7b,0x73,0x63,0x63,0x00,0x1c,0x36,0x63,0x63,0x63,0x36,0x1c,0x00,
|
||||
0x3f,0x66,0x66,0x3e,0x06,0x06,0x0f,0x00,0x1e,0x33,0x33,0x33,0x3b,0x1e,0x38,0x00,
|
||||
0x3f,0x66,0x66,0x3e,0x36,0x66,0x67,0x00,0x1e,0x33,0x07,0x0e,0x38,0x33,0x1e,0x00,
|
||||
0x3f,0x2d,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,0x33,0x33,0x33,0x33,0x33,0x33,0x3f,0x00,
|
||||
0x33,0x33,0x33,0x33,0x33,0x1e,0x0c,0x00,0x63,0x63,0x63,0x6b,0x7f,0x77,0x63,0x00,
|
||||
0x63,0x63,0x36,0x1c,0x1c,0x36,0x63,0x00,0x33,0x33,0x33,0x1e,0x0c,0x0c,0x1e,0x00,
|
||||
0x7f,0x63,0x31,0x18,0x4c,0x66,0x7f,0x00,0x1e,0x06,0x06,0x06,0x06,0x06,0x1e,0x00,
|
||||
0x03,0x06,0x0c,0x18,0x30,0x60,0x40,0x00,0x1e,0x18,0x18,0x18,0x18,0x18,0x1e,0x00,
|
||||
0x08,0x1c,0x36,0x63,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,
|
||||
0x0c,0x0c,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1e,0x30,0x3e,0x33,0x6e,0x00,
|
||||
0x07,0x06,0x06,0x3e,0x66,0x66,0x3b,0x00,0x00,0x00,0x1e,0x33,0x03,0x33,0x1e,0x00,
|
||||
0x38,0x30,0x30,0x3e,0x33,0x33,0x6e,0x00,0x00,0x00,0x1e,0x33,0x3f,0x03,0x1e,0x00,
|
||||
0x1c,0x36,0x06,0x0f,0x06,0x06,0x0f,0x00,0x00,0x00,0x6e,0x33,0x33,0x3e,0x30,0x1f,
|
||||
0x07,0x06,0x36,0x6e,0x66,0x66,0x67,0x00,0x0c,0x00,0x0e,0x0c,0x0c,0x0c,0x1e,0x00,
|
||||
0x30,0x00,0x30,0x30,0x30,0x33,0x33,0x1e,0x07,0x06,0x66,0x36,0x1e,0x36,0x67,0x00,
|
||||
0x0e,0x0c,0x0c,0x0c,0x0c,0x0c,0x1e,0x00,0x00,0x00,0x33,0x7f,0x7f,0x6b,0x63,0x00,
|
||||
0x00,0x00,0x1f,0x33,0x33,0x33,0x33,0x00,0x00,0x00,0x1e,0x33,0x33,0x33,0x1e,0x00,
|
||||
0x00,0x00,0x3b,0x66,0x66,0x3e,0x06,0x0f,0x00,0x00,0x6e,0x33,0x33,0x3e,0x30,0x78,
|
||||
0x00,0x00,0x3b,0x6e,0x66,0x06,0x0f,0x00,0x00,0x00,0x3e,0x03,0x1e,0x30,0x1f,0x00,
|
||||
0x08,0x0c,0x3e,0x0c,0x0c,0x2c,0x18,0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x6e,0x00,
|
||||
0x00,0x00,0x33,0x33,0x33,0x1e,0x0c,0x00,0x00,0x00,0x63,0x6b,0x7f,0x7f,0x36,0x00,
|
||||
0x00,0x00,0x63,0x36,0x1c,0x36,0x63,0x00,0x00,0x00,0x33,0x33,0x33,0x3e,0x30,0x1f,
|
||||
0x00,0x00,0x3f,0x19,0x0c,0x26,0x3f,0x00,0x38,0x0c,0x0c,0x07,0x0c,0x0c,0x38,0x00,
|
||||
0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x00,0x07,0x0c,0x0c,0x38,0x0c,0x0c,0x07,0x00,
|
||||
0x6e,0x3b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
vx_pin a[16];
|
||||
vx_pin d[8];
|
||||
vx_pin ce, oe, we, vcc, gnd;
|
||||
uint8_t* mem;
|
||||
bool driving;
|
||||
int we_last;
|
||||
vx_buffer fb;
|
||||
vx_timer paint;
|
||||
} chip_t;
|
||||
|
||||
static chip_t G;
|
||||
static uint8_t fbpix[FB_W*FB_H*4];
|
||||
|
||||
static uint16_t read_addr(void) {
|
||||
uint16_t v = 0;
|
||||
for (int i = 0; i < 16; i++) if (vx_pin_read(G.a[i])) v |= (1u << i);
|
||||
return v;
|
||||
}
|
||||
static uint8_t read_data_bus(void) {
|
||||
uint8_t v = 0;
|
||||
for (int i = 0; i < 8; i++) if (vx_pin_read(G.d[i])) v |= (1u << i);
|
||||
return v;
|
||||
}
|
||||
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);
|
||||
int we_low = (vx_pin_read(G.we) == 0);
|
||||
if (ce_low && oe_low && !we_low) {
|
||||
uint16_t addr = read_addr();
|
||||
if (addr < 0x40) { release_data(); return; } /* keyboard owns 0x2000-0x203F */
|
||||
drive_data(G.mem[addr]);
|
||||
} else {
|
||||
release_data();
|
||||
}
|
||||
}
|
||||
|
||||
static void on_addr_or_ctrl(void* u, vx_pin p, int v) { (void)u;(void)p;(void)v; update_outputs(); }
|
||||
|
||||
static void on_we(void* u, vx_pin p, int value) {
|
||||
(void)u;(void)p;
|
||||
int ce_low = (vx_pin_read(G.ce) == 0);
|
||||
if (G.we_last == 0 && value == 1 && ce_low) {
|
||||
uint16_t addr = read_addr();
|
||||
G.mem[addr] = read_data_bus();
|
||||
}
|
||||
G.we_last = value;
|
||||
update_outputs();
|
||||
}
|
||||
|
||||
static void put_px(int x, int y, uint8_t lit) {
|
||||
if ((unsigned)x < FB_W && (unsigned)y < FB_H) {
|
||||
int o = (y*FB_W + x)*4;
|
||||
fbpix[o]=lit?0x33:0x00; fbpix[o+1]=lit?0xE0:0x12; fbpix[o+2]=lit?0x33:0x00; fbpix[o+3]=0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* Render the whole 32x16 screen from video RAM, then blit. Reading the memory
|
||||
we own (rather than snooping writes) keeps the picture correct no matter when
|
||||
this chip was instantiated relative to the CPU. */
|
||||
static void on_paint(void* u) {
|
||||
(void)u;
|
||||
for (int row = 0; row < ROWS; row++) {
|
||||
for (int col = 0; col < COLS; col++) {
|
||||
uint8_t ch = G.mem[VRAM_BASE + row*COLS + col] & 0x7f;
|
||||
for (int line = 0; line < 8; line++) {
|
||||
uint8_t bits = font8x8[ch*8 + line];
|
||||
for (int px = 0; px < 8; px++) put_px(col*8+px, row*8+line, (bits >> px) & 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix));
|
||||
}
|
||||
|
||||
void chip_setup(void) {
|
||||
char name[4];
|
||||
for (int i = 0; i < 16; 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.we = vx_pin_register("WE", VX_INPUT);
|
||||
G.vcc = vx_pin_register("VCC", VX_INPUT);
|
||||
G.gnd = vx_pin_register("GND", VX_INPUT);
|
||||
|
||||
G.mem = (uint8_t*)calloc(RAM_SIZE, 1);
|
||||
G.driving = false;
|
||||
G.we_last = vx_pin_read(G.we);
|
||||
|
||||
for (int i = 0; i < 16; i++) vx_pin_watch(G.a[i], VX_EDGE_BOTH, on_addr_or_ctrl, 0);
|
||||
vx_pin_watch(G.ce, VX_EDGE_BOTH, on_addr_or_ctrl, 0);
|
||||
vx_pin_watch(G.oe, VX_EDGE_BOTH, on_addr_or_ctrl, 0);
|
||||
vx_pin_watch(G.we, VX_EDGE_BOTH, on_we, 0);
|
||||
update_outputs();
|
||||
|
||||
uint32_t w, h;
|
||||
G.fb = vx_framebuffer_init(&w, &h);
|
||||
for (int i = 0; i < FB_W*FB_H; i++) { fbpix[i*4]=0; fbpix[i*4+1]=0x12; fbpix[i*4+2]=0; fbpix[i*4+3]=0xFF; }
|
||||
vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix));
|
||||
G.paint = vx_timer_create(on_paint, 0);
|
||||
vx_timer_start(G.paint, 33000000ULL, true); /* ~30 fps */
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schema": "velxio-chip/v1",
|
||||
"name": "RAM 64K + Display (Galaksija)",
|
||||
"author": "Velxio (font8x8: Daniel Hepper / Marcel Sondaar, public domain)",
|
||||
"license": "MIT (chip) / public-domain (font)",
|
||||
"description": "64KB SRAM that also renders the Galaksija 32x16 text screen from its own video RAM (0x2800-0x2BFF) into a 256x128 framebuffer on a ~30fps timer. Reads from real memory (not snooped writes) so the picture is correct regardless of chip load order. Yields reads of 0x2000-0x203F to the memory-mapped keyboard. Phase 3 chip-to-chip bus proof.",
|
||||
"display": { "width": 256, "height": 128 },
|
||||
"pins": ["A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","A14","A15","D0","D1","D2","D3","D4","D5","D6","D7","CE","OE","WE","VCC","GND"]
|
||||
}
|
||||
Loading…
Reference in New Issue