185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
|
|
import { RP2040, GPIOPinState } from 'rp2040js';
|
||
|
|
import { PinManager } from './PinManager';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
||
|
|
*
|
||
|
|
* Features:
|
||
|
|
* - ARM Cortex-M0+ CPU emulation at 125 MHz
|
||
|
|
* - 30 GPIO pins (GPIO0-GPIO29)
|
||
|
|
* - ADC on GPIO26-GPIO29 (A0-A3)
|
||
|
|
* - LED_BUILTIN on GPIO25
|
||
|
|
*
|
||
|
|
* Arduino-pico pin mapping (Earle Philhower's core):
|
||
|
|
* D0 = GPIO0 … D29 = GPIO29
|
||
|
|
* A0 = GPIO26 … A3 = GPIO29
|
||
|
|
* LED_BUILTIN = GPIO25
|
||
|
|
*/
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
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> = [];
|
||
|
|
|
||
|
|
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`);
|
||
|
|
|
||
|
|
this.rp2040 = new RP2040();
|
||
|
|
|
||
|
|
// Load binary into flash starting at offset 0 (maps to 0x10000000)
|
||
|
|
this.rp2040.flash.set(bytes, 0);
|
||
|
|
|
||
|
|
// Set up GPIO listeners
|
||
|
|
this.setupGpioListeners();
|
||
|
|
|
||
|
|
console.log('[RP2040] CPU initialized, GPIO listeners attached');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Same interface as AVRSimulator for store compatibility */
|
||
|
|
loadHex(_hexContent: string): void {
|
||
|
|
console.warn('[RP2040] loadHex() called on RP2040Simulator — use loadBinary() instead');
|
||
|
|
}
|
||
|
|
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
getADC(): any {
|
||
|
|
return this.rp2040?.adc ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private setupGpioListeners(): void {
|
||
|
|
this.gpioUnsubscribers.forEach(fn => fn());
|
||
|
|
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;
|
||
|
|
|
||
|
|
const unsub = gpio.addListener((state: GPIOPinState, _oldState: GPIOPinState) => {
|
||
|
|
const isHigh = state === GPIOPinState.High;
|
||
|
|
this.pinManager.triggerPinChange(pin, isHigh);
|
||
|
|
});
|
||
|
|
this.gpioUnsubscribers.push(unsub);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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...');
|
||
|
|
|
||
|
|
let frameCount = 0;
|
||
|
|
const execute = (_timestamp: number) => {
|
||
|
|
if (!this.running || !this.rp2040) return;
|
||
|
|
|
||
|
|
const cyclesTarget = Math.floor(CYCLES_PER_FRAME * this.speed);
|
||
|
|
const { core } = this.rp2040;
|
||
|
|
// Access the internal clock — rp2040js attaches it to the RP2040 instance
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
const clock = (this.rp2040 as any).clock;
|
||
|
|
|
||
|
|
try {
|
||
|
|
let cyclesDone = 0;
|
||
|
|
while (cyclesDone < cyclesTarget) {
|
||
|
|
if (core.waiting) {
|
||
|
|
if (clock) {
|
||
|
|
const jump: number = clock.nanosToNextAlarm;
|
||
|
|
clock.tick(jump);
|
||
|
|
cyclesDone += Math.ceil(jump / CYCLE_NANOS);
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const cycles: number = core.executeInstruction();
|
||
|
|
if (clock) clock.tick(cycles * CYCLE_NANOS);
|
||
|
|
cyclesDone += cycles;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
frameCount++;
|
||
|
|
if (frameCount % 60 === 0) {
|
||
|
|
console.log(`[RP2040] Frame ${frameCount}, PC: 0x${core.PC.toString(16)}`);
|
||
|
|
}
|
||
|
|
} 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();
|
||
|
|
if (this.rp2040) {
|
||
|
|
const flashCopy = new Uint8Array(this.rp2040.flash);
|
||
|
|
this.rp2040 = new RP2040();
|
||
|
|
this.rp2040.flash.set(flashCopy, 0);
|
||
|
|
this.setupGpioListeners();
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|