diff --git a/frontend/src/simulation/AVRSimulator.ts b/frontend/src/simulation/AVRSimulator.ts index 97c22084..d08c39c4 100644 --- a/frontend/src/simulation/AVRSimulator.ts +++ b/frontend/src/simulation/AVRSimulator.ts @@ -295,6 +295,15 @@ const MEGA_PORT_CONFIGS = [ ]; export class AVRSimulator { + // Digital input pins are driven from the SPICE solve + // (connectDigitalInputsToMcu) for nets backed by a real source/element, so + // `digitalRead()` reflects the real wiring (a pin wired to 5V reads HIGH, a + // button to 5V reads HIGH when pressed) instead of a hardcoded part seed. + // The connector skips floating nets, so event-driven parts with no SPICE + // model (rotary encoder, keypad, dialer, dip-switch, stepper) keep driving + // their pins via the part layer. Input-control parts (button / slide-switch) + // check this flag and skip their direct seed — see BasicParts.spiceDriven(). + readonly spiceDrivenInputs = true; private cpu: CPU | null = null; /** Peripherals kept alive by reference so GC doesn't collect their CPU hooks */ private peripherals: unknown[] = []; diff --git a/frontend/src/simulation/spice/CircuitSimulationService.ts b/frontend/src/simulation/spice/CircuitSimulationService.ts index 1fb3e8d7..3988e51b 100644 --- a/frontend/src/simulation/spice/CircuitSimulationService.ts +++ b/frontend/src/simulation/spice/CircuitSimulationService.ts @@ -67,6 +67,9 @@ export interface ElectricalSnapshot { timeWaveforms?: TimeWaveforms; /** Convergence warnings from the solver. */ warnings: string[]; + /** Nets backed by a real source/element (see NetlistBuilder). Gates which + * MCU input pins connectDigitalInputsToMcu may drive from the solve. */ + sourcedNets: Set; } /** What the service needs from the scheduler. */ @@ -130,6 +133,7 @@ export class CircuitSimulationService { nets: string[]; voltageSources: string[]; analysisKind: 'op' | 'tran' | 'ac'; + sourcedNets: Set; } | null = null; /** Set by `stop()`. Once true, `tick()` and `handleMcuEdge()` @@ -302,7 +306,7 @@ export class CircuitSimulationService { })), }; const input = buildInputFromStore(snap as Parameters[0]); - const { netlist, pinNetMap, nets, voltageSources } = buildNetlist(input); + const { netlist, pinNetMap, nets, voltageSources, sourcedNets } = buildNetlist(input); // Tell the scheduler exactly which vectors we want — every net // voltage + every branch current. @@ -326,6 +330,7 @@ export class CircuitSimulationService { nets, voltageSources, analysisKind: input.analysis.kind, + sourcedNets, }; this.publishFromLastResult(); } @@ -380,6 +385,7 @@ export class CircuitSimulationService { analysisMode: ctx.analysisKind, timeWaveforms, warnings: result.warnings, + sourcedNets: ctx.sourcedNets, }); } diff --git a/frontend/src/simulation/spice/NetlistBuilder.ts b/frontend/src/simulation/spice/NetlistBuilder.ts index be20a913..3e632c0b 100644 --- a/frontend/src/simulation/spice/NetlistBuilder.ts +++ b/frontend/src/simulation/spice/NetlistBuilder.ts @@ -67,6 +67,18 @@ export interface BuildNetlistResult { * request branch currents (`i(v_)`). */ voltageSources: string[]; + /** + * Nets that are backed by a real electrical source/element — a power rail + * ('0' / 'vcc_rail'), a board GPIO V-source, an internal pull resistor, or + * any net a componentToSpice mapper emitted a card for (resistor, button + * switch, divider, etc.). Excludes purely-floating nets (which only get the + * step-7 auto-pull-down). `connectDigitalInputsToMcu` drives an MCU input pin + * from the solve ONLY when its net is in here, so event-driven parts with no + * SPICE model (rotary encoder, keypad, dialer, dip-switch, stepper) keep + * driving their own pins via the part layer instead of being forced LOW by a + * floating-net read. + */ + sourcedNets: Set; } export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { @@ -138,6 +150,11 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { const cards: string[] = []; const modelLines = new Set(); const dominantVcc = boards[0]?.vcc ?? 5; + // Nets backed by a real source/element (see BuildNetlistResult.sourcedNets). + // Rails are always sourced; component + GPIO-source + pull nets are added + // below. Deliberately NOT populated from the step-7 auto-pull-down cards, + // since those mark FLOATING nets. + const sourcedNets = new Set(['0', 'vcc_rail']); for (const comp of components) { const localLookup = (pinName: string) => netLookup(comp.id, pinName); @@ -145,6 +162,11 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { if (!emission) continue; cards.push(...emission.cards); for (const m of emission.modelsUsed) modelLines.add(m); + // Every net this component connects to now has a real SPICE element on it. + for (const pinName of pinsReferencedByWires(comp.id, wires)) { + const n = netLookup(comp.id, pinName); + if (n) sourcedNets.add(n); + } } // ── 5. Board GPIO sources ───────────────────────────────────────────────── @@ -174,6 +196,7 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { cards.push( `R_pull_${sanitizeSpiceId(board.id)}_${sanitizeSpiceId(pinName)} ${net} ${rail} 45000`, ); + sourcedNets.add(net); // weak pull to a rail → determinate idle level } continue; } @@ -181,6 +204,7 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { if (net === '0' || net === 'vcc_rail') continue; // already served const v = state.type === 'digital' ? state.v : state.duty * board.vcc; cards.push(`V_${sanitizeSpiceId(board.id)}_${sanitizeSpiceId(pinName)} ${net} 0 DC ${v}`); + sourcedNets.add(net); // board GPIO V-source drives this net (e.g. cross-board input) } } @@ -203,6 +227,8 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { const b = netLookup(w.end.componentId, w.end.pinName); if (!a || !b) continue; cards.push(`R_wire_${w.id} ${a} ${b} ${ohms}`); + sourcedNets.add(a); + sourcedNets.add(b); } // ── 7. Auto pull-downs for floating nets ───────────────────────────────── @@ -279,6 +305,7 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { pinNetMap, nets, voltageSources, + sourcedNets, }; } diff --git a/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts b/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts index fb2effee..cd8f5f84 100644 --- a/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts +++ b/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts @@ -43,7 +43,7 @@ export function connectDigitalInputsToMcu(): () => void { const lastLevel = new Map(); function injectDigitalInputs() { - const { nodeVoltages, pinNetMap } = useElectricalStore.getState(); + const { nodeVoltages, pinNetMap, sourcedNets } = useElectricalStore.getState(); const { boards } = useSimulatorStore.getState(); for (const board of boards) { const sim = getBoardSimulator(board.id) as @@ -58,6 +58,14 @@ export function connectDigitalInputsToMcu(): () => void { const gpio = gpioFromPinName(key.slice(prefix.length)); if (gpio < 0) continue; if (driven.has(gpio)) continue; // the MCU drives this pin (digitalWrite) + // Only drive pins whose net is backed by a real source/element (rail, + // pull, button switch, divider, cross-board output, …). A net that is + // only floating (an event-driven part like a rotary encoder / keypad + // that has no SPICE model) is left to the part layer, which seeds the + // pin directly — otherwise its ~0 V floating read would force it LOW + // and fight the part. This is what makes it safe to enable + // spiceDrivenInputs on the AVR (which has many such part-driven pins). + if (!sourcedNets.has(net)) continue; const v = nodeVoltages[net]; if (v == null) continue; const stateKey = `${board.id}:${gpio}`; diff --git a/frontend/src/simulation/spice/start.ts b/frontend/src/simulation/spice/start.ts index 0979c4ad..f583bc55 100644 --- a/frontend/src/simulation/spice/start.ts +++ b/frontend/src/simulation/spice/start.ts @@ -50,6 +50,7 @@ function createElectricalStorePort(): ElectricalStorePort { error: snapshot.warnings[0] ?? null, lastSolveMs: 0, submittedNetlist: '', + sourcedNets: snapshot.sourcedNets, }); }, }; diff --git a/frontend/src/store/useElectricalStore.ts b/frontend/src/store/useElectricalStore.ts index b8e47f6b..d2a7a46b 100644 --- a/frontend/src/store/useElectricalStore.ts +++ b/frontend/src/store/useElectricalStore.ts @@ -25,6 +25,10 @@ export interface ElectricalSnapshot { error: string | null; lastSolveMs: number; submittedNetlist: string; + /** Nets backed by a real source/element (rail, GPIO V-source, pull, or any + * component card). connectDigitalInputsToMcu only drives MCU input pins + * whose net is here, so floating event-part pins aren't forced LOW. */ + sourcedNets: Set; } interface ElectricalState extends ElectricalSnapshot { @@ -51,6 +55,7 @@ const EMPTY: ElectricalSnapshot = { error: null, lastSolveMs: 0, submittedNetlist: '', + sourcedNets: new Set(), }; export const useElectricalStore = create((set) => ({