fix(stop): reset CPU to PC=0 on Stop (real-life power-cycle semantics)

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.
This commit is contained in:
David Montero 2026-05-26 21:28:41 +02:00
parent 858e160e6f
commit d64eebc200
2 changed files with 22 additions and 24 deletions

View File

@ -139,16 +139,12 @@ export class PinManager {
} }
/** /**
* Soft cleanup for stopBoard: drop the MCU-output classification so * Drop only the MCU-output classification (SPICE side). Used by
* the next Run doesn't keep emitting stale V-sources on the SPICE * paths that need to forget which pins were driven this session
* side from a pin the previous program had driven. The cached pin * without disturbing the cached pin states or notifying listeners.
* STATES (`pinStates`) stay put components that hold visual state * For the user-facing Stop / Reset / firmware-reload flows use
* (7-segment, NeoPixel, LCD, dot-matrix) keep showing the last * `hardResetPinStates` those are cold boots and the next Run
* pattern, which is what users expect from a pause/stop. On resume, * must start from setup() with every visual cleared.
* 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.
*/ */
resetPinStates(): void { resetPinStates(): void {
this.outputPins.clear(); this.outputPins.clear();

View File

@ -1599,13 +1599,19 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
} else if (isEsp32Kind(board.boardKind)) { } else if (isEsp32Kind(board.boardKind)) {
getEsp32Bridge(boardId)?.disconnect(); getEsp32Bridge(boardId)?.disconnect();
} else { } 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 // Hard reset: clear cached pin states AND notify listeners so
// collectPinStates doesn't emit stale V-sources before the new // multiplexed displays (7-segment, LED matrix, NeoPixel) clear
// firmware has driven any pins. // the frozen frame they were holding when power was cut, instead
getBoardPinManager(boardId)?.resetPinStates(); // of carrying it into the next run.
getBoardPinManager(boardId)?.hardResetPinStates();
set((s) => { set((s) => {
const boards = s.boards.map((b) => (b.id === boardId ? { ...b, running: false } : b)); const boards = s.boards.map((b) => (b.id === boardId ? { ...b, running: false } : b));
@ -1629,15 +1635,11 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const sim = getBoardSimulator(boardId); const sim = getBoardSimulator(boardId);
if (sim) { if (sim) {
sim.reset(); sim.reset();
// Reset is a hard reboot — the CPU starts at PC=0, every pin // Hard reboot: CPU back to PC=0, every pin floats, every
// goes back to floating, every output classification is // output classification dropped, and listeners notified so
// dropped. The hardResetPinStates call also notifies // visual components (7-segment, NeoPixel, LCD) clear their
// listeners that previously-HIGH pins are now LOW, so visual // stale frame instead of freezing on whatever was lit.
// components (7-segment, NeoPixel, LCD) clear their stale // Same semantics as Stop — both behave like cutting power.
// 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.
getBoardPinManager(boardId)?.hardResetPinStates(); getBoardPinManager(boardId)?.hardResetPinStates();
// NOTE: do NOT reassign sim.onSerialData here. sim.reset() // NOTE: do NOT reassign sim.onSerialData here. sim.reset()
// recreates the USART but the new usart.onByteTransmit // recreates the USART but the new usart.onByteTransmit