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 { isStm32BoardKind } from '../../types/board';
|
||||||
import { stm32PinNameToLinear } from '../Stm32Bridge';
|
import { stm32PinNameToLinear } from '../Stm32Bridge';
|
||||||
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
||||||
|
import { boardPinToNumber } from '../../utils/boardPinMapping';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a board pin name (e.g. "9", "A0", "GP26", "GPIO32") to the
|
* 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 {
|
export function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number {
|
||||||
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
|
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
|
||||||
if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) return -1;
|
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,
|
// Single source of truth: utils/boardPinMapping knows every family's
|
||||||
// turning 'GPIO32' into parseInt('IO32') = NaN → -1 (dead branch).
|
// 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')) {
|
if (pinName.startsWith('GPIO')) {
|
||||||
const n = parseInt(pinName.slice(4), 10);
|
const g = parseInt(pinName.slice(4), 10);
|
||||||
return Number.isFinite(n) ? n : -1;
|
return Number.isFinite(g) ? g : -1;
|
||||||
}
|
}
|
||||||
if (pinName.startsWith('GP')) {
|
if (pinName.startsWith('GP')) {
|
||||||
const n = parseInt(pinName.slice(2), 10);
|
const g = parseInt(pinName.slice(2), 10);
|
||||||
return Number.isFinite(n) ? n : -1;
|
return Number.isFinite(g) ? g : -1;
|
||||||
}
|
}
|
||||||
if (/^A\d+$/.test(pinName)) {
|
if (/^A\d+$/.test(pinName)) {
|
||||||
return 14 + parseInt(pinName.slice(1), 10);
|
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)) {
|
if (/^PB\d+$/.test(pinName)) {
|
||||||
return parseInt(pinName.slice(2), 10);
|
return parseInt(pinName.slice(2), 10);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { useElectricalStore } from '../../store/useElectricalStore';
|
||||||
import { setAdcVoltage } from '../parts/partUtils';
|
import { setAdcVoltage } from '../parts/partUtils';
|
||||||
import type { BoardKind } from '../../types/board';
|
import type { BoardKind } from '../../types/board';
|
||||||
import { interpolateAt } from './waveformStats';
|
import { interpolateAt } from './waveformStats';
|
||||||
|
import { boardPinToNumber } from '../../utils/boardPinMapping';
|
||||||
|
|
||||||
// Which Arduino-style pin name maps to which ADC channel, per board.
|
// Which Arduino-style pin name maps to which ADC channel, per board.
|
||||||
function adcRange(prefix: string, start: number, count: number) {
|
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 },
|
{ pinName: 'GP29', channel: 3 },
|
||||||
],
|
],
|
||||||
|
|
||||||
// ESP32
|
// ESP32 — pinNames here are lookup keys into pinNetMap, so they MUST be
|
||||||
esp32: adcRange('GPIO', 32, 8),
|
// the boards' REAL silkscreen names. The old 'GPIO32'-style keys never
|
||||||
'esp32-devkit-c-v4': adcRange('GPIO', 32, 8),
|
// matched the bare-number pin names actual wires carry ('34', '4', 'A0'),
|
||||||
'esp32-cam': adcRange('GPIO', 32, 8),
|
// 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),
|
'wemos-lolin32-lite': adcRange('GPIO', 32, 8),
|
||||||
'esp32-s3': adcRange('GPIO', 1, 10),
|
'esp32-s3': adcRange('', 1, 10),
|
||||||
'xiao-esp32-s3': adcRange('GPIO', 1, 10),
|
'xiao-esp32-s3': adcRange('D', 0, 9),
|
||||||
'arduino-nano-esp32': adcRange('A', 0, 8),
|
'arduino-nano-esp32': adcRange('A', 0, 8),
|
||||||
'esp32-c3': adcRange('GPIO', 0, 6),
|
'esp32-c3': adcRange('', 0, 6),
|
||||||
'xiao-esp32-c3': adcRange('GPIO', 0, 6),
|
'xiao-esp32-c3': adcRange('D', 0, 4),
|
||||||
'aitewinrobot-esp32c3-supermini': adcRange('GPIO', 0, 6),
|
'aitewinrobot-esp32c3-supermini': adcRange('', 0, 6),
|
||||||
};
|
};
|
||||||
|
|
||||||
function avrPinFromName(_name: string, channel: number): number {
|
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,
|
'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 {
|
export function connectAnalogInputsToMcu(): () => void {
|
||||||
const patchedAdcs = new WeakSet<object>();
|
const patchedAdcs = new WeakSet<object>();
|
||||||
const qemuWaveformChannels = new Set<string>();
|
const qemuWaveformChannels = new Set<string>();
|
||||||
|
|
@ -138,8 +159,8 @@ export function connectAnalogInputsToMcu(): () => void {
|
||||||
const v = nodeVoltages[netName];
|
const v = nodeVoltages[netName];
|
||||||
if (v == null) continue;
|
if (v == null) continue;
|
||||||
const clamped = Math.max(0, Math.min(vMax, v));
|
const clamped = Math.max(0, Math.min(vMax, v));
|
||||||
const gpioPin = ADC_PIN_TO_GPIO[board.boardKind]?.(pinName, channel);
|
const gpioPin = gpioForBoardPin(board.boardKind, pinName, channel);
|
||||||
if (gpioPin != null) setAdcVoltage(sim, gpioPin, clamped);
|
if (gpioPin >= 0) setAdcVoltage(sim, gpioPin, clamped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,8 +189,7 @@ export function connectAnalogInputsToMcu(): () => void {
|
||||||
setAdcWaveform?: (pin: number, samples: Uint16Array, periodNs: number) => boolean;
|
setAdcWaveform?: (pin: number, samples: Uint16Array, periodNs: number) => boolean;
|
||||||
};
|
};
|
||||||
if (typeof shim.setAdcWaveform !== 'function') continue;
|
if (typeof shim.setAdcWaveform !== 'function') continue;
|
||||||
const gpioFn = ADC_PIN_TO_GPIO[board.boardKind];
|
const gpioFn = (pn: string, ch: number) => gpioForBoardPin(board.boardKind, pn, ch);
|
||||||
if (!gpioFn) continue;
|
|
||||||
const boardId = board.id;
|
const boardId = board.id;
|
||||||
const seen = new Set<number>();
|
const seen = new Set<number>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@
|
||||||
import { useSimulatorStore, getBoardSimulator, getBoardPinManager } from '../../store/useSimulatorStore';
|
import { useSimulatorStore, getBoardSimulator, getBoardPinManager } from '../../store/useSimulatorStore';
|
||||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||||
import { isStm32BoardKind } from '../../types/board';
|
import { isStm32BoardKind } from '../../types/board';
|
||||||
|
import type { BoardKind } from '../../types/board';
|
||||||
import { stm32PinNameToLinear } from '../Stm32Bridge';
|
import { stm32PinNameToLinear } from '../Stm32Bridge';
|
||||||
|
import { boardPinToNumber } from '../../utils/boardPinMapping';
|
||||||
|
|
||||||
// 3.3 V LVCMOS thresholds with a hysteresis band so a node hovering near the
|
// 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
|
// 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;
|
const V_LOW = 0.8;
|
||||||
|
|
||||||
/** Map a board pin name to a plain GPIO number, or -1 if it isn't one we
|
/** 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.). */
|
* drive digitally (GND/VCC/UART-named pads, etc.). Delegates to the
|
||||||
function gpioFromPinName(name: string): number {
|
* 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"
|
if (/^\d+$/.test(name)) return parseInt(name, 10); // "4", "15"
|
||||||
const m = name.match(/^GPIO(\d+)$/i) || name.match(/^GP(\d+)$/i);
|
const m = name.match(/^GPIO(\d+)$/i) || name.match(/^GP(\d+)$/i);
|
||||||
return m ? parseInt(m[1], 10) : -1;
|
return m ? parseInt(m[1], 10) : -1;
|
||||||
|
|
@ -61,7 +68,7 @@ export function connectDigitalInputsToMcu(): () => void {
|
||||||
for (const [key, net] of pinNetMap) {
|
for (const [key, net] of pinNetMap) {
|
||||||
if (!key.startsWith(prefix)) continue;
|
if (!key.startsWith(prefix)) continue;
|
||||||
const pinName = key.slice(prefix.length);
|
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 (gpio < 0) continue;
|
||||||
if (driven.has(gpio)) continue; // the MCU drives this pin (digitalWrite)
|
if (driven.has(gpio)) continue; // the MCU drives this pin (digitalWrite)
|
||||||
// Only drive pins whose net is backed by a real source/element (rail,
|
// 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);
|
if (pin >= 11 && pin <= 20) return 10 + (pin - 11);
|
||||||
return -1;
|
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 >= 36 && pin <= 39) return pin - 36; // GPIO 36→CH0 … 39→CH3
|
||||||
if (pin >= 32 && pin <= 35) return pin - 28; // GPIO 32→CH4 … 35→CH7
|
if (pin >= 32 && pin <= 35) return pin - 28; // GPIO 32→CH4 … 35→CH7
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue