2026-03-30 07:27:41 +07:00
|
|
|
|
import { RP2040, GPIOPinState, ConsoleLogger, LogLevel, USBCDC } from 'rp2040js';
|
2026-03-06 07:07:03 +07:00
|
|
|
|
import type { RPI2C } from 'rp2040js';
|
2026-03-05 05:28:33 +07:00
|
|
|
|
import { PinManager } from './PinManager';
|
2026-03-06 07:07:03 +07:00
|
|
|
|
import { bootromB1 } from './rp2040-bootrom';
|
2026-03-30 07:27:41 +07:00
|
|
|
|
import { loadUF2, loadUserFiles, getFirmware } from './MicroPythonLoader';
|
2026-03-05 05:28:33 +07:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
|
|
|
|
|
*
|
|
|
|
|
|
* Features:
|
2026-03-06 07:07:03 +07:00
|
|
|
|
* - ARM Cortex-M0+ dual-core Cortex-M0+ CPU at 125 MHz (single-core emulated)
|
2026-04-22 02:45:45 +07:00
|
|
|
|
* - 30 GPIO pins (GPIO0-GPIO29) xc fv nn
|
2026-03-06 07:07:03 +07:00
|
|
|
|
* - 2× UART, 2× SPI, 2× I2C
|
|
|
|
|
|
* - ADC on GPIO26-GPIO29 (A0-A3) + internal temp sensor (ch4)
|
|
|
|
|
|
* - PWM on any GPIO
|
2026-03-05 05:28:33 +07:00
|
|
|
|
* - LED_BUILTIN on GPIO25
|
2026-03-06 07:07:03 +07:00
|
|
|
|
* - Full bootrom B1 for proper boot sequence
|
2026-03-05 05:28:33 +07:00
|
|
|
|
*
|
|
|
|
|
|
* Arduino-pico pin mapping (Earle Philhower's core):
|
|
|
|
|
|
* D0 = GPIO0 … D29 = GPIO29
|
|
|
|
|
|
* A0 = GPIO26 … A3 = GPIO29
|
|
|
|
|
|
* LED_BUILTIN = GPIO25
|
2026-03-06 07:07:03 +07:00
|
|
|
|
* Default Serial → UART0 (GPIO0=TX, GPIO1=RX)
|
|
|
|
|
|
* Default I2C → I2C0 (GPIO4=SDA, GPIO5=SCL)
|
|
|
|
|
|
* Default SPI → SPI0 (GPIO16=MISO, GPIO19=MOSI, GPIO18=SCK, GPIO17=CS)
|
2026-03-05 05:28:33 +07:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const F_CPU = 125_000_000; // 125 MHz
|
|
|
|
|
|
const CYCLE_NANOS = 1e9 / F_CPU; // nanoseconds per cycle (~8 ns)
|
|
|
|
|
|
const FPS = 60;
|
|
|
|
|
|
const CYCLES_PER_FRAME = Math.floor(F_CPU / FPS); // ~2 083 333
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
/** Virtual I2C device interface for RP2040 */
|
|
|
|
|
|
export interface RP2040I2CDevice {
|
|
|
|
|
|
/** 7-bit I2C address */
|
|
|
|
|
|
address: number;
|
|
|
|
|
|
/** Called when master writes a byte */
|
2026-04-22 02:45:45 +07:00
|
|
|
|
writeByte(value: number): boolean; // return true for ACK
|
2026-03-06 07:07:03 +07:00
|
|
|
|
/** Called when master reads a byte */
|
|
|
|
|
|
readByte(): number;
|
|
|
|
|
|
/** Optional: called on STOP condition */
|
|
|
|
|
|
stop?(): void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
|
export class RP2040Simulator {
|
|
|
|
|
|
private rp2040: RP2040 | null = null;
|
|
|
|
|
|
private running = false;
|
|
|
|
|
|
private animationFrame: number | null = null;
|
|
|
|
|
|
public pinManager: PinManager;
|
|
|
|
|
|
private speed = 1.0;
|
|
|
|
|
|
private gpioUnsubscribers: Array<() => void> = [];
|
2026-03-06 07:07:03 +07:00
|
|
|
|
private flashCopy: Uint8Array | null = null;
|
2026-03-21 03:11:12 +07:00
|
|
|
|
private totalCycles = 0;
|
|
|
|
|
|
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
2026-03-22 02:41:20 +07:00
|
|
|
|
private pioStepAccum = 0;
|
2026-03-30 07:27:41 +07:00
|
|
|
|
private usbCDC: USBCDC | null = null;
|
|
|
|
|
|
private micropythonMode = false;
|
2026-03-06 07:07:03 +07:00
|
|
|
|
|
2026-03-30 07:27:41 +07:00
|
|
|
|
/** Serial output callback — fires for each byte the Pico sends on UART0 (or USBCDC in MicroPython mode) */
|
2026-03-06 07:07:03 +07:00
|
|
|
|
public onSerialData: ((char: string) => void) | null = null;
|
|
|
|
|
|
|
2026-03-11 22:09:24 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* Fires for every GPIO pin transition with a millisecond timestamp.
|
|
|
|
|
|
* Used by the oscilloscope / logic analyzer.
|
|
|
|
|
|
* timeMs is derived from the RP2040 cycle counter (cycles / F_CPU * 1000).
|
|
|
|
|
|
*/
|
|
|
|
|
|
public onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
/** I2C virtual devices on each bus */
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private i2cDevices: [Map<number, RP2040I2CDevice>, Map<number, RP2040I2CDevice>] = [
|
|
|
|
|
|
new Map(),
|
|
|
|
|
|
new Map(),
|
|
|
|
|
|
];
|
2026-03-06 07:07:03 +07:00
|
|
|
|
private activeI2CDevice: [RP2040I2CDevice | null, RP2040I2CDevice | null] = [null, null];
|
2026-03-05 05:28:33 +07:00
|
|
|
|
|
|
|
|
|
|
constructor(pinManager: PinManager) {
|
|
|
|
|
|
this.pinManager = pinManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Load a compiled binary into the RP2040 flash memory.
|
|
|
|
|
|
* Accepts a base64-encoded string of the raw .bin file output by arduino-cli.
|
|
|
|
|
|
*/
|
|
|
|
|
|
loadBinary(base64: string): void {
|
|
|
|
|
|
console.log('[RP2040] Loading binary...');
|
|
|
|
|
|
|
|
|
|
|
|
const binaryStr = atob(base64);
|
|
|
|
|
|
const bytes = new Uint8Array(binaryStr.length);
|
|
|
|
|
|
for (let i = 0; i < binaryStr.length; i++) {
|
|
|
|
|
|
bytes[i] = binaryStr.charCodeAt(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[RP2040] Binary size: ${bytes.length} bytes`);
|
2026-03-06 07:07:03 +07:00
|
|
|
|
this.flashCopy = bytes;
|
2026-03-05 05:28:33 +07:00
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
this.initMCU(bytes);
|
|
|
|
|
|
console.log('[RP2040] CPU initialized with bootrom, UART, I2C, SPI, GPIO');
|
2026-03-05 05:28:33 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Same interface as AVRSimulator for store compatibility */
|
2026-03-06 07:07:03 +07:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2026-03-05 05:28:33 +07:00
|
|
|
|
loadHex(_hexContent: string): void {
|
|
|
|
|
|
console.warn('[RP2040] loadHex() called on RP2040Simulator — use loadBinary() instead');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 07:27:41 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* Load MicroPython firmware + user .py files into RP2040 flash.
|
|
|
|
|
|
* Uses USBCDC for serial (REPL) instead of UART.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadMicroPython(
|
|
|
|
|
|
files: Array<{ name: string; content: string }>,
|
|
|
|
|
|
onProgress?: (loaded: number, total: number) => void,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
console.log('[RP2040] Loading MicroPython firmware...');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Get MicroPython UF2 firmware (cached in IndexedDB)
|
|
|
|
|
|
const firmware = await getFirmware(onProgress);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Create fresh RP2040 instance
|
|
|
|
|
|
this.rp2040 = new RP2040();
|
|
|
|
|
|
this.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
|
|
|
|
|
this.rp2040.loadBootrom(bootromB1);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Load UF2 firmware into flash
|
|
|
|
|
|
loadUF2(firmware, this.rp2040.flash);
|
|
|
|
|
|
console.log(`[RP2040] MicroPython UF2 loaded (${firmware.length} bytes)`);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Create LittleFS with user files and load into flash
|
|
|
|
|
|
await loadUserFiles(files, this.rp2040.flash);
|
|
|
|
|
|
console.log(`[RP2040] LittleFS loaded with ${files.length} file(s)`);
|
|
|
|
|
|
|
|
|
|
|
|
// Keep a flash copy for reset
|
|
|
|
|
|
this.flashCopy = new Uint8Array(this.rp2040.flash);
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Set up USBCDC for serial REPL (instead of UART)
|
|
|
|
|
|
this.usbCDC = new USBCDC(this.rp2040.usbCtrl);
|
|
|
|
|
|
this.usbCDC.onDeviceConnected = () => {
|
|
|
|
|
|
// Send newline to trigger the REPL prompt
|
|
|
|
|
|
this.usbCDC!.sendSerialByte('\r'.charCodeAt(0));
|
|
|
|
|
|
this.usbCDC!.sendSerialByte('\n'.charCodeAt(0));
|
|
|
|
|
|
};
|
|
|
|
|
|
this.usbCDC.onSerialData = (buffer: Uint8Array) => {
|
|
|
|
|
|
for (const byte of buffer) {
|
|
|
|
|
|
if (this.onSerialData) {
|
|
|
|
|
|
this.onSerialData(String.fromCharCode(byte));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 6. Set PC to flash start
|
|
|
|
|
|
this.rp2040.core.PC = 0x10000000;
|
|
|
|
|
|
|
|
|
|
|
|
// 7. Wire peripherals (I2C, SPI, ADC, PIO, GPIO — same as Arduino mode)
|
|
|
|
|
|
// But skip UART serial wiring since MicroPython uses USBCDC
|
|
|
|
|
|
this.rp2040.uart[1].onByte = (value: number) => {
|
|
|
|
|
|
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
|
|
|
|
|
|
};
|
|
|
|
|
|
this.wireI2C(0);
|
|
|
|
|
|
this.wireI2C(1);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.rp2040.spi[0].onTransmit = (v: number) => {
|
|
|
|
|
|
this.rp2040!.spi[0].completeTransmit(v);
|
|
|
|
|
|
};
|
|
|
|
|
|
this.rp2040.spi[1].onTransmit = (v: number) => {
|
|
|
|
|
|
this.rp2040!.spi[1].completeTransmit(v);
|
|
|
|
|
|
};
|
2026-03-30 07:27:41 +07:00
|
|
|
|
this.rp2040.adc.channelValues[0] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[1] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[2] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[3] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[4] = 876;
|
|
|
|
|
|
|
|
|
|
|
|
// Patch PIO (same as initMCU)
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
for (const pio of (this.rp2040 as any).pio) {
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
pio.run = function (this: any) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
if (this.runTimer) {
|
|
|
|
|
|
clearTimeout(this.runTimer);
|
|
|
|
|
|
this.runTimer = null;
|
|
|
|
|
|
}
|
2026-03-30 07:27:41 +07:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pioStepAccum = 0;
|
|
|
|
|
|
|
|
|
|
|
|
this.setupGpioListeners();
|
|
|
|
|
|
this.micropythonMode = true;
|
|
|
|
|
|
console.log('[RP2040] MicroPython ready');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns true if currently in MicroPython mode */
|
|
|
|
|
|
isMicroPythonMode(): boolean {
|
|
|
|
|
|
return this.micropythonMode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
getADC(): any {
|
|
|
|
|
|
return this.rp2040?.adc ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
/** Get underlying RP2040 instance (for advanced usage / tests) */
|
|
|
|
|
|
getMCU(): RP2040 | null {
|
|
|
|
|
|
return this.rp2040;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Private initialization ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private initMCU(programBytes: Uint8Array): void {
|
|
|
|
|
|
this.rp2040 = new RP2040();
|
|
|
|
|
|
|
|
|
|
|
|
// Suppress noisy internal logs (only show errors)
|
|
|
|
|
|
this.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
|
|
|
|
|
|
|
|
|
|
|
// Load RP2040 B1 bootrom — needed for proper boot sequence
|
|
|
|
|
|
this.rp2040.loadBootrom(bootromB1);
|
|
|
|
|
|
|
|
|
|
|
|
// Load binary into flash starting at offset 0 (maps to 0x10000000)
|
|
|
|
|
|
this.rp2040.flash.set(programBytes, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Set PC to flash start (boot vector)
|
|
|
|
|
|
this.rp2040.core.PC = 0x10000000;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Wire UART0 (default Serial port for Arduino-Pico) ────────────
|
2026-03-06 07:07:10 +07:00
|
|
|
|
let serialBuffer = '';
|
2026-03-06 07:07:03 +07:00
|
|
|
|
this.rp2040.uart[0].onByte = (value: number) => {
|
2026-03-06 07:07:10 +07:00
|
|
|
|
const ch = String.fromCharCode(value);
|
|
|
|
|
|
serialBuffer += ch;
|
|
|
|
|
|
if (ch === '\n') {
|
|
|
|
|
|
console.log('[RP2040 UART0]', serialBuffer.trimEnd());
|
|
|
|
|
|
serialBuffer = '';
|
|
|
|
|
|
}
|
2026-03-06 07:07:03 +07:00
|
|
|
|
if (this.onSerialData) {
|
2026-03-06 07:07:10 +07:00
|
|
|
|
this.onSerialData(ch);
|
2026-03-06 07:07:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Wire UART1 (Serial1) — also forward to onSerialData for now ──
|
|
|
|
|
|
this.rp2040.uart[1].onByte = (value: number) => {
|
|
|
|
|
|
if (this.onSerialData) {
|
|
|
|
|
|
this.onSerialData(String.fromCharCode(value));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Wire I2C0 and I2C1 ───────────────────────────────────────────
|
|
|
|
|
|
this.wireI2C(0);
|
|
|
|
|
|
this.wireI2C(1);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Wire SPI0 and SPI1 — default loopback ────────────────────────
|
|
|
|
|
|
this.rp2040.spi[0].onTransmit = (value: number) => {
|
|
|
|
|
|
this.rp2040!.spi[0].completeTransmit(value); // loopback
|
|
|
|
|
|
};
|
|
|
|
|
|
this.rp2040.spi[1].onTransmit = (value: number) => {
|
|
|
|
|
|
this.rp2040!.spi[1].completeTransmit(value); // loopback
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Set default ADC values ───────────────────────────────────────
|
|
|
|
|
|
// Channel 0-3: GPIO26-29, channel 4: internal temp sensor
|
|
|
|
|
|
// Default to mid-range (~1.65V on 3.3V ref, 12-bit)
|
|
|
|
|
|
this.rp2040.adc.channelValues[0] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[1] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[2] = 2048;
|
|
|
|
|
|
this.rp2040.adc.channelValues[3] = 2048;
|
|
|
|
|
|
// Internal temp sensor: T = 27 - (V - 0.706) / 0.001721
|
|
|
|
|
|
// For 27°C: V = 0.706V → ADC = 0.706/3.3 * 4095 ≈ 876
|
|
|
|
|
|
this.rp2040.adc.channelValues[4] = 876;
|
|
|
|
|
|
|
2026-03-22 02:41:20 +07:00
|
|
|
|
// ── Patch PIO to use synchronous stepping instead of setTimeout ──
|
|
|
|
|
|
// rp2040js PIO uses setTimeout(() => this.run(), 0) which deadlocks
|
|
|
|
|
|
// when the CPU busy-waits for PIO FIFO space (e.g. pio_sm_put_blocking).
|
|
|
|
|
|
// We step PIO synchronously in the execute loop instead.
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
for (const pio of (this.rp2040 as any).pio) {
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
pio.run = function (this: any) {
|
|
|
|
|
|
if (this.runTimer) {
|
|
|
|
|
|
clearTimeout(this.runTimer);
|
|
|
|
|
|
this.runTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
// No-op: execute loop calls pio.step() synchronously
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pioStepAccum = 0;
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
// ── Set up GPIO listeners ────────────────────────────────────────
|
|
|
|
|
|
this.setupGpioListeners();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private wireI2C(bus: 0 | 1): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
const i2c: RPI2C = this.rp2040.i2c[bus];
|
|
|
|
|
|
const devices = this.i2cDevices[bus];
|
|
|
|
|
|
i2c.onStart = () => {
|
|
|
|
|
|
i2c.completeStart();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
i2c.onConnect = (address: number) => {
|
|
|
|
|
|
const device = devices.get(address);
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
this.activeI2CDevice[bus] = device;
|
|
|
|
|
|
i2c.completeConnect(true); // ACK
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.activeI2CDevice[bus] = null;
|
|
|
|
|
|
i2c.completeConnect(false); // NACK
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
i2c.onWriteByte = (value: number) => {
|
|
|
|
|
|
const dev = this.activeI2CDevice[bus];
|
|
|
|
|
|
if (dev) {
|
|
|
|
|
|
const ack = dev.writeByte(value);
|
|
|
|
|
|
i2c.completeWrite(ack);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
i2c.completeWrite(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
i2c.onReadByte = () => {
|
|
|
|
|
|
const dev = this.activeI2CDevice[bus];
|
|
|
|
|
|
if (dev) {
|
|
|
|
|
|
i2c.completeRead(dev.readByte());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
i2c.completeRead(0xff);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
i2c.onStop = () => {
|
|
|
|
|
|
const dev = this.activeI2CDevice[bus];
|
|
|
|
|
|
if (dev?.stop) dev.stop();
|
|
|
|
|
|
this.activeI2CDevice[bus] = null;
|
|
|
|
|
|
i2c.completeStop();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
|
private setupGpioListeners(): void {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.gpioUnsubscribers.forEach((fn) => fn());
|
2026-03-05 05:28:33 +07:00
|
|
|
|
this.gpioUnsubscribers = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
|
|
|
|
|
|
for (let gpioIdx = 0; gpioIdx < 30; gpioIdx++) {
|
|
|
|
|
|
const pin = gpioIdx;
|
|
|
|
|
|
const gpio = this.rp2040.gpio[gpioIdx];
|
|
|
|
|
|
if (!gpio) continue;
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
const unsub = gpio.addListener((state: GPIOPinState) => {
|
2026-03-23 04:46:52 +07:00
|
|
|
|
const isHigh = state === GPIOPinState.High || state === GPIOPinState.InputPullUp;
|
2026-03-05 05:28:33 +07:00
|
|
|
|
this.pinManager.triggerPinChange(pin, isHigh);
|
2026-03-11 22:09:24 +07:00
|
|
|
|
if (this.onPinChangeWithTime && this.rp2040) {
|
2026-03-22 00:02:46 +07:00
|
|
|
|
// IClock interface exposes `nanos` (not `timeUs`)
|
2026-03-11 22:09:24 +07:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
const clk = (this.rp2040 as any).clock;
|
2026-03-22 00:02:46 +07:00
|
|
|
|
const timeMs = clk ? (clk.nanos as number) / 1_000_000 : 0;
|
2026-03-11 22:09:24 +07:00
|
|
|
|
this.onPinChangeWithTime(pin, isHigh, timeMs);
|
|
|
|
|
|
}
|
2026-03-05 05:28:33 +07:00
|
|
|
|
});
|
|
|
|
|
|
this.gpioUnsubscribers.push(unsub);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
// ── Public API ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
|
start(): void {
|
|
|
|
|
|
if (this.running || !this.rp2040) {
|
|
|
|
|
|
console.warn('[RP2040] Already running or not initialized');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.running = true;
|
|
|
|
|
|
console.log('[RP2040] Starting simulation at 125 MHz...');
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
const execute = () => {
|
2026-03-05 05:28:33 +07:00
|
|
|
|
if (!this.running || !this.rp2040) return;
|
|
|
|
|
|
|
|
|
|
|
|
const cyclesTarget = Math.floor(CYCLES_PER_FRAME * this.speed);
|
|
|
|
|
|
const { core } = this.rp2040;
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
const clock = (this.rp2040 as any).clock;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let cyclesDone = 0;
|
2026-03-22 02:41:20 +07:00
|
|
|
|
const pioDiv = this.getPIOClockDiv();
|
2026-03-05 05:28:33 +07:00
|
|
|
|
while (cyclesDone < cyclesTarget) {
|
|
|
|
|
|
if (core.waiting) {
|
|
|
|
|
|
if (clock) {
|
|
|
|
|
|
const jump: number = clock.nanosToNextAlarm;
|
2026-03-22 02:41:20 +07:00
|
|
|
|
if (jump <= 0) {
|
|
|
|
|
|
// No clock alarms — step PIO so it can unblock the CPU
|
|
|
|
|
|
// (e.g. PIO consuming FIFO data may generate an interrupt)
|
|
|
|
|
|
this.stepPIO();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-21 03:11:12 +07:00
|
|
|
|
const jumped = Math.ceil(jump / CYCLE_NANOS);
|
2026-03-22 02:41:20 +07:00
|
|
|
|
const pioSteps = Math.floor(jumped / pioDiv);
|
2026-03-22 03:12:06 +07:00
|
|
|
|
// Advance clock incrementally per PIO step so GPIO transitions
|
|
|
|
|
|
// get accurate timestamps (not all lumped at the end of the jump).
|
|
|
|
|
|
const nanoPerPioStep = pioDiv * CYCLE_NANOS;
|
|
|
|
|
|
const maxSteps = Math.min(pioSteps, 50000);
|
|
|
|
|
|
let nanosStepped = 0;
|
|
|
|
|
|
for (let i = 0; i < maxSteps; i++) {
|
|
|
|
|
|
clock.tick(nanoPerPioStep);
|
|
|
|
|
|
nanosStepped += nanoPerPioStep;
|
|
|
|
|
|
this.totalCycles += pioDiv;
|
|
|
|
|
|
this.stepPIO();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Tick any remaining nanoseconds not covered by PIO steps
|
|
|
|
|
|
const remaining = jump - nanosStepped;
|
|
|
|
|
|
if (remaining > 0) {
|
|
|
|
|
|
clock.tick(remaining);
|
|
|
|
|
|
this.totalCycles += Math.ceil(remaining / CYCLE_NANOS);
|
|
|
|
|
|
}
|
|
|
|
|
|
cyclesDone += jumped;
|
2026-03-21 03:11:12 +07:00
|
|
|
|
this.flushScheduledPinChanges();
|
2026-03-05 05:28:33 +07:00
|
|
|
|
} else {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const cycles: number = core.executeInstruction();
|
|
|
|
|
|
if (clock) clock.tick(cycles * CYCLE_NANOS);
|
|
|
|
|
|
cyclesDone += cycles;
|
2026-03-21 03:11:12 +07:00
|
|
|
|
this.totalCycles += cycles;
|
2026-03-22 02:41:20 +07:00
|
|
|
|
// Step PIO synchronously at the PIO clock rate
|
|
|
|
|
|
this.pioStepAccum += cycles;
|
2026-03-22 03:12:06 +07:00
|
|
|
|
while (this.pioStepAccum >= pioDiv) {
|
2026-03-22 02:41:20 +07:00
|
|
|
|
this.pioStepAccum -= pioDiv;
|
|
|
|
|
|
this.stepPIO();
|
|
|
|
|
|
}
|
2026-03-21 03:11:12 +07:00
|
|
|
|
this.flushScheduledPinChanges();
|
2026-03-05 05:28:33 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[RP2040] Simulation error:', error);
|
|
|
|
|
|
this.stop();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.animationFrame = requestAnimationFrame(execute);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.animationFrame = requestAnimationFrame(execute);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
|
|
if (!this.running) return;
|
|
|
|
|
|
this.running = false;
|
|
|
|
|
|
if (this.animationFrame !== null) {
|
|
|
|
|
|
cancelAnimationFrame(this.animationFrame);
|
|
|
|
|
|
this.animationFrame = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('[RP2040] Simulation stopped');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reset(): void {
|
|
|
|
|
|
this.stop();
|
2026-03-21 03:11:12 +07:00
|
|
|
|
this.totalCycles = 0;
|
|
|
|
|
|
this.scheduledPinChanges = [];
|
2026-03-06 07:07:03 +07:00
|
|
|
|
if (this.rp2040 && this.flashCopy) {
|
2026-03-30 07:27:41 +07:00
|
|
|
|
if (this.micropythonMode) {
|
|
|
|
|
|
// In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS)
|
|
|
|
|
|
this.rp2040 = new RP2040();
|
|
|
|
|
|
this.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
|
|
|
|
|
this.rp2040.loadBootrom(bootromB1);
|
|
|
|
|
|
this.rp2040.flash.set(this.flashCopy);
|
|
|
|
|
|
this.rp2040.core.PC = 0x10000000;
|
|
|
|
|
|
|
|
|
|
|
|
// Re-wire USBCDC
|
|
|
|
|
|
this.usbCDC = new USBCDC(this.rp2040.usbCtrl);
|
|
|
|
|
|
this.usbCDC.onDeviceConnected = () => {
|
|
|
|
|
|
this.usbCDC!.sendSerialByte('\r'.charCodeAt(0));
|
|
|
|
|
|
this.usbCDC!.sendSerialByte('\n'.charCodeAt(0));
|
|
|
|
|
|
};
|
|
|
|
|
|
this.usbCDC.onSerialData = (buffer: Uint8Array) => {
|
|
|
|
|
|
for (const byte of buffer) {
|
|
|
|
|
|
if (this.onSerialData) this.onSerialData(String.fromCharCode(byte));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Re-wire peripherals (skipping UART0 serial)
|
|
|
|
|
|
this.rp2040.uart[1].onByte = (value: number) => {
|
|
|
|
|
|
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
|
|
|
|
|
|
};
|
|
|
|
|
|
this.wireI2C(0);
|
|
|
|
|
|
this.wireI2C(1);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.rp2040.spi[0].onTransmit = (v: number) => {
|
|
|
|
|
|
this.rp2040!.spi[0].completeTransmit(v);
|
|
|
|
|
|
};
|
|
|
|
|
|
this.rp2040.spi[1].onTransmit = (v: number) => {
|
|
|
|
|
|
this.rp2040!.spi[1].completeTransmit(v);
|
|
|
|
|
|
};
|
2026-03-30 07:27:41 +07:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
for (const pio of (this.rp2040 as any).pio) {
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
pio.run = function (this: any) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
if (this.runTimer) {
|
|
|
|
|
|
clearTimeout(this.runTimer);
|
|
|
|
|
|
this.runTimer = null;
|
|
|
|
|
|
}
|
2026-03-30 07:27:41 +07:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pioStepAccum = 0;
|
|
|
|
|
|
this.setupGpioListeners();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.initMCU(this.flashCopy);
|
|
|
|
|
|
}
|
2026-03-05 05:28:33 +07:00
|
|
|
|
console.log('[RP2040] CPU reset');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isRunning(): boolean {
|
|
|
|
|
|
return this.running;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSpeed(speed: number): void {
|
|
|
|
|
|
this.speed = Math.max(0.1, Math.min(10.0, speed));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 07:07:03 +07:00
|
|
|
|
getSpeed(): number {
|
|
|
|
|
|
return this.speed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 03:11:12 +07:00
|
|
|
|
/** Returns the CPU clock frequency in Hz. */
|
|
|
|
|
|
getClockHz(): number {
|
|
|
|
|
|
return F_CPU;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns total CPU cycles executed since last reset/load. */
|
|
|
|
|
|
getCurrentCycles(): number {
|
|
|
|
|
|
return this.totalCycles;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Schedule a GPIO pin state change at a specific future cycle count.
|
|
|
|
|
|
* Enables cycle-accurate protocol simulation (e.g. HC-SR04 echo timing).
|
|
|
|
|
|
*/
|
|
|
|
|
|
schedulePinChange(pin: number, state: boolean, atCycle: number): void {
|
|
|
|
|
|
let i = this.scheduledPinChanges.length;
|
|
|
|
|
|
while (i > 0 && this.scheduledPinChanges[i - 1].cycle > atCycle) i--;
|
|
|
|
|
|
this.scheduledPinChanges.splice(i, 0, { cycle: atCycle, pin, state });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 02:41:20 +07:00
|
|
|
|
/** Get the PIO clock divider from the first enabled state machine. */
|
|
|
|
|
|
private getPIOClockDiv(): number {
|
|
|
|
|
|
if (!this.rp2040) return 64;
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
for (const pio of (this.rp2040 as any).pio) {
|
|
|
|
|
|
if (pio.stopped) continue;
|
|
|
|
|
|
for (const m of pio.machines) {
|
|
|
|
|
|
if (m.enabled) {
|
|
|
|
|
|
return Math.max(1, m.clockDivInt || 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 64; // default
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Step PIO state machines synchronously (prevents setTimeout deadlock). */
|
|
|
|
|
|
private stepPIO(): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
const pio = (this.rp2040 as any).pio;
|
|
|
|
|
|
if (pio[0] && !pio[0].stopped) pio[0].step();
|
|
|
|
|
|
if (pio[1] && !pio[1].stopped) pio[1].step();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 03:11:12 +07:00
|
|
|
|
private flushScheduledPinChanges(): void {
|
|
|
|
|
|
if (this.scheduledPinChanges.length === 0) return;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
while (
|
|
|
|
|
|
this.scheduledPinChanges.length > 0 &&
|
|
|
|
|
|
this.scheduledPinChanges[0].cycle <= this.totalCycles
|
|
|
|
|
|
) {
|
2026-03-21 03:11:12 +07:00
|
|
|
|
const { pin, state } = this.scheduledPinChanges.shift()!;
|
|
|
|
|
|
this.setPinState(pin, state);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* Drive a GPIO pin externally (e.g. from a button or slider).
|
|
|
|
|
|
* GPIO n = Arduino D(n) for Raspberry Pi Pico.
|
|
|
|
|
|
*/
|
|
|
|
|
|
setPinState(arduinoPin: number, state: boolean): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
const gpio = this.rp2040.gpio[arduinoPin];
|
|
|
|
|
|
if (gpio) {
|
|
|
|
|
|
gpio.setInputValue(state);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-06 07:07:03 +07:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-30 07:27:41 +07:00
|
|
|
|
* Send text to UART0 RX (or USBCDC in MicroPython mode).
|
2026-03-06 07:07:03 +07:00
|
|
|
|
*/
|
|
|
|
|
|
serialWrite(text: string): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
2026-03-30 07:27:41 +07:00
|
|
|
|
if (this.micropythonMode && this.usbCDC) {
|
|
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
|
|
|
|
this.usbCDC.sendSerialByte(text.charCodeAt(i));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
|
|
|
|
this.rp2040.uart[0].feedByte(text.charCodeAt(i));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Send a raw byte to the serial interface (for control characters like Ctrl+C).
|
|
|
|
|
|
*/
|
|
|
|
|
|
serialWriteByte(byte: number): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
if (this.micropythonMode && this.usbCDC) {
|
|
|
|
|
|
this.usbCDC.sendSerialByte(byte);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.rp2040.uart[0].feedByte(byte);
|
2026-03-06 07:07:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Register a virtual I2C device on the specified bus (0 or 1).
|
|
|
|
|
|
* Default bus 0 = Wire, bus 1 = Wire1.
|
|
|
|
|
|
*/
|
|
|
|
|
|
addI2CDevice(device: RP2040I2CDevice, bus: 0 | 1 = 0): void {
|
|
|
|
|
|
this.i2cDevices[bus].set(device.address, device);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Remove an I2C device by address.
|
|
|
|
|
|
*/
|
|
|
|
|
|
removeI2CDevice(address: number, bus: 0 | 1 = 0): void {
|
|
|
|
|
|
this.i2cDevices[bus].delete(address);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Set ADC channel value (0-4095 for 12-bit).
|
|
|
|
|
|
* Channels 0-3 = GPIO26-29, channel 4 = internal temperature sensor.
|
|
|
|
|
|
*/
|
|
|
|
|
|
setADCValue(channel: number, value: number): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
if (channel >= 0 && channel < 5) {
|
|
|
|
|
|
this.rp2040.adc.channelValues[channel] = Math.max(0, Math.min(4095, value));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Set SPI onTransmit handler for a bus (0 or 1).
|
|
|
|
|
|
* callback receives TX byte and must call completeTransmit on the SPI instance.
|
|
|
|
|
|
*/
|
|
|
|
|
|
setSPIHandler(bus: 0 | 1, handler: (value: number) => number): void {
|
|
|
|
|
|
if (!this.rp2040) return;
|
|
|
|
|
|
const spi = this.rp2040.spi[bus];
|
|
|
|
|
|
spi.onTransmit = (value: number) => {
|
|
|
|
|
|
const response = handler(value);
|
|
|
|
|
|
spi.completeTransmit(response);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-03-23 04:03:17 +07:00
|
|
|
|
|
|
|
|
|
|
// ── Generic sensor registration (board-agnostic API) ──────────────────────
|
|
|
|
|
|
// RP2040 handles all sensor protocols locally via schedulePinChange,
|
|
|
|
|
|
// so these return false / no-op — the sensor runs its own frontend logic.
|
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
registerSensor(_type: string, _pin: number, _props: Record<string, unknown>): boolean {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-23 04:03:17 +07:00
|
|
|
|
updateSensor(_pin: number, _props: Record<string, unknown>): void {}
|
|
|
|
|
|
unregisterSensor(_pin: number): void {}
|
2026-03-05 05:28:33 +07:00
|
|
|
|
}
|