From d64eebc200b23e08a5725a010f350f1313e9f7b2 Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 26 May 2026 21:28:41 +0200 Subject: [PATCH] fix(stop): reset CPU to PC=0 on Stop (real-life power-cycle semantics) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix preserved display state on Stop so Resume could pick up the multiplexed frame seamlessly — but that's Pause semantics, not Stop. On a real Arduino, hitting the physical Stop is cutting power: the next Run must boot from setup(), not continue at the saved PC. User report on https://velxio.dev/example/uno-7segment : > empieza a contar, le doy stop en el 6, le doy run y sigue desde 6 stopBoard now: - calls sim.reset() (was sim.stop()) — CPU back to PC=0 - calls hardResetPinStates() (was the soft resetPinStates) — clears cached states AND notifies listeners so 7-seg / NeoPixel / LCD blank out instead of freezing on whatever was lit. Reset and Stop are now the same cold-boot semantics; Reset still additionally clears serial output + baud rate. The soft resetPinStates() helper stays for internal SPICE-classification-only paths that don't want listener fan-out. --- frontend/src/simulation/PinManager.ts | 16 +++++-------- frontend/src/store/useSimulatorStore.ts | 30 +++++++++++++------------ 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/frontend/src/simulation/PinManager.ts b/frontend/src/simulation/PinManager.ts index 4d159270..ff0388ba 100644 --- a/frontend/src/simulation/PinManager.ts +++ b/frontend/src/simulation/PinManager.ts @@ -139,16 +139,12 @@ export class PinManager { } /** - * Soft cleanup for stopBoard: drop the MCU-output classification so - * the next Run doesn't keep emitting stale V-sources on the SPICE - * side from a pin the previous program had driven. The cached pin - * STATES (`pinStates`) stay put — components that hold visual state - * (7-segment, NeoPixel, LCD, dot-matrix) keep showing the last - * pattern, which is what users expect from a pause/stop. On resume, - * avr8js's port-listener fires only for bits that CHANGED relative - * to its internal oldValue, so blanking the cache here would race: - * if the sketch's port register matches what it was pre-stop, no - * pinChange fires and the display would never recover. + * Drop only the MCU-output classification (SPICE side). Used by + * paths that need to forget which pins were driven this session + * without disturbing the cached pin states or notifying listeners. + * For the user-facing Stop / Reset / firmware-reload flows use + * `hardResetPinStates` — those are cold boots and the next Run + * must start from setup() with every visual cleared. */ resetPinStates(): void { this.outputPins.clear(); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 611f76d6..d44cf02a 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -1599,13 +1599,19 @@ export const useSimulatorStore = create((set, get) => { } else if (isEsp32Kind(board.boardKind)) { getEsp32Bridge(boardId)?.disconnect(); } else { - getBoardSimulator(boardId)?.stop(); + // Stop is "cut power": pressing Run again must boot from setup() + // not resume mid-loop, so reset the CPU to PC=0 here. Without + // this the AVR keeps its program counter and the next Run picks + // up wherever it left off — which is fine for Pause but wrong + // for the physical Stop button users expect. + getBoardSimulator(boardId)?.reset(); } - // Drop MCU-output classification so the next Run starts clean and - // collectPinStates doesn't emit stale V-sources before the new - // firmware has driven any pins. - getBoardPinManager(boardId)?.resetPinStates(); + // Hard reset: clear cached pin states AND notify listeners so + // multiplexed displays (7-segment, LED matrix, NeoPixel) clear + // the frozen frame they were holding when power was cut, instead + // of carrying it into the next run. + getBoardPinManager(boardId)?.hardResetPinStates(); set((s) => { const boards = s.boards.map((b) => (b.id === boardId ? { ...b, running: false } : b)); @@ -1629,15 +1635,11 @@ export const useSimulatorStore = create((set, get) => { const sim = getBoardSimulator(boardId); if (sim) { sim.reset(); - // Reset is a hard reboot — the CPU starts at PC=0, every pin - // goes back to floating, every output classification is - // dropped. The hardResetPinStates call also notifies - // listeners that previously-HIGH pins are now LOW, so visual - // components (7-segment, NeoPixel, LCD) clear their stale - // pattern instead of freezing on whatever was lit at the - // moment of Reset. The Stop path uses the lighter - // resetPinStates() that ONLY drops the output classification - // (preserves the display so it can resume) — see PinManager. + // Hard reboot: CPU back to PC=0, every pin floats, every + // output classification dropped, and listeners notified so + // visual components (7-segment, NeoPixel, LCD) clear their + // stale frame instead of freezing on whatever was lit. + // Same semantics as Stop — both behave like cutting power. getBoardPinManager(boardId)?.hardResetPinStates(); // NOTE: do NOT reassign sim.onSerialData here. sim.reset() // recreates the USART but the new usart.onByteTransmit