feat(chipbus): Galaksija keyboard - type BASIC over the bus

Adds a memory-mapped keyboard so you can type into the Galaksija. Based on
the libretro Galaksija core's scheme (not guessed): reading 0x2000+offset
returns 0xFE when the key at that matrix offset is held, 0xFF otherwise;
the keyMap gives the offset per key ('A'=1 ... Enter=48, Space=31, etc.).

- galaksija-keyboard.c: drives reads of 0x2000-0x203F from a keys[] table and
  exports set_key(offset, down) for the host to push key events. Never drives
  outside the keyboard range.
- galaksija-ram.c: ram-64k variant that yields reads of 0x2000-0x203F to the
  keyboard (writes still go to RAM), so the two never fight for the bus.
- ChipRuntime: ChipInstance.hasKeyboard + setKey() expose the chip's set_key.
- CustomChipPart: bridges browser keydown/keyup (by KeyboardEvent.code, via
  GALAKSIJA_KEY_OFFSET) into the chip, ignoring keystrokes while the code
  editor or an input is focused so typing code is never hijacked.
- The gallery example gains the keyboard chip (now 7 chips, 99 wires) and uses
  galaksija-ram.

Test chipbus-galaksija-keyboard: pressing 'A' (offset 1) makes the BASIC
monitor echo "A" after its ">" prompt and advances the cursor. 41 chipbus
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-06-05 14:37:42 -03:00
parent a393e3e91d
commit 94627b99d2
14 changed files with 673 additions and 81 deletions

View File

@ -0,0 +1,96 @@
/**
* Phase 3 typing on the Galaksija over the chip-to-chip bus
* (project/multichip-bus/). The full machine plus a memory-mapped keyboard:
* Z80 + galaksija-rom + galaksija-ram + inverter + galaksija-display +
* galaksija-keyboard.
*
* Galaksija reads its keyboard as memory: reading 0x2000+offset returns 0xFE
* when the key at that matrix offset is held, 0xFF otherwise (the scheme used by
* the libretro Galaksija core; offsets from its keyMap, e.g. 'A' = 1). The
* keyboard chip drives those reads from a keys[] table that the host pushes via
* the exported set_key(offset, down); galaksija-ram yields reads of 0x2000-0x203F
* so the two never fight for the bus. Pressing 'A' (offset 1) makes the BASIC
* monitor echo "A" after its ">" prompt proving end-to-end keyboard input.
*/
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'), kbd: f('galaksija-keyboard.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 KBD = [...range(14).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'RD'];
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', 'kbd']) 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'); wire('z80', 'A13', 'kbd', 'A13');
for (const i of range(8)) for (const c of ['rom', 'ram', 'disp', 'kbd']) wire('z80', `D${i}`, c, `D${i}`);
wire('z80', 'RD', 'rom', 'OE'); wire('z80', 'RD', 'ram', 'OE'); wire('z80', 'RD', 'kbd', 'RD');
wire('z80', 'WR', 'ram', 'WE'); wire('z80', 'WR', 'disp', 'WR');
const STATE: ChipNetState = { wires: W, components: ['z80', 'rom', 'ram', 'inv', 'disp', 'kbd'].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]));
// Lit pixels (bright green) inside character cell (col,row).
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 — typing on the Galaksija', () => {
beforeEach(() => { setChipBusEnabledForTest(true); resetChipNetIndexForTest(); resetBusNets(); });
afterEach(() => { setChipBusEnabledForTest(null); resetChipNetIndexForTest(); resetBusNets(); });
it('pressing A echoes "A" after the BASIC prompt', async () => {
const pm = new PinManager();
const mk = async (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 });
const z80 = await mk('z80', 'z80', Z80); z80.start();
(await mk('rom', 'rom', ROM)).start();
(await mk('ram', 'ram', RAM)).start();
(await mk('inv', 'inv', INV)).start();
const disp = await mk('disp', 'disp', DISP, { width: 256, height: 128 });
let fb: Uint8Array | null = null; disp.onFramebufferUpdate((r) => { fb = r as Uint8Array; }); disp.start();
const kbd = await mk('kbd', 'kbd', KBD); kbd.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)); // boot to the READY prompt
disp.tickTimers(50_000_000n);
expect(fb).not.toBeNull();
// Boot screen row 1 is ">_": ">" at col 0, the cursor at col 1, col 2 blank.
expect(cellLit(fb!, 2, 1)).toBe(0);
// Press 'A' (offset 1), let the monitor scan + echo, then release.
const setKey = (off: number, down: number) => (kbd.exports as { set_key: (o: number, d: number) => void }).set_key(off, down);
setKey(1, 1);
z80.tickTimers(BigInt(240000 * 250));
setKey(1, 0);
z80.tickTimers(BigInt(360000 * 250));
disp.tickTimers(420_000_000n);
// "A" was echoed at col 1 and the cursor advanced to col 2: row 1 now reads
// ">A_". The cursor at col 2 (previously blank) proves a character was typed.
expect(cellLit(fb!, 2, 1), 'the cursor advanced — a character was typed').toBeGreaterThan(0);
expect(cellLit(fb!, 1, 1), 'the "A" glyph is at the input column').toBeGreaterThan(6);
z80.dispose(); kbd.dispose(); disp.dispose();
}, 60_000);
});

View File

@ -0,0 +1,43 @@
/*
* galaksija-keyboard Galaksija memory-mapped keyboard (Phase 3,
* project/multichip-bus/). Replicates the scheme used by the libretro Galaksija
* core: the keyboard occupies addresses 0x2000-0x203F; reading 0x2000+offset
* returns 0xFE when the key at that matrix offset is pressed, 0xFF otherwise.
* The chip drives the data bus on a memory READ in that range and exposes
* set_key(offset, down) for the host to push browser key events. It never
* drives outside the keyboard range; the paired galaksija-ram releases reads in
* 0x2000-0x203F so the two never fight for the bus.
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
static vx_pin a[14], d[8], rd;
static int rd_last;
static bool driving;
static uint8_t keys[64];
static uint16_t read_addr(void){ uint16_t v=0; for(int i=0;i<14;i++) if(vx_pin_read(a[i])) v|=(1u<<i); return v; }
static int in_range(uint16_t addr){ return (addr & 0x3FC0) == 0x2000; } /* 0x2000-0x203F */
static void drive_data(uint8_t v){ for(int i=0;i<8;i++){ vx_pin_set_mode(d[i],VX_OUTPUT); vx_pin_write(d[i],(v>>i)&1);} driving=true; }
static void release_data(void){ if(!driving) return; for(int i=0;i<8;i++) vx_pin_set_mode(d[i],VX_INPUT); driving=false; }
static void update(void){
if(vx_pin_read(rd)==0){ uint16_t addr=read_addr(); if(in_range(addr)) drive_data(keys[addr & 0x3F]); else release_data(); }
else release_data();
}
static void on_change(void* u, vx_pin p, int v){ (void)u;(void)p;(void)v; update(); }
/* Exported: the host (browser keydown/keyup bridge) sets a key's state. */
void set_key(int offset, int down){ if(offset>=0 && offset<64) keys[offset] = down ? 0xFE : 0xFF; }
void chip_setup(void){
char name[4];
for(int i=0;i<14;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;} 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; d[i]=vx_pin_register(name,VX_INPUT);}
rd=vx_pin_register("RD",VX_INPUT); rd_last=vx_pin_read(rd); driving=false;
for(int i=0;i<64;i++) keys[i]=0xFF; /* all released */
for(int i=0;i<14;i++) vx_pin_watch(a[i],VX_EDGE_BOTH,on_change,0);
vx_pin_watch(rd,VX_EDGE_BOTH,on_change,0);
update();
}

View File

@ -0,0 +1 @@
{ "schema": "velxio-chip/v1", "name": "Galaksija Keyboard", "author": "Velxio", "license": "MIT", "description": "Galaksija memory-mapped keyboard: drives reads of 0x2000-0x203F with key state (0xFE pressed / 0xFF released). Host pushes browser keys via set_key.", "pins": ["A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","D0","D1","D2","D3","D4","D5","D6","D7","RD"] }

View File

@ -0,0 +1,154 @@
/*
* galaksija-ram 64 KB SRAM, Galaksija variant.
*
* Same as ram-64k but it does NOT drive reads of 0x2000-0x203F: that range is
* the memory-mapped keyboard (owned by galaksija-keyboard). Writes still go to
* RAM everywhere. Phase 3 of project/multichip-bus/.
*
* Pin contract (idealised 64 KB byte-wide SRAM, see autosearch/09):
* A0..A15 input 16-bit address
* D0..D7 bidirectional 8-bit data (output on read, input on write)
* CE̅ input active-low chip enable
* OE̅ input active-low output enable
* WE̅ input active-low write enable (latch on rising edge)
* VCC, GND power
*
* Read mode: CE̅=0 AND OE̅=0 AND WE̅=1 drive D pins from mem[addr].
* Write mode: CE̅=0 AND WE̅ rising edge (with data already on D pins)
* latch mem[addr] := data.
* Standby: CE̅=1 D pins released.
*
* The 64 KB array is zero-initialised at chip_setup. Real SRAM powers
* up indeterminate; zero-init is a deliberate simplification that
* matches every common simulator (Wokwi, etc.) and is what
* ram-64k.test.js's blank-state assertion expects.
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#define RAM_SIZE 0x10000 /* 64 KB */
/* mem[] is malloc'd at chip_setup, NOT a static array, so the linker
doesn't include 64 KB of BSS in the chip's initial memory image.
The host (ChipRuntime.ts) provides 2 pages = 128 KB initial and
permits growth up to 16 pages = 1 MB, more than enough for 64 KB
on the heap plus stack. */
typedef struct {
vx_pin a[16];
vx_pin d[8];
vx_pin ce;
vx_pin oe;
vx_pin we;
vx_pin vcc;
vx_pin gnd;
uint8_t* mem;
bool driving;
int we_last;
} chip_t;
static chip_t G;
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);
/* Drive only on a true read: selected, output enabled, not writing. */
if (ce_low && oe_low && !we_low) {
uint16_t addr = read_addr();
/* The Galaksija memory-mapped keyboard owns reads of 0x2000-0x203F
* (internal 0x00-0x3F when A13 is the chip-select); yield the bus to
* the keyboard chip there so the two never both drive it. */
if (addr < 0x40) { release_data(); return; }
drive_data(G.mem[addr]);
} else {
release_data();
}
}
static void on_addr_or_ctrl(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin; (void)value;
update_outputs();
}
static void on_we(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin;
int ce_low = (vx_pin_read(G.ce) == 0);
/* Latch on rising edge of WE̅ when chip is selected.
(Pin watch was registered for EDGE_BOTH so we detect both
transitions; rising means we_last==0 and value==1.) */
if (G.we_last == 0 && value == 1 && ce_low) {
uint16_t addr = read_addr();
uint8_t data = read_data_bus();
G.mem[addr] = data;
}
G.we_last = value;
/* WE̅ change also affects whether we should be driving D in read
mode (during write, we must release). */
update_outputs();
}
void chip_setup(void) {
char name[4];
/* A0..A15 inputs */
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);
}
/* D0..D7 inputs (bidirectional; we switch to OUTPUT during reads) */
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); /* sample initial WE̅ level */
/* Watches: address and CE/OE affect outputs; WE is special because
its rising edge is the write-latch trigger. */
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();
}

View File

@ -0,0 +1 @@
{ "schema": "velxio-chip/v1", "name": "RAM 64K (Galaksija)", "author": "Velxio", "license": "MIT", "description": "64KB SRAM, Galaksija variant: yields reads of 0x2000-0x203F to the memory-mapped keyboard.", "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"] }

File diff suppressed because one or more lines are too long

View File

@ -720,6 +720,25 @@ export class ChipInstance {
return this._framebuffer !== null;
}
// ── Keyboard (chips that export set_key, e.g. galaksija-keyboard) ─────────
/** True if the chip exposes a host-driven keyboard via an exported
* `set_key(offset, down)`. The host (CustomChipPart) bridges browser key
* events into it. */
get hasKeyboard(): boolean {
return typeof this.exports?.set_key === 'function';
}
/** Push a key state into the chip's key table. `offset` is the chip-specific
* matrix offset; `down` is press/release. No-op if the chip has no keyboard. */
setKey(offset: number, down: boolean): void {
try {
this.exports?.set_key?.(offset, down ? 1 : 0);
} catch {
/* swallow chip errors */
}
}
// ── Timers ───────────────────────────────────────────────────────────────
private _timer_create(cbIdx: number, userData: number): number {

View File

@ -23,6 +23,20 @@ import { useElectricalStore } from '../../store/useElectricalStore';
import { clearChipDrives } from '../customChips/chipPinDrives';
import { requestElectricalResolve } from '../spice/electricalResolveHook';
// Physical-key (KeyboardEvent.code) -> Galaksija keyboard matrix offset, from
// the libretro Galaksija core's keyMap. The chip's set_key takes this offset;
// reading 0x2000+offset on the bus returns pressed/released.
const GALAKSIJA_KEY_OFFSET: Record<string, number> = {
KeyA: 1, KeyB: 2, KeyC: 3, KeyD: 4, KeyE: 5, KeyF: 6, KeyG: 7, KeyH: 8, KeyI: 9,
KeyJ: 10, KeyK: 11, KeyL: 12, KeyM: 13, KeyN: 14, KeyO: 15, KeyP: 16, KeyQ: 17,
KeyR: 18, KeyS: 19, KeyT: 20, KeyU: 21, KeyV: 22, KeyW: 23, KeyX: 24, KeyY: 25,
KeyZ: 26, ArrowUp: 27, ArrowDown: 28, ArrowLeft: 29, Backspace: 29,
ArrowRight: 30, Space: 31, Digit0: 32, Digit1: 33, Digit2: 34, Digit3: 35,
Digit4: 36, Digit5: 37, Digit6: 38, Digit7: 39, Digit8: 40, Digit9: 41,
Semicolon: 42, Quote: 43, Comma: 44, Equal: 45, Period: 46, Slash: 47,
Enter: 48, Tab: 49, Delete: 51, ShiftLeft: 53, ShiftRight: 53,
};
PartSimulationRegistry.register('custom-chip', {
attachEvents: (_element, simulator, getArduinoPin, componentId) => {
const sim = simulator as any;
@ -153,6 +167,7 @@ PartSimulationRegistry.register('custom-chip', {
let uartListener: ((byte: number) => void) | null = null;
let rafHandle = 0;
let disposed = false;
let keyboardCleanup: (() => void) | undefined;
(async () => {
try {
@ -194,6 +209,42 @@ PartSimulationRegistry.register('custom-chip', {
});
}
// Bridge the browser keyboard → a chip's memory-mapped keyboard (a chip
// exporting set_key, e.g. galaksija-keyboard). Maps physical keys
// (e.code) to the chip's matrix offsets. Ignores keystrokes while an
// editable element (the code editor, an input) is focused so typing code
// is never hijacked; the user types into the computer by clicking the
// canvas first. Held keys send one press (the chip's firmware handles
// auto-repeat).
if (inst.hasKeyboard && typeof window !== 'undefined') {
const editable = () => {
const a = document.activeElement as HTMLElement | null;
return (
!!a &&
(a.tagName === 'INPUT' ||
a.tagName === 'TEXTAREA' ||
a.isContentEditable ||
a.closest('.monaco-editor') != null)
);
};
const onDown = (e: KeyboardEvent) => {
if (e.repeat || editable()) return;
const o = GALAKSIJA_KEY_OFFSET[e.code];
if (o !== undefined) { instance?.setKey(o, true); e.preventDefault(); }
};
const onUp = (e: KeyboardEvent) => {
if (editable()) return;
const o = GALAKSIJA_KEY_OFFSET[e.code];
if (o !== undefined) instance?.setKey(o, false);
};
window.addEventListener('keydown', onDown);
window.addEventListener('keyup', onUp);
keyboardCleanup = () => {
window.removeEventListener('keydown', onDown);
window.removeEventListener('keyup', onUp);
};
}
// Drive the chip's timer-based execution every frame. Chips that
// register a periodic `vx_timer_create` (e.g. a CPU-emulator chip
// stepping its core, or a sensor publishing samples) need a
@ -240,6 +291,7 @@ PartSimulationRegistry.register('custom-chip', {
if (rafHandle) cancelAnimationFrame(rafHandle);
rafHandle = 0;
if (uartListener) bridges.uartListeners.delete(uartListener);
if (keyboardCleanup) keyboardCleanup();
if (instance) instance.dispose();
instance = null;
// Drop this chip's SPICE voltage sources so a stopped chip stops

View File

@ -0,0 +1,43 @@
/*
* galaksija-keyboard Galaksija memory-mapped keyboard (Phase 3,
* project/multichip-bus/). Replicates the scheme used by the libretro Galaksija
* core: the keyboard occupies addresses 0x2000-0x203F; reading 0x2000+offset
* returns 0xFE when the key at that matrix offset is pressed, 0xFF otherwise.
* The chip drives the data bus on a memory READ in that range and exposes
* set_key(offset, down) for the host to push browser key events. It never
* drives outside the keyboard range; the paired galaksija-ram releases reads in
* 0x2000-0x203F so the two never fight for the bus.
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
static vx_pin a[14], d[8], rd;
static int rd_last;
static bool driving;
static uint8_t keys[64];
static uint16_t read_addr(void){ uint16_t v=0; for(int i=0;i<14;i++) if(vx_pin_read(a[i])) v|=(1u<<i); return v; }
static int in_range(uint16_t addr){ return (addr & 0x3FC0) == 0x2000; } /* 0x2000-0x203F */
static void drive_data(uint8_t v){ for(int i=0;i<8;i++){ vx_pin_set_mode(d[i],VX_OUTPUT); vx_pin_write(d[i],(v>>i)&1);} driving=true; }
static void release_data(void){ if(!driving) return; for(int i=0;i<8;i++) vx_pin_set_mode(d[i],VX_INPUT); driving=false; }
static void update(void){
if(vx_pin_read(rd)==0){ uint16_t addr=read_addr(); if(in_range(addr)) drive_data(keys[addr & 0x3F]); else release_data(); }
else release_data();
}
static void on_change(void* u, vx_pin p, int v){ (void)u;(void)p;(void)v; update(); }
/* Exported: the host (browser keydown/keyup bridge) sets a key's state. */
void set_key(int offset, int down){ if(offset>=0 && offset<64) keys[offset] = down ? 0xFE : 0xFF; }
void chip_setup(void){
char name[4];
for(int i=0;i<14;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;} 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; d[i]=vx_pin_register(name,VX_INPUT);}
rd=vx_pin_register("RD",VX_INPUT); rd_last=vx_pin_read(rd); driving=false;
for(int i=0;i<64;i++) keys[i]=0xFF; /* all released */
for(int i=0;i<14;i++) vx_pin_watch(a[i],VX_EDGE_BOTH,on_change,0);
vx_pin_watch(rd,VX_EDGE_BOTH,on_change,0);
update();
}

View File

@ -0,0 +1 @@
{ "schema": "velxio-chip/v1", "name": "Galaksija Keyboard", "author": "Velxio", "license": "MIT", "description": "Galaksija memory-mapped keyboard: drives reads of 0x2000-0x203F with key state (0xFE pressed / 0xFF released). Host pushes browser keys via set_key.", "pins": ["A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","D0","D1","D2","D3","D4","D5","D6","D7","RD"] }

View File

@ -0,0 +1,154 @@
/*
* galaksija-ram 64 KB SRAM, Galaksija variant.
*
* Same as ram-64k but it does NOT drive reads of 0x2000-0x203F: that range is
* the memory-mapped keyboard (owned by galaksija-keyboard). Writes still go to
* RAM everywhere. Phase 3 of project/multichip-bus/.
*
* Pin contract (idealised 64 KB byte-wide SRAM, see autosearch/09):
* A0..A15 input 16-bit address
* D0..D7 bidirectional 8-bit data (output on read, input on write)
* CE̅ input active-low chip enable
* OE̅ input active-low output enable
* WE̅ input active-low write enable (latch on rising edge)
* VCC, GND power
*
* Read mode: CE̅=0 AND OE̅=0 AND WE̅=1 drive D pins from mem[addr].
* Write mode: CE̅=0 AND WE̅ rising edge (with data already on D pins)
* latch mem[addr] := data.
* Standby: CE̅=1 D pins released.
*
* The 64 KB array is zero-initialised at chip_setup. Real SRAM powers
* up indeterminate; zero-init is a deliberate simplification that
* matches every common simulator (Wokwi, etc.) and is what
* ram-64k.test.js's blank-state assertion expects.
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#define RAM_SIZE 0x10000 /* 64 KB */
/* mem[] is malloc'd at chip_setup, NOT a static array, so the linker
doesn't include 64 KB of BSS in the chip's initial memory image.
The host (ChipRuntime.ts) provides 2 pages = 128 KB initial and
permits growth up to 16 pages = 1 MB, more than enough for 64 KB
on the heap plus stack. */
typedef struct {
vx_pin a[16];
vx_pin d[8];
vx_pin ce;
vx_pin oe;
vx_pin we;
vx_pin vcc;
vx_pin gnd;
uint8_t* mem;
bool driving;
int we_last;
} chip_t;
static chip_t G;
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);
/* Drive only on a true read: selected, output enabled, not writing. */
if (ce_low && oe_low && !we_low) {
uint16_t addr = read_addr();
/* The Galaksija memory-mapped keyboard owns reads of 0x2000-0x203F
* (internal 0x00-0x3F when A13 is the chip-select); yield the bus to
* the keyboard chip there so the two never both drive it. */
if (addr < 0x40) { release_data(); return; }
drive_data(G.mem[addr]);
} else {
release_data();
}
}
static void on_addr_or_ctrl(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin; (void)value;
update_outputs();
}
static void on_we(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin;
int ce_low = (vx_pin_read(G.ce) == 0);
/* Latch on rising edge of WE̅ when chip is selected.
(Pin watch was registered for EDGE_BOTH so we detect both
transitions; rising means we_last==0 and value==1.) */
if (G.we_last == 0 && value == 1 && ce_low) {
uint16_t addr = read_addr();
uint8_t data = read_data_bus();
G.mem[addr] = data;
}
G.we_last = value;
/* WE̅ change also affects whether we should be driving D in read
mode (during write, we must release). */
update_outputs();
}
void chip_setup(void) {
char name[4];
/* A0..A15 inputs */
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);
}
/* D0..D7 inputs (bidirectional; we switch to OUTPUT during reads) */
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); /* sample initial WE̅ level */
/* Watches: address and CE/OE affect outputs; WE is special because
its rising edge is the write-latch trigger. */
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();
}

View File

@ -0,0 +1 @@
{ "schema": "velxio-chip/v1", "name": "RAM 64K (Galaksija)", "author": "Velxio", "license": "MIT", "description": "64KB SRAM, Galaksija variant: yields reads of 0x2000-0x203F to the memory-mapped keyboard.", "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"] }