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:
David Montero Crespo 2026-06-05 15:47:30 -03:00
parent 94627b99d2
commit 47adb0b1c8
11 changed files with 769 additions and 32 deletions

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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 */
}

View File

@ -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"]
}

View File

@ -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

View File

@ -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 */
}

View File

@ -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"]
}