velxio/frontend/src/simulation/RP2040Simulator.ts

185 lines
5.2 KiB
TypeScript
Raw Normal View History

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);
}
}
}