feat(sim): custom chips read inputs (buttons/switches/sensors) with no board
The chip-output board-less path existed (chipPinDrives -> SPICE voltage sources -> LEDs). The INPUT direction was missing: a chip pin wired to a pushbutton had its net solved by ngspice, but nothing fed that net's state back to the PinManager key the chip reads via vx_pin_read. So a board-less chip could light LEDs but never read a button (verified: i8080 counter stayed at 0 on press). connectChipInputsToSolve subscribes to the electrical store and, after each solve, thresholds every wired chip input pin's net voltage to HIGH/LOW and triggerPinChange()s the chip's synthetic pin — updating getPinState (polling) and firing onPinChange edges. Pins the chip is actively driving are skipped so it never fights its own outputs. Hooked alongside connectAnalogInputsToMcu in start.ts. Solver-agnostic; reads only the electrical store shape. Also gives the board-less button examples a pull-down on each chip BTN pin so they read a clean LOW when open (a button-to-VCC floats HIGH otherwise): i8080-button-counter (2) and i8080-killbits (8). - new connectChipInputsToSolve.ts; start.ts wiring. - examples-retro-intel: pull-down resistors + wires for the button examples. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
694f038988
commit
dd62b94c1d
|
|
@ -314,7 +314,8 @@ export const retroIntelExamples: ExampleProject[] = [
|
|||
y: 110 + i * 50,
|
||||
properties: { color: i < 4 ? 'red' : 'orange' },
|
||||
})),
|
||||
// 2 buttons
|
||||
// 2 buttons, each with a pull-down so the chip pin reads a clean LOW
|
||||
// when the button is open (the button ties the pin to +5V when pressed).
|
||||
{
|
||||
type: 'wokwi-pushbutton',
|
||||
id: 'btn-inc',
|
||||
|
|
@ -329,6 +330,20 @@ export const retroIntelExamples: ExampleProject[] = [
|
|||
y: 470,
|
||||
properties: { color: 'red' },
|
||||
},
|
||||
{
|
||||
type: 'wokwi-resistor',
|
||||
id: 'rpd-inc',
|
||||
x: 300,
|
||||
y: 580,
|
||||
properties: { value: '10000' },
|
||||
},
|
||||
{
|
||||
type: 'wokwi-resistor',
|
||||
id: 'rpd-rst',
|
||||
x: 480,
|
||||
y: 580,
|
||||
properties: { value: '10000' },
|
||||
},
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
|
|
@ -387,6 +402,31 @@ export const retroIntelExamples: ExampleProject[] = [
|
|||
end: { componentId: 'psu', pinName: 'SIG' },
|
||||
color: '#e74c3c',
|
||||
},
|
||||
// Pull-downs: chip BTN pin -> 10k -> GND (clean LOW when not pressed)
|
||||
{
|
||||
id: 'pd-inc-sig',
|
||||
start: { componentId: 'rpd-inc', pinName: '1' },
|
||||
end: { componentId: 'i8080c', pinName: 'BTN_INC' },
|
||||
color: '#000000',
|
||||
},
|
||||
{
|
||||
id: 'pd-inc-gnd',
|
||||
start: { componentId: 'rpd-inc', pinName: '2' },
|
||||
end: { componentId: 'psu', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
{
|
||||
id: 'pd-rst-sig',
|
||||
start: { componentId: 'rpd-rst', pinName: '1' },
|
||||
end: { componentId: 'i8080c', pinName: 'BTN_RST' },
|
||||
color: '#000000',
|
||||
},
|
||||
{
|
||||
id: 'pd-rst-gnd',
|
||||
start: { componentId: 'rpd-rst', pinName: '2' },
|
||||
end: { componentId: 'psu', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -448,6 +488,14 @@ export const retroIntelExamples: ExampleProject[] = [
|
|||
y: 110 + i * 50,
|
||||
properties: { color: 'green' },
|
||||
})),
|
||||
// Pull-downs so each chip BTN pin reads a clean LOW when not pressed.
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
type: 'wokwi-resistor',
|
||||
id: `rpd-${i}`,
|
||||
x: 1240,
|
||||
y: 110 + i * 50,
|
||||
properties: { value: '10000' },
|
||||
})),
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
|
|
@ -492,6 +540,19 @@ export const retroIntelExamples: ExampleProject[] = [
|
|||
end: { componentId: 'psu', pinName: 'SIG' },
|
||||
color: '#e74c3c',
|
||||
})),
|
||||
// Pull-downs: each chip BTN pin -> 10k -> GND
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
id: `pd-${i}-sig`,
|
||||
start: { componentId: `rpd-${i}`, pinName: '1' },
|
||||
end: { componentId: 'i8080cpu', pinName: `BTN${i}` },
|
||||
color: '#000000',
|
||||
})),
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({
|
||||
id: `pd-${i}-gnd`,
|
||||
start: { componentId: `rpd-${i}`, pinName: '2' },
|
||||
end: { componentId: 'psu', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* connectChipInputsToSolve — feed solved net voltages back into custom-chip
|
||||
* INPUT pins, so a chip can read buttons / switches / sensors with no board.
|
||||
*
|
||||
* A custom chip already drives its OUTPUT pins into the netlist (chipPinDrives
|
||||
* -> SPICE voltage sources -> LEDs light). The INPUT direction was missing: a
|
||||
* chip pin wired to a pushbutton / switch / sensor had its net solved by
|
||||
* ngspice, but NOTHING wrote that net's state back to the PinManager key the
|
||||
* chip reads via `vx_pin_read`. So a chip could light LEDs board-less but never
|
||||
* read an input without an Arduino driving the pin.
|
||||
*
|
||||
* After every solve we look up each wired chip pin's net voltage, threshold it
|
||||
* to HIGH/LOW, and `triggerPinChange()` the chip's synthetic pin — which both
|
||||
* updates `getPinState` (polling reads) and fires the chip's `onPinChange` edge
|
||||
* handlers. Pins the chip is actively DRIVING are skipped so we never fight its
|
||||
* own outputs. This is the input counterpart of the chip-output SPICE path and
|
||||
* is solver-agnostic — it reads only the electrical store shape.
|
||||
*/
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||
import { syntheticChipPin } from '../customChips/syntheticPins';
|
||||
import { getChipDrivenPins } from '../customChips/chipPinDrives';
|
||||
|
||||
// 5V-logic thresholds with a hysteresis band so a node hovering near the
|
||||
// midpoint doesn't chatter HIGH/LOW every solve. Digital inputs (a button to
|
||||
// VCC with a pull-down) swing fully, so the band is rarely entered.
|
||||
const V_HIGH = 3.0;
|
||||
const V_LOW = 2.0;
|
||||
|
||||
/** Pin names declared by a chip.json (entries may be strings or {name,...}). */
|
||||
function chipPinNames(chipJsonStr: string): string[] {
|
||||
try {
|
||||
const obj = JSON.parse(chipJsonStr);
|
||||
if (Array.isArray(obj.pins)) {
|
||||
return obj.pins
|
||||
.map((p: unknown) =>
|
||||
typeof p === 'string' ? p : String((p as { name?: string } | null)?.name ?? ''),
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch {
|
||||
/* malformed chip.json — no readable pins */
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function connectChipInputsToSolve(): () => void {
|
||||
// Last logic level written per synthetic pin, so we only emit real edges
|
||||
// (and so the hysteresis band can hold the previous level).
|
||||
const lastState = new Map<number, boolean>();
|
||||
|
||||
function writeChipInputs() {
|
||||
const { nodeVoltages, pinNetMap } = useElectricalStore.getState();
|
||||
const sim = useSimulatorStore.getState();
|
||||
const pinManager = sim.pinManager;
|
||||
if (!pinManager) return;
|
||||
|
||||
for (const comp of sim.components) {
|
||||
if (comp.metadataId !== 'custom-chip') continue;
|
||||
const props = comp.properties as Record<string, unknown>;
|
||||
const names = chipPinNames(String(props.chipJson ?? '{}'));
|
||||
if (names.length === 0) continue;
|
||||
// Pins the chip is currently driving as outputs — never overwrite those.
|
||||
const driven = new Set(getChipDrivenPins(comp.id).map((d) => d.pin));
|
||||
|
||||
for (const pinName of names) {
|
||||
if (driven.has(pinName)) continue;
|
||||
const net = pinNetMap.get(`${comp.id}:${pinName}`);
|
||||
if (!net) continue;
|
||||
const v = nodeVoltages[net];
|
||||
if (v == null) continue;
|
||||
|
||||
const synth = syntheticChipPin(comp.id, pinName);
|
||||
const prev = lastState.get(synth);
|
||||
let next: boolean;
|
||||
if (v >= V_HIGH) next = true;
|
||||
else if (v <= V_LOW) next = false;
|
||||
else next = prev ?? false; // inside the hysteresis band — hold
|
||||
if (prev === next) continue;
|
||||
lastState.set(synth, next);
|
||||
pinManager.triggerPinChange(synth, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unsub = useElectricalStore.subscribe((state, prev) => {
|
||||
if (state.nodeVoltages !== prev.nodeVoltages) writeChipInputs();
|
||||
});
|
||||
// Initial pass for examples that pre-populate the store before mount.
|
||||
writeChipInputs();
|
||||
return () => unsub();
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
type ElectricalSnapshot,
|
||||
} from './CircuitSimulationService';
|
||||
import { connectAnalogInputsToMcu } from './connectAnalogInputsToMcu';
|
||||
import { connectChipInputsToSolve } from './connectChipInputsToSolve';
|
||||
import { connectMcuEdgesToService } from './connectMcuEdgesToService';
|
||||
import { setElectricalResolveHook } from './electricalResolveHook';
|
||||
import { collectPinStates } from './collectPinStates';
|
||||
|
|
@ -81,6 +82,7 @@ export function startSimulation(): () => void {
|
|||
|
||||
const unsubService = service.start();
|
||||
const unsubAdc = connectAnalogInputsToMcu();
|
||||
const unsubChipIn = connectChipInputsToSolve();
|
||||
const unsubEdges = connectMcuEdgesToService(service);
|
||||
|
||||
// Let custom chips request a re-solve when they toggle an output pin, so
|
||||
|
|
@ -136,6 +138,7 @@ export function startSimulation(): () => void {
|
|||
setElectricalResolveHook(null);
|
||||
unsubService();
|
||||
unsubAdc();
|
||||
unsubChipIn();
|
||||
unsubEdges();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue