From a18964b401eca21fa4a1c5ff38bb15ae4fa61acf Mon Sep 17 00:00:00 2001 From: David Montero Date: Fri, 31 Jul 2026 19:20:59 +0200 Subject: [PATCH] spice: unify silkscreen->GPIO mapping on boardPinToNumber (fixes STM32 outputs, nano-esp32 buttons, esp32-family SPICE analog) Three parallel half-implementations of the pin-name mapping each broke a different board family (2026-07 emulation-gaps audit): - collectPinStates.pinNameToArduinoPin returned -1 for STM32 'PC13' (no V source stamped: LED dark with correct firmware) and for nano-esp32 'D2'/'A0' (no pull resistor stamped for INPUT_PULLUP buttons). - connectDigitalInputsToMcu.gpioFromPinName only knew digits/GPIO/GP, so labeled pins were never driven from the solve (buttons stuck LOW). - connectAnalogInputsToMcu's ADC_PIN_MAP used 'GPIO32'-style lookup keys that never matched the real bare-number wire pin names, so the SPICE->ADC path skipped every esp32-family pin; its per-kind GPIO converters also mapped nano-esp32 A-pins with the AVR convention. All three now delegate to utils/boardPinMapping.boardPinToNumber (with the previous behavior as fallback for unknown names). Also adds the ESP32-C3 branch to the store's adcChannelForPin (GPIO0-5 -> CH0-5, verified against the qemu SARADC channel layout). --- .../src/simulation/spice/collectPinStates.ts | 25 ++++++---- .../spice/connectAnalogInputsToMcu.ts | 46 +++++++++++++------ .../spice/connectDigitalInputsToMcu.ts | 13 ++++-- frontend/src/store/useSimulatorStore.ts | 6 +++ 4 files changed, 66 insertions(+), 24 deletions(-) diff --git a/frontend/src/simulation/spice/collectPinStates.ts b/frontend/src/simulation/spice/collectPinStates.ts index e6f55303..1524e40c 100644 --- a/frontend/src/simulation/spice/collectPinStates.ts +++ b/frontend/src/simulation/spice/collectPinStates.ts @@ -13,6 +13,7 @@ import type { BoardKind } from '../../types/board'; import { isStm32BoardKind } from '../../types/board'; import { stm32PinNameToLinear } from '../Stm32Bridge'; import { BOARD_PIN_GROUPS } from './boardPinGroups'; +import { boardPinToNumber } from '../../utils/boardPinMapping'; /** * Convert a board pin name (e.g. "9", "A0", "GP26", "GPIO32") to the @@ -27,21 +28,29 @@ import { BOARD_PIN_GROUPS } from './boardPinGroups'; export function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number { const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default; if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) return -1; - // 'GPIO' must be tested BEFORE 'GP': the GP branch used to shadow it, - // turning 'GPIO32' into parseInt('IO32') = NaN → -1 (dead branch). + // Single source of truth: utils/boardPinMapping knows every family's + // silkscreen->GPIO map (nano-esp32 D2->GPIO5, xiao D-pins, STM32 + // PC13->linear 45, Pico GP*, ...). This function used to be a parallel + // half-implementation: 'PC13'/'D2' fell through to -1, so STM32 outputs + // never got a V source stamped (LED dark with correct firmware, lianqi + // 07-23) and nano-esp32 INPUT_PULLUP pins never got their pull resistor + // (dead START button, prabkagi 07-21 — 2026-07 emulation-gaps audit). + const n = boardPinToNumber(boardKind, pinName); + if (n != null) return n; + // Fallbacks for names boardPinToNumber doesn't know (older saved + // projects / generic boards): GPIO before GP so 'GPIO32' never parses + // as 'IO32'; A uses the AVR convention; ATtiny85 PB is identity. if (pinName.startsWith('GPIO')) { - const n = parseInt(pinName.slice(4), 10); - return Number.isFinite(n) ? n : -1; + const g = parseInt(pinName.slice(4), 10); + return Number.isFinite(g) ? g : -1; } if (pinName.startsWith('GP')) { - const n = parseInt(pinName.slice(2), 10); - return Number.isFinite(n) ? n : -1; + const g = parseInt(pinName.slice(2), 10); + return Number.isFinite(g) ? g : -1; } if (/^A\d+$/.test(pinName)) { return 14 + parseInt(pinName.slice(1), 10); } - // ATtiny85 port-style names (PB0..PB5) map 1:1 to Arduino pin numbers - // because ATTinyCore's pinMap is identity for the tiny85 variant. if (/^PB\d+$/.test(pinName)) { return parseInt(pinName.slice(2), 10); } diff --git a/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts b/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts index 97693b8f..4805feab 100644 --- a/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts +++ b/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts @@ -30,6 +30,7 @@ import { useElectricalStore } from '../../store/useElectricalStore'; import { setAdcVoltage } from '../parts/partUtils'; import type { BoardKind } from '../../types/board'; import { interpolateAt } from './waveformStats'; +import { boardPinToNumber } from '../../utils/boardPinMapping'; // Which Arduino-style pin name maps to which ADC channel, per board. function adcRange(prefix: string, start: number, count: number) { @@ -72,17 +73,21 @@ const ADC_PIN_MAP: PartialADC path silently skipped every esp32-family pin + // (2026-07 emulation-gaps audit; divider circuits read 0 forever). + esp32: adcRange('', 32, 8), + 'esp32-devkit-c-v4': adcRange('', 32, 8), + 'esp32-cam': adcRange('', 32, 8), 'wemos-lolin32-lite': adcRange('GPIO', 32, 8), - 'esp32-s3': adcRange('GPIO', 1, 10), - 'xiao-esp32-s3': adcRange('GPIO', 1, 10), + 'esp32-s3': adcRange('', 1, 10), + 'xiao-esp32-s3': adcRange('D', 0, 9), 'arduino-nano-esp32': adcRange('A', 0, 8), - 'esp32-c3': adcRange('GPIO', 0, 6), - 'xiao-esp32-c3': adcRange('GPIO', 0, 6), - 'aitewinrobot-esp32c3-supermini': adcRange('GPIO', 0, 6), + 'esp32-c3': adcRange('', 0, 6), + 'xiao-esp32-c3': adcRange('D', 0, 4), + 'aitewinrobot-esp32c3-supermini': adcRange('', 0, 6), }; function avrPinFromName(_name: string, channel: number): number { @@ -114,6 +119,22 @@ const ADC_PIN_TO_GPIO: Partial GPIO for the ADC injection. ESP32-family boards go + * through the canonical boardPinToNumber table (nano-esp32 'A0' -> GPIO1, + * xiao 'D1' -> GPIO2, bare '34' -> 34, ...); AVR/ATtiny/Pico keep their + * legacy per-kind converters (their sims expect Arduino pin numbers, not + * GPIOs — e.g. uno 'A0' -> 14). */ +function gpioForBoardPin(kind: BoardKind, pinName: string, channel: number): number { + if (kind === 'esp32' || kind.startsWith('esp32') || kind.startsWith('xiao-esp32') + || kind === 'arduino-nano-esp32' || kind === 'wemos-lolin32-lite' + || kind === 'aitewinrobot-esp32c3-supermini') { + const n = boardPinToNumber(kind, pinName); + if (n != null && n >= 0) return n; + } + const legacy = ADC_PIN_TO_GPIO[kind]; + return legacy ? legacy(pinName, channel) : -1; +} + export function connectAnalogInputsToMcu(): () => void { const patchedAdcs = new WeakSet(); const qemuWaveformChannels = new Set(); @@ -138,8 +159,8 @@ export function connectAnalogInputsToMcu(): () => void { const v = nodeVoltages[netName]; if (v == null) continue; const clamped = Math.max(0, Math.min(vMax, v)); - const gpioPin = ADC_PIN_TO_GPIO[board.boardKind]?.(pinName, channel); - if (gpioPin != null) setAdcVoltage(sim, gpioPin, clamped); + const gpioPin = gpioForBoardPin(board.boardKind, pinName, channel); + if (gpioPin >= 0) setAdcVoltage(sim, gpioPin, clamped); } } } @@ -168,8 +189,7 @@ export function connectAnalogInputsToMcu(): () => void { setAdcWaveform?: (pin: number, samples: Uint16Array, periodNs: number) => boolean; }; if (typeof shim.setAdcWaveform !== 'function') continue; - const gpioFn = ADC_PIN_TO_GPIO[board.boardKind]; - if (!gpioFn) continue; + const gpioFn = (pn: string, ch: number) => gpioForBoardPin(board.boardKind, pn, ch); const boardId = board.id; const seen = new Set(); diff --git a/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts b/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts index 843a02e2..e7422ec7 100644 --- a/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts +++ b/frontend/src/simulation/spice/connectDigitalInputsToMcu.ts @@ -22,7 +22,9 @@ import { useSimulatorStore, getBoardSimulator, getBoardPinManager } from '../../store/useSimulatorStore'; import { useElectricalStore } from '../../store/useElectricalStore'; import { isStm32BoardKind } from '../../types/board'; +import type { BoardKind } from '../../types/board'; import { stm32PinNameToLinear } from '../Stm32Bridge'; +import { boardPinToNumber } from '../../utils/boardPinMapping'; // 3.3 V LVCMOS thresholds with a hysteresis band so a node hovering near the // midpoint doesn't chatter. A pulled-up idle input sits at ~3.3 V and a @@ -31,8 +33,13 @@ const V_HIGH = 2.0; const V_LOW = 0.8; /** Map a board pin name to a plain GPIO number, or -1 if it isn't one we - * drive digitally (GND/VCC/UART-named pads, etc.). */ -function gpioFromPinName(name: string): number { + * drive digitally (GND/VCC/UART-named pads, etc.). Delegates to the + * canonical silkscreen->GPIO table so labeled pins resolve too — the old + * digits/GPIO-only regex returned -1 for nano-esp32 'D2'/'A0', which left + * INPUT_PULLUP buttons permanently LOW on that family (2026-07 audit). */ +function gpioFromPinName(name: string, boardKind: BoardKind): number { + const n = boardPinToNumber(boardKind, name); + if (n != null) return n; if (/^\d+$/.test(name)) return parseInt(name, 10); // "4", "15" const m = name.match(/^GPIO(\d+)$/i) || name.match(/^GP(\d+)$/i); return m ? parseInt(m[1], 10) : -1; @@ -61,7 +68,7 @@ export function connectDigitalInputsToMcu(): () => void { for (const [key, net] of pinNetMap) { if (!key.startsWith(prefix)) continue; const pinName = key.slice(prefix.length); - const gpio = isStm32 ? stm32PinNameToLinear(pinName) : gpioFromPinName(pinName); + const gpio = isStm32 ? stm32PinNameToLinear(pinName) : gpioFromPinName(pinName, board.boardKind); 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, diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index bd57a8d0..7059949c 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -225,6 +225,12 @@ class Esp32BridgeShim { if (pin >= 11 && pin <= 20) return 10 + (pin - 11); return -1; } + if (kind === 'esp32-c3' || kind === 'xiao-esp32-c3' || kind === 'aitewinrobot-esp32c3-supermini') { + // C3: ADC1 = GPIO0-4 -> CH0-4, GPIO5 (ADC2_CH0) -> index 5. + // Verified against the qemu SARADC: esp32_adc_set{channel:3} is what + // analogRead(3) returns (emulation-gaps harness, 2026-07-31). + return pin >= 0 && pin <= 5 ? pin : -1; + } if (pin >= 36 && pin <= 39) return pin - 36; // GPIO 36→CH0 … 39→CH3 if (pin >= 32 && pin <= 35) return pin - 28; // GPIO 32→CH4 … 35→CH7 return -1;