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:
parent
a393e3e91d
commit
94627b99d2
|
|
@ -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);
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
Loading…
Reference in New Issue