velxio/frontend/src/simulation/RiscVSimulator.ts

210 lines
6.8 KiB
TypeScript
Raw Normal View History

/**
* RiscVSimulator CH32V003-compatible RV32I simulator wrapper.
*
* Wraps RiscVCore with:
* - requestAnimationFrame execution loop (~48 MHz @ 60 fps)
* - CH32V003 MMIO: UART1 (0x40013800), GPIO A/C/D (0x40010800/0x40010C00/0x40011400)
* - Intel HEX loader (flash @ 0x08000000, RAM @ 0x20000000)
* - Serial I/O and pin-change callbacks matching AVRSimulator interface
*/
import { RiscVCore } from './RiscVCore';
import { PinManager } from './PinManager';
import { hexToUint8Array } from '../utils/hexParser';
// CH32V003 memory map
const FLASH_BASE = 0x08000000;
const RAM_BASE = 0x20000000;
const FLASH_SIZE = 16 * 1024; // 16 KB
const RAM_SIZE = 2 * 1024; // 2 KB
// Combined flat buffer: flash first, then RAM
const MEM_SIZE = FLASH_SIZE + RAM_SIZE;
// CH32V003 clock
const CPU_HZ = 48_000_000;
const CYCLES_PER_FRAME = Math.round(CPU_HZ / 60);
// ── CH32V003 UART1 MMIO (0x40013800) ────────────────────────────────────────
// STATR offset 0x00 — status register (bit 7 = TXE, bit 5 = RXNE, bit 6 = TC)
// DATAR offset 0x04 — data register (write = TX, read = RX)
const UART1_BASE = 0x40013800;
const UART1_SIZE = 0x400;
const UART1_STATR = 0x00;
const UART1_DATAR = 0x04;
// ── CH32V003 GPIO MMIO ───────────────────────────────────────────────────────
// Each GPIO bank: CRL=0x00, CRH=0x04, INDR=0x08, OUTDR=0x0C, BSHR=0x10, BCR=0x14, LCKR=0x18
const GPIOA_BASE = 0x40010800;
const GPIOC_BASE = 0x40010c00;
const GPIOD_BASE = 0x40011400;
const GPIO_SIZE = 0x400;
const GPIO_OUTDR = 0x0c; // Output data register
// Pin offsets: PA0-7 → simulator pins 0-7, PC0-7 → 8-15, PD0-7 → 16-23
const GPIO_PIN_OFFSET: Record<number, number> = {
[GPIOA_BASE]: 0,
[GPIOC_BASE]: 8,
[GPIOD_BASE]: 16,
};
export class RiscVSimulator {
private core: RiscVCore;
private running = false;
private animFrameId = 0;
private rxFifo: number[] = [];
private gpioOutdr: Record<number, number> = {
[GPIOA_BASE]: 0,
[GPIOC_BASE]: 0,
[GPIOD_BASE]: 0,
};
public pinManager: PinManager;
public onSerialData: ((ch: string) => void) | null = null;
public onBaudRateChange: ((baud: number) => void) | null = null;
public onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;
constructor(pinManager: PinManager) {
this.pinManager = pinManager;
// Flat memory: flash at offset 0, RAM at offset FLASH_SIZE
const mem = new Uint8Array(MEM_SIZE);
this.core = new RiscVCore(mem, FLASH_BASE);
this._registerUart();
this._registerGpio(GPIOA_BASE);
this._registerGpio(GPIOC_BASE);
this._registerGpio(GPIOD_BASE);
// Map RAM: RiscVCore only covers [FLASH_BASE, FLASH_BASE + MEM_SIZE).
// To make RAM work we extend by adding a second MMIO region that redirects
// to the same flat buffer at offset FLASH_SIZE.
const ramOffset = FLASH_SIZE;
this.core.addMmio(
RAM_BASE,
RAM_SIZE,
(addr) => mem[ramOffset + (addr - RAM_BASE)],
(addr, val) => {
mem[ramOffset + (addr - RAM_BASE)] = val;
},
);
}
// ── MMIO registration ──────────────────────────────────────────────────────
private _registerUart(): void {
this.core.addMmio(
UART1_BASE,
UART1_SIZE,
(addr) => {
const off = addr - UART1_BASE;
if (off === UART1_STATR) {
// TXE (bit 7) always ready; RXNE (bit 5) set when RX FIFO has data
return 0b1000_0000 | (this.rxFifo.length > 0 ? 0b0010_0000 : 0);
}
if (off === UART1_DATAR) {
return this.rxFifo.length > 0 ? this.rxFifo.shift()! : 0;
}
return 0;
},
(addr, val) => {
const off = addr - UART1_BASE;
if (off === UART1_DATAR) {
this.onSerialData?.(String.fromCharCode(val & 0xff));
}
},
);
}
private _registerGpio(base: number): void {
const pinOffset = GPIO_PIN_OFFSET[base];
this.core.addMmio(
base,
GPIO_SIZE,
(addr) => {
const off = addr - base;
if (off === GPIO_OUTDR) return this.gpioOutdr[base];
return 0;
},
(addr, val) => {
const off = addr - base;
if (off === GPIO_OUTDR) {
const prev = this.gpioOutdr[base];
this.gpioOutdr[base] = val;
const changed = prev ^ val;
if (changed) {
const timeMs = (this.core.cycles / CPU_HZ) * 1000;
for (let bit = 0; bit < 8; bit++) {
if (changed & (1 << bit)) {
const pin = pinOffset + bit;
const state = !!(val & (1 << bit));
this.onPinChangeWithTime?.(pin, state, timeMs);
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples End-to-end pipeline fixes uncovered while auditing the /examples gallery. Each bug shipped past green unit + snapshot tests because none of those run firmware + render LEDs. Added scripts/visual-led-test.mjs as a CDP-driven visual harness that loads each example, runs the simulator, samples `wokwi-led.brightness`, and asserts toggle / gradient / initial-off invariants — exits non-zero on any regression. Frontend simulator - PinManager.updatePort: new optional ddrMask param. A pin is added to `outputPins` only if the DDR bit is set, so the PORTx write that enables INPUT_PULLUP (DDR=0, PORT=1) no longer falsely marks the pin as MCU output. AVRSimulator now reads DDRB/C/D (0x24/0x27/0x2A on Uno/Nano, 0x37 on ATtiny85, per-port table on Mega) and forwards it. - AVRSimulator: pass DDR mask alongside every port-listener fire. - BasicParts pushbutton{,-6mm}: seed pin HIGH in attachEvents so `digitalRead()` returns HIGH while idle. avr8js doesn't auto-simulate INPUT_PULLUP — without this the firmware reads LOW from boot and thinks the button is permanently pressed (the "LED is always on, pressing does nothing" UX bug). - connectMcuEdgesToService: suppress synthetic digital edges on pins with active PWM, AND subscribe to onPwmChange to re-tick the netlist on duty changes. Fade-LED now produces a true gradient (6 distinct brightness levels across a fade cycle) instead of a binary 0/full toggle. - CircuitSimulationService.handleMcuEdge: replace single-slot pendingMcuEdge with a per-pin Map. Multiple pins toggling during the same in-flight tick used to overwrite each other; now every pin's most-recent edge replays after the tick. Fixes Traffic-Light RED→ YELLOW→GREEN sequencing. - NetlistBuilder: new sanitizeSpiceId() helper replaces hyphens with underscores in V-source names. ngspice's interactive `alter` command treats `-` as an operator and silently no-ops on hyphenated source names, so mid-simulation MCU pin transitions stopped propagating after the first solve. MixedModeScheduler.onMcuPinChange and CircuitSimulationService self-heal use the same sanitizer so names stay consistent across emit/alter/lookup. Also added a regex-based fallback in step 2 so any board pin matching `GND.\d+` canonicalises to net "0" — ESP32-C3 dev kits expose up to 10 GND pins and the per-board `groundPinNames` list missed several, leaving wires floating instead of grounded. - collectPinStates: emit V-sources only for pins in `outputPins`, not every wired board pin. Leaves INPUT pins (analog sensors on A0, pull-down dividers, etc.) free for the SPICE solver instead of being shorted to 0 V by an ideal MCU V-source. - start.ts: extended __spiceDebug to also expose outputPinsByBoard + nodeVoltages + pinNetMapEntries for the visual harness. - ESP32 / RP2040 / RISC-V / C3 simulators: pass `'mcu'` source flag to triggerPinChange / setPinState so the new outputPins tracking fires on those boards too (was AVR-only before). - useSimulatorStore: stopBoard/resetBoard call pm.resetPinStates() so outputPins clears between runs; Esp32Bridge.onPinChange passes the `'mcu'` flag in all three places it's wired. - types/board.ts: ATtiny85 FQBN `clock=internal16mhz` → `clock=16pll` (ATTinyCore 1.5.2 renamed the option). Backend - esp-idf-template/main/CMakeLists.txt: skip the `-DLED_BUILTIN=2` fallback for esp32c3 and esp32s3 targets. Both variants already define LED_BUILTIN in pins_arduino.h via a self-define macro (`#define LED_BUILTIN LED_BUILTIN` + `static const uint8_t LED_BUILTIN = ...;`). Pre-defining the symbol from the command line expanded the static-const declaration to `static const uint8_t 2 = ...;` — a syntax error that broke every ESP32-C3 / S3 build (`expected unqualified-id before numeric constant`). Examples - examples.ts: bulk-fix 72 wire endpoints that referenced `componentId: 'nano-rp2040'` / `'esp32-c3'` etc. (boards that don't exist on the canvas). Replaced with `'arduino-uno'` (the canvas board-id convention) and converted `D<n>` pin names to `GP<n>` for Pico-style boards. Affects pico-blink, pico-i2c-scanner, pico-i2c-rtc-read, pico-spi-loopback, c3-blink and others. Tests - scripts/visual-led-test.mjs: CDP-driven harness. Default suite covers Blink (single-pin), Button (idle-OFF invariant — catches the INPUT_PULLUP regression), Traffic-Light (multi-pin sequencing), Fade-LED (PWM gradient — ≥3 distinct levels), RGB-LED (≥3 PWM pins driven). Run via `npm --prefix frontend run test:visual` against a Chrome on `:9222` + vite on `:5174` + backend on `:8001`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 07:47:45 +07:00
this.pinManager.setPinState(pin, state, 'mcu');
}
}
}
}
},
);
}
// ── HEX loading ────────────────────────────────────────────────────────────
loadHex(hexContent: string): void {
// Reset flash region
const mem = (this.core as unknown as { mem: Uint8Array }).mem;
mem.fill(0, 0, FLASH_SIZE);
const bytes = hexToUint8Array(hexContent);
const maxCopy = Math.min(bytes.length, FLASH_SIZE);
mem.set(bytes.subarray(0, maxCopy), 0);
this.core.reset(FLASH_BASE);
console.log(`[RiscV] Loaded ${maxCopy} bytes`);
}
// ── Lifecycle ──────────────────────────────────────────────────────────────
start(): void {
if (this.running) return;
this.running = true;
this._loop();
}
stop(): void {
this.running = false;
cancelAnimationFrame(this.animFrameId);
}
reset(): void {
this.stop();
this.rxFifo = [];
this.gpioOutdr = { [GPIOA_BASE]: 0, [GPIOC_BASE]: 0, [GPIOD_BASE]: 0 };
this.core.reset(FLASH_BASE);
}
serialWrite(text: string): void {
for (let i = 0; i < text.length; i++) {
this.rxFifo.push(text.charCodeAt(i));
}
}
setPinState(_pin: number, _state: boolean): void {
// Input pin injection not yet implemented for RISC-V GPIO
}
isRunning(): boolean {
return this.running;
}
// ── Execution loop ─────────────────────────────────────────────────────────
private _loop(): void {
if (!this.running) return;
for (let i = 0; i < CYCLES_PER_FRAME; i++) {
this.core.step();
}
this.animFrameId = requestAnimationFrame(() => this._loop());
}
}