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)
|
||||
_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)
|
||||
_PULL_PIN = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
|
||||
|
||||
|
||||
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_ = [
|
||||
('picsimlab_write_pin', _WRITE_PIN),
|
||||
('picsimlab_dir_pin', _DIR_PIN),
|
||||
|
|
@ -95,6 +99,7 @@ class _CallbacksT(ctypes.Structure):
|
|||
('pinmap', ctypes.c_void_p),
|
||||
('picsimlab_rmt_event', _RMT_EVENT),
|
||||
('picsimlab_gpio_matrix_cb', _GPIO_MATRIX_CB),
|
||||
('picsimlab_pull_pin', _PULL_PIN),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -222,6 +227,16 @@ def main() -> None:
|
|||
return
|
||||
_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):
|
||||
if _stopped.is_set():
|
||||
return
|
||||
|
|
@ -291,6 +306,7 @@ def main() -> None:
|
|||
pinmap = ctypes.cast(_PINMAP, ctypes.c_void_p).value,
|
||||
picsimlab_rmt_event = _RMT_EVENT(_on_rmt_event),
|
||||
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))
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { I2CDevice } from './I2CBusManager';
|
|||
import { bootromB1 } from './rp2040-bootrom';
|
||||
import { loadUF2, loadUserFiles, getFirmware } from './MicroPythonLoader';
|
||||
import { type PioPeripheral, createPioPeripheral } from './PioPeripheral';
|
||||
import { requestElectricalResolve } from './spice/electricalResolveHook';
|
||||
|
||||
/**
|
||||
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
||||
|
|
@ -145,6 +146,14 @@ export class IdleSpinDetector {
|
|||
export type RP2040I2CDevice = I2CDevice;
|
||||
|
||||
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 running = false;
|
||||
private animationFrame: number | null = null;
|
||||
|
|
@ -768,7 +777,30 @@ export class RP2040Simulator {
|
|||
if (!gpio) continue;
|
||||
|
||||
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');
|
||||
if (this.onPinChangeWithTime && this.rp2040) {
|
||||
// IClock interface exposes `nanos` (not `timeUs`)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
* { type: 'serial_output', data: { data: string, uart?: number } }
|
||||
* { 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_pull', data: { pin: number, pull: 0|1|2 } } // 0=none 1=up 2=down
|
||||
* { type: 'system', data: { event: string, ... } }
|
||||
* { type: 'error', data: { message: string } }
|
||||
*
|
||||
|
|
@ -82,6 +83,10 @@ export class Stm32Bridge {
|
|||
onPinChange: ((gpioPin: number, state: boolean) => void) | null = null;
|
||||
onPinChangeWithTime: ((gpioPin: number, state: boolean, timeMs: number) => 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;
|
||||
onDisconnected: (() => 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);
|
||||
break;
|
||||
}
|
||||
case 'gpio_pull': {
|
||||
this.onPinPull?.(msg.data.pin as number, msg.data.pull as 0 | 1 | 2);
|
||||
break;
|
||||
}
|
||||
case 'system': {
|
||||
const evt = msg.data.event as string;
|
||||
if (evt === 'crash') this.onCrash?.(msg.data);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
import { getBoardPinManager } from '../../store/useSimulatorStore';
|
||||
import type { PinSourceState } from './types';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { isStm32BoardKind } from '../../types/board';
|
||||
import { stm32PinNameToLinear } from '../Stm32Bridge';
|
||||
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
||||
|
||||
/**
|
||||
|
|
@ -73,6 +75,25 @@ export function collectPinStates(
|
|||
|
||||
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) {
|
||||
const arduinoPin = pinNameToArduinoPin(pinName, boardKind);
|
||||
if (arduinoPin < 0) continue;
|
||||
|
|
|
|||
|
|
@ -599,6 +599,13 @@ function makePinPullHandler(boardId: string) {
|
|||
// channel through the `.spi` adapter — identical surface to Esp32BridgeShim,
|
||||
// minus the ESP32-only WiFi / proxy-resync machinery. ──────────────────────
|
||||
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;
|
||||
onSerialData: ((ch: string) => 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);
|
||||
// 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 = () => {
|
||||
set((s) => {
|
||||
const boards = s.boards.map((b) => (b.id === id ? { ...b, running: false } : b));
|
||||
|
|
|
|||
Loading…
Reference in New Issue