feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
/**
|
|
|
|
|
* Frontend wrapper for POST /api/compile-rom — compiles a chip-program file
|
|
|
|
|
* (8080 ASM, Intel HEX, raw .bin) into base64 ROM bytes that get stored on
|
|
|
|
|
* a custom-chip component's `romBytes` property. The chip's emulator then
|
|
|
|
|
* reads those bytes at chip_setup via vx_rom_size / vx_rom_read.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export type RomTarget = '8080' | 'z80' | '8086' | '4004';
|
2026-05-19 10:31:10 +07:00
|
|
|
export type RomFormat = 'asm' | 'hex' | 'bin' | 'c';
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
|
|
|
|
|
export interface RomCompileResult {
|
|
|
|
|
success: boolean;
|
|
|
|
|
rom_base64: string | null;
|
|
|
|
|
byte_size: number;
|
|
|
|
|
stderr: string;
|
|
|
|
|
error: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BASE = '/api/compile-rom';
|
|
|
|
|
|
|
|
|
|
export async function compileRom(
|
|
|
|
|
source: string,
|
|
|
|
|
target: RomTarget,
|
|
|
|
|
format: RomFormat,
|
|
|
|
|
): Promise<RomCompileResult> {
|
|
|
|
|
const res = await fetch(`${BASE}/`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ source, target, format }),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
rom_base64: null,
|
|
|
|
|
byte_size: 0,
|
|
|
|
|
stderr: '',
|
|
|
|
|
error: `HTTP ${res.status}: ${text}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return (await res.json()) as RomCompileResult;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 10:31:10 +07:00
|
|
|
/** Classify a filename as a chip-program file (vs an Arduino sketch).
|
|
|
|
|
*
|
|
|
|
|
* `.c` is intentionally NOT in the always-list — Arduino sketches use .c
|
|
|
|
|
* too. The toolbar disambiguates by checking whether a custom-chip on
|
|
|
|
|
* the canvas has `programFile === activeFile.name`. If yes, .c is a chip
|
|
|
|
|
* program (SDCC route); if no, it's an Arduino sketch (arduino-cli route).
|
|
|
|
|
*/
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
export function isChipProgramFile(name: string): boolean {
|
|
|
|
|
const lower = name.toLowerCase();
|
|
|
|
|
return (
|
|
|
|
|
lower.endsWith('.s') ||
|
|
|
|
|
lower.endsWith('.asm') ||
|
|
|
|
|
lower.endsWith('.hex') ||
|
|
|
|
|
lower.endsWith('.bin')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Pick a sensible compile format from the filename extension. */
|
|
|
|
|
export function formatForFile(name: string): RomFormat {
|
|
|
|
|
const lower = name.toLowerCase();
|
|
|
|
|
if (lower.endsWith('.hex')) return 'hex';
|
|
|
|
|
if (lower.endsWith('.bin')) return 'bin';
|
2026-05-19 10:31:10 +07:00
|
|
|
if (lower.endsWith('.c') || lower.endsWith('.cpp')) return 'c';
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
return 'asm';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Pick the right target CPU from the chip's chip.json programTargets,
|
|
|
|
|
* falling back to 8080 (the only one wired up today). */
|
|
|
|
|
export function targetForChip(chipJsonStr: string): RomTarget {
|
|
|
|
|
try {
|
|
|
|
|
const obj = JSON.parse(chipJsonStr);
|
|
|
|
|
if (Array.isArray(obj.programTargets) && obj.programTargets.length > 0) {
|
|
|
|
|
const t = String(obj.programTargets[0]).toLowerCase();
|
|
|
|
|
if (t === '8080' || t === 'z80' || t === '8086' || t === '4004') return t;
|
|
|
|
|
}
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
return '8080';
|
|
|
|
|
}
|
2026-06-04 03:52:11 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A custom chip is "programmable" — it runs a user program / ROM, like a CPU
|
|
|
|
|
* emulator — when its chip.json declares `programTargets`, or it already
|
|
|
|
|
* references a program file. Behaviour / driver chips (a servo driver, a
|
|
|
|
|
* sensor) declare no programTargets and are edited only in the chip designer.
|
|
|
|
|
*
|
|
|
|
|
* This (not `programFile`) is the canonical predicate: a chip dropped fresh
|
|
|
|
|
* from the gallery has an empty programFile until we seed one, but its
|
|
|
|
|
* chip.json already says it's a CPU.
|
|
|
|
|
*/
|
|
|
|
|
export function isProgrammableChip(
|
|
|
|
|
props: Record<string, unknown> | null | undefined,
|
|
|
|
|
): boolean {
|
|
|
|
|
if (!props) return false;
|
|
|
|
|
if (String(props.programFile ?? '').trim()) return true;
|
|
|
|
|
try {
|
|
|
|
|
const obj = JSON.parse(String(props.chipJson ?? '{}'));
|
|
|
|
|
return Array.isArray(obj.programTargets) && obj.programTargets.length > 0;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Default editable program file name for a freshly-added programmable chip.
|
|
|
|
|
* We seed C — SDCC compiles it to the chip's CPU (z80 / 8080 / ...). */
|
|
|
|
|
export const DEFAULT_CHIP_PROGRAM_FILE = 'program.c';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Starter C program seeded into a newly-added programmable chip's editor
|
|
|
|
|
* group, so the chip has an editable program from the moment it lands on the
|
|
|
|
|
* canvas. Walks a single LED across the 8 memory-mapped outputs — it compiles
|
|
|
|
|
* and does something visible on Run. Mirrors the working chaser.c idiom
|
|
|
|
|
* (volatile MMIO pointer + nop-based delay; SDCC treats plain `char` as
|
|
|
|
|
* unsigned on these CPUs, so the pattern uses an explicit unsigned byte).
|
|
|
|
|
*/
|
|
|
|
|
export const DEFAULT_CHIP_PROGRAM_C = `/* Program for the programmable CPU chip — compiled by SDCC and loaded as the
|
|
|
|
|
* chip's ROM. Memory-mapped I/O matches the z80-cpu / i8080-cpu map:
|
|
|
|
|
*
|
|
|
|
|
* 0xC000 LED_OUT write: bit i drives output pin LEDi
|
|
|
|
|
* 0xC003 BTN_IN read: bit i reads input pin BTNi
|
|
|
|
|
*
|
|
|
|
|
* Edit this and click Run. (Rename to .s to write assembly instead.)
|
|
|
|
|
*/
|
|
|
|
|
#define LED_OUT (*(volatile unsigned char *)0xC000)
|
|
|
|
|
#define BTN_IN (*(volatile unsigned char *)0xC003)
|
|
|
|
|
|
|
|
|
|
static void delay(unsigned int loops) {
|
|
|
|
|
while (loops--) {
|
|
|
|
|
__asm
|
|
|
|
|
nop
|
|
|
|
|
__endasm;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main(void) {
|
|
|
|
|
unsigned char bit = 0x01;
|
|
|
|
|
while (1) {
|
|
|
|
|
LED_OUT = bit; /* light one LED */
|
|
|
|
|
delay(5000);
|
|
|
|
|
bit <<= 1; /* walk it left */
|
|
|
|
|
if (bit == 0) bit = 0x01; /* wrap around */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`;
|