sim: drive RP2040 + STM32 digital inputs from the real circuit
Extend the spice-driven input path (already live for AVR/ESP32) to RP2040 and STM32 so digitalRead() of an INPUT pin reflects the actual wiring: a pin tied to a rail reads that rail, and an INPUT_PULLUP button-to-GND reads idle-HIGH / pressed-LOW instead of floating or inverted. RP2040 (rp2040js, frontend-only): the GPIO listener now splits input vs output mode. Input pins report their pad pull (InputPullUp/Down) via setPinPull and seed the pull's idle level (rp2040js does not auto-apply the pad pull to the readable input register); the SPICE solve then overrides via connectDigital- InputsToMcu when the net is actually sourced. Output pins drive as before. spiceDrivenInputs = true. STM32 (backend QEMU): the worker now forwards a new gpio_pull event (from the libqemu-arm picsimlab_pull_pin callback) so the netlist stamps the matching weak resistor; Stm32Bridge surfaces it, Stm32BridgeShim opts into spiceDrivenInputs, and collectPinStates maps PA0/PC13 names to the linear pin so the pull is read. STM32 outputs stay on the part layer (unchanged). Event-driven parts with no SPICE model (rotary encoder, keypad) remain protected by the existing sourcedNets gate in the connector.
This commit is contained in:
parent
48099c0bda
commit
ed132afb91
|
|
@ -83,9 +83,13 @@ _SPI_EVENT = ctypes.CFUNCTYPE(ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint16)
|
||||||
_UART_TX = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint8)
|
_UART_TX = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint8)
|
||||||
_RMT_EVENT = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32)
|
_RMT_EVENT = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32)
|
||||||
_GPIO_MATRIX_CB = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
|
_GPIO_MATRIX_CB = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
|
||||||
|
_PULL_PIN = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
|
||||||
|
|
||||||
|
|
||||||
class _CallbacksT(ctypes.Structure):
|
class _CallbacksT(ctypes.Structure):
|
||||||
|
# MUST match, field-for-field, the callbacks_t struct in
|
||||||
|
# hw/arm/stm32_picsimlab.c. picsimlab_pull_pin is appended last (matching
|
||||||
|
# the C append) so the ESP32-compatible prefix stays byte-identical.
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
('picsimlab_write_pin', _WRITE_PIN),
|
('picsimlab_write_pin', _WRITE_PIN),
|
||||||
('picsimlab_dir_pin', _DIR_PIN),
|
('picsimlab_dir_pin', _DIR_PIN),
|
||||||
|
|
@ -95,6 +99,7 @@ class _CallbacksT(ctypes.Structure):
|
||||||
('pinmap', ctypes.c_void_p),
|
('pinmap', ctypes.c_void_p),
|
||||||
('picsimlab_rmt_event', _RMT_EVENT),
|
('picsimlab_rmt_event', _RMT_EVENT),
|
||||||
('picsimlab_gpio_matrix_cb', _GPIO_MATRIX_CB),
|
('picsimlab_gpio_matrix_cb', _GPIO_MATRIX_CB),
|
||||||
|
('picsimlab_pull_pin', _PULL_PIN),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -222,6 +227,16 @@ def main() -> None:
|
||||||
return
|
return
|
||||||
_emit({'type': 'gpio_dir', 'pin': int(pin), 'dir': int(direction)})
|
_emit({'type': 'gpio_dir', 'pin': int(pin), 'dir': int(direction)})
|
||||||
|
|
||||||
|
def _on_pull_change(pin, pull):
|
||||||
|
# Internal pull of an INPUT pin changed: 0=none, 1=up, 2=down. The
|
||||||
|
# frontend stamps the matching weak resistor in its circuit netlist so
|
||||||
|
# digitalRead() of a button-to-GND on an INPUT_PULLUP pin reads idle
|
||||||
|
# HIGH / pressed LOW. QEMU already de-dupes (only fires on a real
|
||||||
|
# change), so forward verbatim. `pin` is the linear port*16+pin index.
|
||||||
|
if _stopped.is_set() or pin < 0:
|
||||||
|
return
|
||||||
|
_emit({'type': 'gpio_pull', 'pin': int(pin), 'pull': int(pull)})
|
||||||
|
|
||||||
def _on_uart_tx(uart_id, byte_val):
|
def _on_uart_tx(uart_id, byte_val):
|
||||||
if _stopped.is_set():
|
if _stopped.is_set():
|
||||||
return
|
return
|
||||||
|
|
@ -291,6 +306,7 @@ def main() -> None:
|
||||||
pinmap = ctypes.cast(_PINMAP, ctypes.c_void_p).value,
|
pinmap = ctypes.cast(_PINMAP, ctypes.c_void_p).value,
|
||||||
picsimlab_rmt_event = _RMT_EVENT(_on_rmt_event),
|
picsimlab_rmt_event = _RMT_EVENT(_on_rmt_event),
|
||||||
picsimlab_gpio_matrix_cb = _GPIO_MATRIX_CB(_on_gpio_matrix),
|
picsimlab_gpio_matrix_cb = _GPIO_MATRIX_CB(_on_gpio_matrix),
|
||||||
|
picsimlab_pull_pin = _PULL_PIN(_on_pull_change),
|
||||||
)
|
)
|
||||||
lib.qemu_picsimlab_register_callbacks(ctypes.byref(_cbs_ref))
|
lib.qemu_picsimlab_register_callbacks(ctypes.byref(_cbs_ref))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { I2CDevice } from './I2CBusManager';
|
||||||
import { bootromB1 } from './rp2040-bootrom';
|
import { bootromB1 } from './rp2040-bootrom';
|
||||||
import { loadUF2, loadUserFiles, getFirmware } from './MicroPythonLoader';
|
import { loadUF2, loadUserFiles, getFirmware } from './MicroPythonLoader';
|
||||||
import { type PioPeripheral, createPioPeripheral } from './PioPeripheral';
|
import { type PioPeripheral, createPioPeripheral } from './PioPeripheral';
|
||||||
|
import { requestElectricalResolve } from './spice/electricalResolveHook';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
||||||
|
|
@ -145,6 +146,14 @@ export class IdleSpinDetector {
|
||||||
export type RP2040I2CDevice = I2CDevice;
|
export type RP2040I2CDevice = I2CDevice;
|
||||||
|
|
||||||
export class RP2040Simulator {
|
export class RP2040Simulator {
|
||||||
|
// Drive digital INPUT pins from the solved circuit (connectDigitalInputsToMcu)
|
||||||
|
// instead of the legacy part-seed, so digitalRead() reflects the REAL wiring:
|
||||||
|
// a pin tied to a rail reads that rail, a button-to-GND on an INPUT_PULLUP pin
|
||||||
|
// reads idle-HIGH / pressed-LOW. The internal pull is surfaced from the pad
|
||||||
|
// config in the GPIO listener below (see setupGpioListeners). Mirrors AVR /
|
||||||
|
// ESP32. Event-driven parts with no SPICE model (rotary encoder, keypad) are
|
||||||
|
// protected by the `sourcedNets` gate inside the connector.
|
||||||
|
readonly spiceDrivenInputs = true;
|
||||||
private rp2040: RP2040 | null = null;
|
private rp2040: RP2040 | null = null;
|
||||||
private running = false;
|
private running = false;
|
||||||
private animationFrame: number | null = null;
|
private animationFrame: number | null = null;
|
||||||
|
|
@ -768,7 +777,30 @@ export class RP2040Simulator {
|
||||||
if (!gpio) continue;
|
if (!gpio) continue;
|
||||||
|
|
||||||
const unsub = gpio.addListener((state: GPIOPinState) => {
|
const unsub = gpio.addListener((state: GPIOPinState) => {
|
||||||
const isHigh = state === GPIOPinState.High || state === GPIOPinState.InputPullUp;
|
// rp2040js reports the pin's MODE here, not its external value: Low/High
|
||||||
|
// mean the MCU is driving the pad (outputEnable), while Input/
|
||||||
|
// InputPullUp/InputPullDown/InputBusKeeper mean it's a high-Z input
|
||||||
|
// whose pad pull config is encoded in the state. The listener only fires
|
||||||
|
// on a mode/pull change (an external value change via setInputValue does
|
||||||
|
// not alter `value` for an input pin), so we can split cleanly.
|
||||||
|
if (state >= GPIOPinState.Input) {
|
||||||
|
// INPUT pin. Surface the internal pull so NetlistBuilder stamps the
|
||||||
|
// weak resistor; the actual logic level is injected from the SPICE
|
||||||
|
// solve by connectDigitalInputsToMcu. We do NOT mark the pin as an MCU
|
||||||
|
// output (triggerPinChange 'mcu'), or the connector would skip it.
|
||||||
|
const pull =
|
||||||
|
state === GPIOPinState.InputPullUp ? 1 : state === GPIOPinState.InputPullDown ? 2 : 0;
|
||||||
|
this.pinManager.setPinPull(pin, pull);
|
||||||
|
// Seed the idle level the pull alone would produce (rp2040js does not
|
||||||
|
// auto-apply the pad pull to the readable input register). The
|
||||||
|
// connector overrides this whenever the pin's net is actually sourced
|
||||||
|
// (rail / button / divider); an unwired pulled input keeps this level.
|
||||||
|
if (pull === 1) gpio.setInputValue(true);
|
||||||
|
else if (pull === 2) gpio.setInputValue(false);
|
||||||
|
requestElectricalResolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isHigh = state === GPIOPinState.High;
|
||||||
this.pinManager.triggerPinChange(pin, isHigh, 'mcu');
|
this.pinManager.triggerPinChange(pin, isHigh, 'mcu');
|
||||||
if (this.onPinChangeWithTime && this.rp2040) {
|
if (this.onPinChangeWithTime && this.rp2040) {
|
||||||
// IClock interface exposes `nanos` (not `timeUs`)
|
// IClock interface exposes `nanos` (not `timeUs`)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
* { type: 'serial_output', data: { data: string, uart?: number } }
|
* { type: 'serial_output', data: { data: string, uart?: number } }
|
||||||
* { type: 'gpio_change', data: { pin: number, state: 0|1 } } // linear pin (port*16+pin)
|
* { type: 'gpio_change', data: { pin: number, state: 0|1 } } // linear pin (port*16+pin)
|
||||||
* { type: 'gpio_dir', data: { pin: number, dir: 0|1 } }
|
* { type: 'gpio_dir', data: { pin: number, dir: 0|1 } }
|
||||||
|
* { type: 'gpio_pull', data: { pin: number, pull: 0|1|2 } } // 0=none 1=up 2=down
|
||||||
* { type: 'system', data: { event: string, ... } }
|
* { type: 'system', data: { event: string, ... } }
|
||||||
* { type: 'error', data: { message: string } }
|
* { type: 'error', data: { message: string } }
|
||||||
*
|
*
|
||||||
|
|
@ -82,6 +83,10 @@ export class Stm32Bridge {
|
||||||
onPinChange: ((gpioPin: number, state: boolean) => void) | null = null;
|
onPinChange: ((gpioPin: number, state: boolean) => void) | null = null;
|
||||||
onPinChangeWithTime: ((gpioPin: number, state: boolean, timeMs: number) => void) | null = null;
|
onPinChangeWithTime: ((gpioPin: number, state: boolean, timeMs: number) => void) | null = null;
|
||||||
onPinDir: ((gpioPin: number, dir: 0 | 1) => void) | null = null;
|
onPinDir: ((gpioPin: number, dir: 0 | 1) => void) | null = null;
|
||||||
|
/** Internal pull the guest programmed for an INPUT pin (from PUPDR / CRL+ODR):
|
||||||
|
* 0 = none, 1 = pull-up, 2 = pull-down. gpioPin is the linear pin. The store
|
||||||
|
* records it so the netlist stamps the weak resistor (mirrors ESP32). */
|
||||||
|
onPinPull: ((gpioPin: number, pull: 0 | 1 | 2) => void) | null = null;
|
||||||
onConnected: (() => void) | null = null;
|
onConnected: (() => void) | null = null;
|
||||||
onDisconnected: (() => void) | null = null;
|
onDisconnected: (() => void) | null = null;
|
||||||
onError: ((msg: string) => void) | null = null;
|
onError: ((msg: string) => void) | null = null;
|
||||||
|
|
@ -166,6 +171,10 @@ export class Stm32Bridge {
|
||||||
this.onPinDir?.(msg.data.pin as number, msg.data.dir as 0 | 1);
|
this.onPinDir?.(msg.data.pin as number, msg.data.dir as 0 | 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'gpio_pull': {
|
||||||
|
this.onPinPull?.(msg.data.pin as number, msg.data.pull as 0 | 1 | 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'system': {
|
case 'system': {
|
||||||
const evt = msg.data.event as string;
|
const evt = msg.data.event as string;
|
||||||
if (evt === 'crash') this.onCrash?.(msg.data);
|
if (evt === 'crash') this.onCrash?.(msg.data);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
import { getBoardPinManager } from '../../store/useSimulatorStore';
|
import { getBoardPinManager } from '../../store/useSimulatorStore';
|
||||||
import type { PinSourceState } from './types';
|
import type { PinSourceState } from './types';
|
||||||
import type { BoardKind } from '../../types/board';
|
import type { BoardKind } from '../../types/board';
|
||||||
|
import { isStm32BoardKind } from '../../types/board';
|
||||||
|
import { stm32PinNameToLinear } from '../Stm32Bridge';
|
||||||
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,6 +75,25 @@ export function collectPinStates(
|
||||||
|
|
||||||
const outputPins = pm.getOutputPins();
|
const outputPins = pm.getOutputPins();
|
||||||
|
|
||||||
|
// STM32 names pins PA0/PC13 and keys its PinManager on the linear pin
|
||||||
|
// (port*16+pin). It runs in backend QEMU, where its OUTPUT pins are surfaced
|
||||||
|
// to the canvas via the part layer (not SPICE), so here we only contribute
|
||||||
|
// the INPUT internal pull (reported by the worker's gpio_pull) — enough for
|
||||||
|
// NetlistBuilder to stamp the weak resistor so an INPUT_PULLUP button-to-GND
|
||||||
|
// solves to idle-HIGH / pressed-LOW. connectDigitalInputsToMcu then drives
|
||||||
|
// the guest IDR from the solve. Leaving outputs out keeps STM32 LED rendering
|
||||||
|
// exactly as before.
|
||||||
|
const isStm32 = isStm32BoardKind(boardKind);
|
||||||
|
if (isStm32) {
|
||||||
|
for (const pinName of pinNames) {
|
||||||
|
const linear = stm32PinNameToLinear(pinName);
|
||||||
|
if (linear < 0 || outputPins.has(linear)) continue;
|
||||||
|
const pull = pm.getPinPull(linear);
|
||||||
|
if (pull !== 0) result[pinName] = { type: 'input', pull };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
for (const pinName of pinNames) {
|
for (const pinName of pinNames) {
|
||||||
const arduinoPin = pinNameToArduinoPin(pinName, boardKind);
|
const arduinoPin = pinNameToArduinoPin(pinName, boardKind);
|
||||||
if (arduinoPin < 0) continue;
|
if (arduinoPin < 0) continue;
|
||||||
|
|
|
||||||
|
|
@ -599,6 +599,13 @@ function makePinPullHandler(boardId: string) {
|
||||||
// channel through the `.spi` adapter — identical surface to Esp32BridgeShim,
|
// channel through the `.spi` adapter — identical surface to Esp32BridgeShim,
|
||||||
// minus the ESP32-only WiFi / proxy-resync machinery. ──────────────────────
|
// minus the ESP32-only WiFi / proxy-resync machinery. ──────────────────────
|
||||||
class Stm32BridgeShim {
|
class Stm32BridgeShim {
|
||||||
|
// Drive digital INPUT pins from the solved circuit (connectDigitalInputsToMcu)
|
||||||
|
// instead of the legacy part-seed, so digitalRead() reflects the REAL wiring.
|
||||||
|
// The internal pull is reported by the backend QEMU worker via the bridge's
|
||||||
|
// `gpio_pull` message (wired to makePinPullHandler in the store). Mirrors AVR
|
||||||
|
// / RP2040 / ESP32; event-driven parts with no SPICE model are protected by
|
||||||
|
// the `sourcedNets` gate in the connector.
|
||||||
|
readonly spiceDrivenInputs = true;
|
||||||
pinManager: PinManager;
|
pinManager: PinManager;
|
||||||
onSerialData: ((ch: string) => void) | null = null;
|
onSerialData: ((ch: string) => void) | null = null;
|
||||||
onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;
|
onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;
|
||||||
|
|
@ -1175,6 +1182,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
bridge.onPinChangeWithTime = getOscilloscopeCallback(id);
|
bridge.onPinChangeWithTime = getOscilloscopeCallback(id);
|
||||||
|
// Record the guest's internal pull so NetlistBuilder stamps the weak
|
||||||
|
// resistor; the connector then drives the pin from the solved circuit.
|
||||||
|
bridge.onPinPull = makePinPullHandler(id);
|
||||||
bridge.onDisconnected = () => {
|
bridge.onDisconnected = () => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const boards = s.boards.map((b) => (b.id === id ? { ...b, running: false } : b));
|
const boards = s.boards.map((b) => (b.id === id ? { ...b, running: false } : b));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue