diff --git a/frontend/src/data/examples-retro-intel.ts b/frontend/src/data/examples-retro-intel.ts index 0bbf2e84..16f1d358 100644 --- a/frontend/src/data/examples-retro-intel.ts +++ b/frontend/src/data/examples-retro-intel.ts @@ -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', + })), ], }, diff --git a/frontend/src/simulation/spice/connectChipInputsToSolve.ts b/frontend/src/simulation/spice/connectChipInputsToSolve.ts new file mode 100644 index 00000000..cbb1a9f4 --- /dev/null +++ b/frontend/src/simulation/spice/connectChipInputsToSolve.ts @@ -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(); + + 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; + 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(); +} diff --git a/frontend/src/simulation/spice/start.ts b/frontend/src/simulation/spice/start.ts index 57bac328..0db17391 100644 --- a/frontend/src/simulation/spice/start.ts +++ b/frontend/src/simulation/spice/start.ts @@ -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(); }; }