spice: seed INPUT_PULLUP pins to their resting level on pull enable
Deterministic repro (emulation-gaps F4): after pinMode(2, INPUT_PULLUP) with a correctly-wired button-to-GND, all 8 setup()-time digitalReads and the first two loop() passes returned 0 — the input register only got its level from the first SPICE solve (~400 ms). That window is what fired phantom emergency-stop latches and auto-started state machines in audited sessions (and an unwired INPUT_PULLUP pin on the qemu bridges never got driven at all: no net in pinNetMap, connector skips it, guest read 0 forever). Fix: PinManager.onPullChange fires on pull transitions; AVRSimulator seeds its port input to the resting level instantly (real silicon does this in ns), and makePinPullHandler does the same for the qemu-bridge boards via the shim's setPinState. Sourced nets are still overridden by the very next solve, so the 'mis-wired button still works' regression guarded by the June work cannot return.
This commit is contained in:
parent
fc048ff941
commit
eb3b04efe3
|
|
@ -373,6 +373,19 @@ export class AVRSimulator {
|
|||
// register devices BEFORE the firmware loads. The real AVRTWI
|
||||
// takes over via `i2cBus.attachMaster(twi)` inside loadHex.
|
||||
this.i2cBus = new I2CBusManager(nullI2CMaster());
|
||||
// Seed the input register to the pull's resting level the instant the
|
||||
// firmware enables it (INPUT_PULLUP -> HIGH). Real silicon does this in
|
||||
// nanoseconds; without the seed, digitalRead returned 0 from pinMode
|
||||
// until the first SPICE solve (~400 ms) — long enough for setup()-time
|
||||
// button checks and emergency-stop latches to fire spuriously (2026-07
|
||||
// audit, reproduced deterministically). The SPICE connector overrides
|
||||
// with the solved level on sourced nets right after, so a button held
|
||||
// at boot still reads pressed within one solve.
|
||||
pinManager.onPullChange = (pin, pull) => {
|
||||
if (pull === 0) return;
|
||||
if (pinManager.getOutputPins().has(pin)) return;
|
||||
this.setPinState(pin, pull === 1);
|
||||
};
|
||||
}
|
||||
|
||||
private get pwmPins() {
|
||||
|
|
|
|||
|
|
@ -185,9 +185,19 @@ export class PinManager {
|
|||
* IO_MUX / pad config): 0 = none, 1 = pull-up, 2 = pull-down. The SPICE
|
||||
* collector reads this back via `getPinPull` to stamp a weak resistor.
|
||||
*/
|
||||
/** Fired on pull-state TRANSITIONS (not repeats). Simulators use it to
|
||||
* seed the pin input to the pull's resting level the moment the firmware
|
||||
* enables it — closing the boot window where INPUT_PULLUP read LOW until
|
||||
* the first SPICE solve (~400 ms): 8/8 setup() reads plus the first two
|
||||
* loop() passes returned 0 in the deterministic repro, which is exactly
|
||||
* the phantom emergency-stop latch of the 2026-07 audit. */
|
||||
onPullChange: ((pin: number, pull: 0 | 1 | 2) => void) | null = null;
|
||||
|
||||
setPinPull(pin: number, pull: 0 | 1 | 2): void {
|
||||
const prev = this.pinPulls.get(pin) ?? 0;
|
||||
if (pull === 0) this.pinPulls.delete(pin);
|
||||
else this.pinPulls.set(pin, pull);
|
||||
if (prev !== pull) this.onPullChange?.(pin, pull);
|
||||
}
|
||||
|
||||
/** Internal pull config for a pin: 0 = none, 1 = pull-up, 2 = pull-down. */
|
||||
|
|
|
|||
|
|
@ -623,12 +623,24 @@ function makeGpioRoutingClearHandler(boardId: string) {
|
|||
function makePinPullHandler(boardId: string) {
|
||||
return (gpio: number, pull: 0 | 1 | 2) => {
|
||||
// Record the internal pull so the netlist stamps a weak resistor
|
||||
// (vcc_rail for pull-up, GND for pull-down) and request a re-solve. The
|
||||
// digital read itself is driven from the solved circuit by
|
||||
// connectDigitalInputsToMcu — we deliberately do NOT seed the pin directly
|
||||
// here, because that would bypass the real wiring and re-introduce the
|
||||
// "mis-wired button still works" bug.
|
||||
pinManagerMap.get(boardId)?.setPinPull(gpio, pull);
|
||||
// (vcc_rail for pull-up, GND for pull-down) and request a re-solve.
|
||||
const pm = pinManagerMap.get(boardId);
|
||||
pm?.setPinPull(gpio, pull);
|
||||
// Seed the guest input to the pull's RESTING level immediately (real
|
||||
// silicon raises the pad in nanoseconds). Two failure modes this
|
||||
// closes (2026-07 audit): (a) the boot window before the first SPICE
|
||||
// solve where INPUT_PULLUP read LOW and phantom-triggered buttons,
|
||||
// and (b) an INPUT_PULLUP pin with NOTHING wired — no net in
|
||||
// pinNetMap, so connectDigitalInputsToMcu never drives it and the
|
||||
// guest read 0 forever. On wired+sourced nets the connector overrides
|
||||
// with the solved level right after, so the "mis-wired button still
|
||||
// works" bug does NOT come back: the solve remains authoritative.
|
||||
if (pull !== 0 && !(pm?.getOutputPins().has(gpio))) {
|
||||
const sim = simulatorMap.get(boardId) as
|
||||
| { setPinState?: (pin: number, state: boolean) => void }
|
||||
| undefined;
|
||||
sim?.setPinState?.(gpio, pull === 1);
|
||||
}
|
||||
requestElectricalResolve();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue