fix(avr): drive digital inputs from the real circuit (SPICE), not a fake seed
An Arduino input wired to a power rail read the wrong level: a pin tied to 5V read LOW, and a button-to-5V read idle-HIGH / pressed-LOW. AVR inputs were never fed the solved circuit voltage (only the ESP32 had spiceDrivenInputs), so a bare-rail input had no driver and buttons fell back to a hardcoded active-low pull-up seed that ignored the wiring. Enable spiceDrivenInputs on AVRSimulator, and gate connectDigitalInputsToMcu on a new NetlistBuilder sourcedNets set (rails, GPIO V-sources, pulls, and any net a component card touches). Only source-backed input pins are driven from the solve; floating nets are left to the part layer, so event-driven parts with no SPICE model (rotary encoder, keypad, dialer, dip-switch, stepper) keep driving their own pins instead of being forced LOW.
This commit is contained in:
parent
d0a51e6239
commit
e81450e348
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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<string>;
|
||||
}
|
||||
|
||||
/** What the service needs from the scheduler. */
|
||||
|
|
@ -130,6 +133,7 @@ export class CircuitSimulationService {
|
|||
nets: string[];
|
||||
voltageSources: string[];
|
||||
analysisKind: 'op' | 'tran' | 'ac';
|
||||
sourcedNets: Set<string>;
|
||||
} | null = null;
|
||||
|
||||
/** Set by `stop()`. Once true, `tick()` and `handleMcuEdge()`
|
||||
|
|
@ -302,7 +306,7 @@ export class CircuitSimulationService {
|
|||
})),
|
||||
};
|
||||
const input = buildInputFromStore(snap as Parameters<typeof buildInputFromStore>[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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,18 @@ export interface BuildNetlistResult {
|
|||
* request branch currents (`i(v_<name>)`).
|
||||
*/
|
||||
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<string>;
|
||||
}
|
||||
|
||||
export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult {
|
||||
|
|
@ -138,6 +150,11 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult {
|
|||
const cards: string[] = [];
|
||||
const modelLines = new Set<string>();
|
||||
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<string>(['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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export function connectDigitalInputsToMcu(): () => void {
|
|||
const lastLevel = new Map<string, boolean>();
|
||||
|
||||
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}`;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ function createElectricalStorePort(): ElectricalStorePort {
|
|||
error: snapshot.warnings[0] ?? null,
|
||||
lastSolveMs: 0,
|
||||
submittedNetlist: '',
|
||||
sourcedNets: snapshot.sourcedNets,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string>;
|
||||
}
|
||||
|
||||
interface ElectricalState extends ElectricalSnapshot {
|
||||
|
|
@ -51,6 +55,7 @@ const EMPTY: ElectricalSnapshot = {
|
|||
error: null,
|
||||
lastSolveMs: 0,
|
||||
submittedNetlist: '',
|
||||
sourcedNets: new Set(),
|
||||
};
|
||||
|
||||
export const useElectricalStore = create<ElectricalState>((set) => ({
|
||||
|
|
|
|||
Loading…
Reference in New Issue