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).
This commit is contained in:
parent
cd5cbb14f3
commit
a18964b401
|
|
@ -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<n> uses the AVR convention; ATtiny85 PB<n> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: Partial<Record<BoardKind, Array<{ pinName: string; channel: n
|
|||
{ pinName: 'GP29', channel: 3 },
|
||||
],
|
||||
|
||||
// ESP32
|
||||
esp32: adcRange('GPIO', 32, 8),
|
||||
'esp32-devkit-c-v4': adcRange('GPIO', 32, 8),
|
||||
'esp32-cam': adcRange('GPIO', 32, 8),
|
||||
// ESP32 — pinNames here are lookup keys into pinNetMap, so they MUST be
|
||||
// the boards' REAL silkscreen names. The old 'GPIO32'-style keys never
|
||||
// matched the bare-number pin names actual wires carry ('34', '4', 'A0'),
|
||||
// so the SPICE->ADC 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<Record<BoardKind, (pinName: string, channel: numb
|
|||
'aitewinrobot-esp32c3-supermini': gpioPinFromName,
|
||||
};
|
||||
|
||||
/** Silkscreen name -> 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<object>();
|
||||
const qemuWaveformChannels = new Set<string>();
|
||||
|
|
@ -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<number>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue