fix(spice): every overlay board was solved as a 5 V Arduino

BOARD_PIN_GROUPS is keyed by the BoardKind union, so a board registered by the
overlay could never appear in it and fell through to `default` — 5 V, ground
pads named GND/GND.1/GND.2, supply pads named 5V/VCC. Every RP2350 and
ESP32-family pro board is a 3.3 V part whose supply pad is called 3V3, so the
netlist powered it from the wrong rail and did not recognise its supply pad at
all. Nothing errors; the numbers are simply wrong.

Caught on the Pimoroni Pico Plus 2 W: a potentiometer across 3V3 and GND read
2.50 V at half travel instead of 1.65 V, because the divider was solving
against a 5 V rail. The same 1.5x error was sitting under every LED current,
pull-up and divider on every pro board.

boardPinGroupFor() consults the pro registry before falling back, and a board
declares its pads through ProBoardDef.power. The OSS table is untouched, so no
existing board changes behaviour.

Found while wiring the ADC seam: enabling the SPICE analog path for a pro board
is what finally made the wrong rail visible.
This commit is contained in:
David Montero Crespo 2026-08-23 03:07:43 +02:00
parent 6e543b5877
commit 0ef656849d
6 changed files with 100 additions and 6 deletions

View File

@ -0,0 +1,53 @@
/**
* A pro board's supply rail.
*
* BOARD_PIN_GROUPS is keyed by the BoardKind union, so an overlay board could
* never appear in it and fell through to `default`: 5 V, with supply pads named
* "5V"/"VCC". Every RP2350 and ESP32-family board the overlay registers is a
* 3.3 V part whose pad is called "3V3", so the whole netlist solved at the wrong
* rail — silently. It surfaces as soon as anything analog is wired: a divider
* off the 3V3 pad of a Pimoroni Pico Plus 2 W solved at 5 V, and a potentiometer
* at half travel read 2.50 V where the hardware gives 1.65 V.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { registerProBoards, type ProBoardDef } from '../lib/proBoardRegistry';
import { boardPinGroupFor, BOARD_PIN_GROUPS } from '../simulation/spice/boardPinGroups';
const KIND = 'test-overlay-3v3-board';
describe('boardPinGroupFor', () => {
beforeEach(() => {
registerProBoards([
{
kind: KIND,
label: '3V3 overlay board',
fqbn: null,
description: 'test',
tag: 'velxio-test-3v3',
size: { w: 10, h: 10 },
power: {
vcc: 3.3,
gnd: ['GND', 'GND.1'],
vcc_pins: ['3V3'],
aux: { volts: 5, pins: ['VBUS', 'VSYS'] },
},
} as ProBoardDef,
]);
});
it('uses the rail a pro board declares, not the 5 V default', () => {
const g = boardPinGroupFor(KIND);
expect(g.vcc).toBe(3.3);
expect(g.vcc_pins).toContain('3V3');
expect(g.aux?.volts).toBe(5);
});
it('still answers for the boards the OSS table names', () => {
expect(boardPinGroupFor('raspberry-pi-pico').vcc).toBe(3.3);
expect(boardPinGroupFor('arduino-uno').vcc).toBe(5);
});
it('falls back to the default for a board nobody has described', () => {
expect(boardPinGroupFor('some-unregistered-kind')).toBe(BOARD_PIN_GROUPS.default);
});
});

View File

@ -99,6 +99,25 @@ export interface ProBoardDef {
* capable GP40/41/42 through 1k) is declared under the name a wire uses.
*/
adcPins?: Array<{ pinName: string; channel: number }>;
/**
* Supply pads and logic voltage, for the SPICE netlist — the overlay's
* version of BOARD_PIN_GROUPS, which is keyed by the BoardKind union and so
* can never name a pro board.
*
* Without it a board falls back to that table's `default`: 5 V, ground pads
* called GND/GND.1/GND.2, supply pads called 5V/VCC. For a 3.3 V board with a
* "3V3" pad that is wrong twice over, and silently — the rail is simply
* solved at 5 V, so every divider, pull-up and LED current on the board is
* off by 1.5x.
*
* Shape matches BoardPinGroup: { vcc, gnd[], vcc_pins[], aux? }.
*/
power?: {
vcc: number;
gnd: string[];
vcc_pins: string[];
aux?: { volts: number; pins: string[] };
};
/** In-browser simulator factory (e.g. the RP2350/Hazard3 emulator). The pm
* argument is the store's PinManager instance. */
createSimulator?: (pm: unknown) => ProBoardSimulator;

View File

@ -6,6 +6,7 @@
* Extend this table as new boards are added.
*/
import type { BoardKind } from '../../types/board';
import { getProBoard } from '../../lib/proBoardRegistry';
export interface BoardPinGroup {
/** Supply voltage (V). */
@ -155,3 +156,24 @@ export const BOARD_PIN_GROUPS: Record<AllBoardKinds, BoardPinGroup> = {
aux: { volts: 5, pins: ['5V'] },
},
};
/**
* The pin classification for a board, including boards this table cannot name.
*
* BOARD_PIN_GROUPS is keyed by the BoardKind union, so an overlay board — whose
* kind is a runtime string — could never appear in it and fell through to
* `default`: a 5 V part whose only supply pad is called "5V". Every RP2350 and
* ESP32-family board registered by the overlay was therefore solved at the
* wrong rail. It shows up the moment anything analog is wired: a divider off
* the 3V3 pad of a Pimoroni Pico Plus 2 W solved at 5 V, so a potentiometer at
* half travel read 2.50 V instead of 1.65 V.
*
* A pro board declares its own supply pads through ProBoardDef.power.
*/
export function boardPinGroupFor(kind: string): BoardPinGroup {
const known = BOARD_PIN_GROUPS[kind as AllBoardKinds];
if (known) return known;
const pro = getProBoard(kind)?.power;
if (pro) return pro;
return BOARD_PIN_GROUPS.default;
}

View File

@ -31,7 +31,7 @@ import { stm32LinearToPinName, stm32PinNameToLinear } from '../Stm32Bridge';
import { isStm32BoardKind, isPiBoardKind } from '../../types/board';
import type { BoardKind } from '../../types/board';
import { useElectricalStore } from '../../store/useElectricalStore';
import { BOARD_PIN_GROUPS } from './boardPinGroups';
import { boardPinGroupFor } from './boardPinGroups';
import { pinNameToArduinoPin } from './collectPinStates';
import type { CircuitSimulationService } from './CircuitSimulationService';
@ -137,7 +137,7 @@ export function connectMcuEdgesToService(service: CircuitSimulationService): ()
function subscribeBoard(boardId: string, boardKind: string): void {
const pm = getBoardPinManager(boardId);
if (!pm) return;
const group = BOARD_PIN_GROUPS[boardKind as keyof typeof BOARD_PIN_GROUPS] ?? BOARD_PIN_GROUPS.default;
const group = boardPinGroupFor(boardKind);
const vcc = group.vcc;
const pinSubs = new Map<number, () => void>();

View File

@ -16,7 +16,7 @@ import type {
} from './types';
import type { Wire } from '../../types/wire';
import type { BoardKind } from '../../types/board';
import { BOARD_PIN_GROUPS } from './boardPinGroups';
import { boardPinGroupFor } from './boardPinGroups';
import { parseValueWithUnits } from './valueParser';
import { PASSIVE_PRESETS } from './componentToSpice';
@ -182,7 +182,7 @@ export function buildInputFromStore(snap: StoreSnapshot): BuildNetlistInput {
}));
const boards: BoardForSpice[] = snap.boards.map((b) => {
const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default;
const group = boardPinGroupFor(b.boardKind);
return {
id: b.id,
boardKind: b.boardKind,

View File

@ -10,7 +10,7 @@
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { buildInputFromStore } from '../spice/storeAdapter';
import { BOARD_PIN_GROUPS } from '../spice/boardPinGroups';
import { boardPinGroupFor } from '../spice/boardPinGroups';
import type { PinSourceState } from '../spice/types';
import { verifyCircuit, type VerificationResult } from './circuitVerifier';
@ -47,7 +47,7 @@ export async function verifyCircuitFromStore(): Promise<VerificationResult | nul
// current and doesn't trip overcurrent / overpower. A circuit
// that would actually fault under HIGH is flagged correctly.
const pinStates: Record<string, PinSourceState> = {};
const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default;
const group = boardPinGroupFor(b.boardKind);
const wiredPinNames = new Set<string>();
for (const w of sim.wires) {
if (w.start.componentId === b.id) wiredPinNames.add(w.start.pinName);