feat(custom-chip): run custom chips with no board (general-purpose sim)
Velxio can now simulate one or more custom-chip CPUs with NO Arduino/ESP32 board on the canvas — a general-purpose electronics simulator, not an MCU-only one. - DynamicComponent: board-less parts get the real shared flat PinManager (instead of a no-op stub) so a custom chip's digital pin writes/reads reach the LEDs/inputs wired to it. - CustomChipPart: the rAF tick respects board-less Run/Stop (freezes while the electrical sim is paused); board behaviour is unchanged. - EditorToolbar.handleRun: board-less Run compiles each chip's WASM/ROM and re-attaches the parts (restartParts) so they pick up the fresh WASM, then resumes the solver. - useSimulatorStore.restartParts(): bump hexEpoch to force part re-attach. - New example "Z80 Larson Scanner (no board)": a programmable Z80 + 8 LEDs + the adjustable power-supply component, no MCU. The chip drives the LEDs through the synthetic-pin + ngspice path added earlier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a57951d854
commit
d5b9a9ceb5
|
|
@ -427,10 +427,17 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
({
|
||||
setPinState: () => {},
|
||||
isRunning: () => false,
|
||||
pinManager: {
|
||||
onPinChange: () => () => {},
|
||||
triggerPinChange: () => {},
|
||||
} as any,
|
||||
// Board-less circuits have no MCU simulator, but a custom chip still
|
||||
// needs a real PinManager so its digital pin writes/reads reach the
|
||||
// components wired to it (LEDs, buttons, other chips). Hand it the
|
||||
// shared flat PinManager that SimulatorCanvas subscribes LEDs to, so
|
||||
// both sides talk on the same numeric/synthetic pin ids. Falls back
|
||||
// to a no-op only if even that isn't ready yet.
|
||||
pinManager:
|
||||
(useSimulatorStore.getState().pinManager as any) ?? {
|
||||
onPinChange: () => () => {},
|
||||
triggerPinChange: () => {},
|
||||
},
|
||||
} as any);
|
||||
// Helper to find Arduino pin connected to a component pin.
|
||||
// Traces through electrically-transparent passive components so that a
|
||||
|
|
|
|||
|
|
@ -610,10 +610,32 @@ export const EditorToolbar = ({
|
|||
if (!ok) return;
|
||||
}
|
||||
|
||||
// Board-less circuits (SPICE-only digital / analog gallery) have no MCU
|
||||
// to start. Resuming the electrical solver replays any switch toggles
|
||||
// captured while paused so the canvas catches up instantly.
|
||||
// Board-less circuits have no MCU to start. If there are custom-chip CPUs
|
||||
// on the canvas, compile them (WASM + ROM) and re-attach so they pick up
|
||||
// the fresh WASM — Velxio runs custom chips with no Arduino/ESP32 board,
|
||||
// as a general-purpose electronics simulator. Then resume the electrical
|
||||
// solver (replays any switch toggles captured while paused).
|
||||
if (isBoardless) {
|
||||
const customChips = useSimulatorStore
|
||||
.getState()
|
||||
.components.filter((c) => c.metadataId === 'custom-chip');
|
||||
if (customChips.length > 0) {
|
||||
setCompiling(true);
|
||||
setConsoleOpen(true);
|
||||
setCompileLogs([]);
|
||||
try {
|
||||
await prepareCustomChips(customChips, files);
|
||||
} catch (e) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
setCompiling(false);
|
||||
// Force the chip parts to re-attach with their freshly compiled WASM.
|
||||
useSimulatorStore.getState().restartParts();
|
||||
}
|
||||
setElectricalPaused(false);
|
||||
setMessage(null);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -621,4 +621,94 @@ export const retroIntelExamples: ExampleProject[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Z80 Larson Scanner — NO board (general-purpose electronics) ──
|
||||
// A programmable Z80 chip + 8 LEDs + a regulated power supply, with NO
|
||||
// Arduino/ESP32 on the canvas. Demonstrates that Velxio runs custom chips
|
||||
// standalone. Board-less (boardFilter: 'digital'); the ROM is pre-baked so
|
||||
// only the chip WASM compiles on Run.
|
||||
{
|
||||
id: 'z80-larson-no-board',
|
||||
title: 'Z80 Larson Scanner (no board)',
|
||||
description:
|
||||
'The programmable Z80 chip drives 8 LEDs with NO Arduino — powered by a ' +
|
||||
'regulated bench supply. Velxio runs custom chips as a general-purpose ' +
|
||||
'electronics simulator. Click Run: a single LED walks back and forth.',
|
||||
category: 'circuits',
|
||||
difficulty: 'intermediate',
|
||||
boardFilter: 'digital',
|
||||
tags: ['retro', 'z80', 'zilog', 'cpu', 'leds', 'larson', 'no-board', 'power-supply', 'custom-chip', 'spice', 'programmable'],
|
||||
code: `// Z80 Larson Scanner — NO Arduino, just the chip + a power supply.\n//\n// Velxio runs custom chips as a general-purpose electronics simulator: the\n// programmable Z80 on the canvas executes its ROM and drives the 8 LEDs\n// directly, powered by the regulated supply (no MCU needed). Click Run.\n\nvoid setup() {}\nvoid loop() {}\n`,
|
||||
components: [
|
||||
{
|
||||
type: 'power-supply',
|
||||
id: 'psu',
|
||||
x: 180,
|
||||
y: 200,
|
||||
properties: { mode: 'dc', voltage: 5, currentLimit: 2, frequency: 50 },
|
||||
},
|
||||
{
|
||||
type: 'custom-chip',
|
||||
id: 'z80cpu',
|
||||
x: 440,
|
||||
y: 150,
|
||||
properties: {
|
||||
chipName: 'Z80 CPU (programmable)',
|
||||
sourceC: z80CpuC,
|
||||
chipJson: z80CpuJ,
|
||||
wasmBase64: '',
|
||||
// Pre-baked larson.s ROM (LD SP,0xBFFF; rotate a bit across 0xC000).
|
||||
romBytes: 'Mf+/PgEyAMD1zRAA8QcY9Q5QBgAQ/g0g+ck=',
|
||||
programFile: '',
|
||||
programTarget: 'z80',
|
||||
},
|
||||
},
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
type: 'wokwi-resistor',
|
||||
id: `r-${i}`,
|
||||
x: 760,
|
||||
y: 110 + i * 50,
|
||||
properties: { value: '220' },
|
||||
})),
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
type: 'wokwi-led',
|
||||
id: `led-${i}`,
|
||||
x: 900,
|
||||
y: 110 + i * 50,
|
||||
properties: { color: i % 2 === 0 ? 'red' : 'orange' },
|
||||
})),
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
id: 'psu-vcc',
|
||||
start: { componentId: 'psu', pinName: 'SIG' },
|
||||
end: { componentId: 'z80cpu', pinName: 'VCC' },
|
||||
color: '#e74c3c',
|
||||
},
|
||||
{
|
||||
id: 'psu-gnd',
|
||||
start: { componentId: 'psu', pinName: 'GND' },
|
||||
end: { componentId: 'z80cpu', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
id: `w-led-${i}`,
|
||||
start: { componentId: 'z80cpu', pinName: `LED${i}` },
|
||||
end: { componentId: `r-${i}`, pinName: '1' },
|
||||
color: '#facc15',
|
||||
})),
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
id: `w-r-${i}`,
|
||||
start: { componentId: `r-${i}`, pinName: '2' },
|
||||
end: { componentId: `led-${i}`, pinName: 'A' },
|
||||
color: '#facc15',
|
||||
})),
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
id: `w-gnd-${i}`,
|
||||
start: { componentId: `led-${i}`, pinName: 'C' },
|
||||
end: { componentId: 'psu', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
})),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
detectSimulatorKind,
|
||||
} from '../customChips';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||
import { clearChipDrives } from '../customChips/chipPinDrives';
|
||||
import { requestElectricalResolve } from '../spice/electricalResolveHook';
|
||||
|
||||
|
|
@ -202,10 +203,18 @@ PartSimulationRegistry.register('custom-chip', {
|
|||
// exposes the same epoch via vx_sim_now_nanos.
|
||||
const tick = () => {
|
||||
if (disposed || !instance) return;
|
||||
try {
|
||||
instance.tickTimers(BigInt(Math.floor(performance.now() * 1_000_000)));
|
||||
} catch (e) {
|
||||
console.error(`[custom-chip:${componentId}] tickTimers threw:`, e);
|
||||
// When there is no board, the chip is driven by the editor's Run /
|
||||
// Stop, which toggle the electrical "paused" flag — freeze the chip
|
||||
// while paused (but keep the rAF alive so Run resumes instantly).
|
||||
// With a board present, behaviour is unchanged: tick whenever attached.
|
||||
const boardless = useSimulatorStore.getState().boards.length === 0;
|
||||
const runnable = !boardless || !useElectricalStore.getState().paused;
|
||||
if (runnable) {
|
||||
try {
|
||||
instance.tickTimers(BigInt(Math.floor(performance.now() * 1_000_000)));
|
||||
} catch (e) {
|
||||
console.error(`[custom-chip:${componentId}] tickTimers threw:`, e);
|
||||
}
|
||||
}
|
||||
rafHandle = requestAnimationFrame(tick);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -827,6 +827,9 @@ interface SimulatorState {
|
|||
startSimulation: () => void;
|
||||
stopSimulation: () => void;
|
||||
resetSimulation: () => void;
|
||||
/** Bump hexEpoch to force every component part to re-attach (e.g. so a
|
||||
* board-less custom chip picks up freshly compiled WASM). */
|
||||
restartParts: () => void;
|
||||
setCompiledHex: (hex: string) => void;
|
||||
setCompiledBinary: (base64: string) => void;
|
||||
setRunning: (running: boolean) => void;
|
||||
|
|
@ -2139,6 +2142,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
get().startBoard(boardId);
|
||||
},
|
||||
|
||||
restartParts: () => set((s) => ({ hexEpoch: s.hexEpoch + 1 })),
|
||||
|
||||
stopSimulation: () => {
|
||||
const { activeBoardId } = get();
|
||||
const boardId = activeBoardId ?? INITIAL_BOARD_ID;
|
||||
|
|
|
|||
Loading…
Reference in New Issue