1458 lines
54 KiB
TypeScript
1458 lines
54 KiB
TypeScript
/**
|
||
* Esp32C3Simulator — Browser-side ESP32-C3 emulator.
|
||
*
|
||
* Wraps RiscVCore (RV32IMC) with:
|
||
* - ESP32-C3 memory map: Flash IROM/DROM @ 0x42000000/0x3C000000,
|
||
* DRAM @ 0x3FC80000, IRAM @ 0x4037C000
|
||
* - UART0 MMIO @ 0x60000000 (serial I/O)
|
||
* - GPIO MMIO @ 0x60004000 (pin output via OUT/W1TS/W1TC registers)
|
||
* - 160 MHz clock, requestAnimationFrame execution loop
|
||
* - Same public interface as AVRSimulator / RiscVSimulator
|
||
*/
|
||
|
||
import { RiscVCore } from './RiscVCore';
|
||
import type { PinManager } from './PinManager';
|
||
import { hexToUint8Array } from '../utils/hexParser';
|
||
import { parseMergedFlashImage } from '../utils/esp32ImageParser';
|
||
|
||
// ── ESP32-C3 Memory Map ──────────────────────────────────────────────────────
|
||
const IROM_BASE = 0x42000000; // Flash instruction region (mapped via MMU)
|
||
const DROM_BASE = 0x3c000000; // Flash data region (read-only alias of same flash)
|
||
const DRAM_BASE = 0x3fc80000; // Data RAM
|
||
const IRAM_BASE = 0x4037c000; // Instruction RAM
|
||
|
||
const IROM_SIZE = 4 * 1024 * 1024; // 4 MB flash buffer
|
||
const DRAM_SIZE = 384 * 1024; // 384 KB DRAM
|
||
const IRAM_SIZE = 384 * 1024; // 384 KB IRAM
|
||
|
||
// ── UART0 @ 0x60000000 ──────────────────────────────────────────────────────
|
||
const UART0_BASE = 0x60000000;
|
||
const UART0_SIZE = 0x400;
|
||
const UART0_FIFO = 0x00; // write TX byte / read RX byte
|
||
const UART0_STATUS = 0x1c; // TXFIFO_CNT in bits [19:16] (0 = empty = ready)
|
||
|
||
// ── GPIO @ 0x60004000 ───────────────────────────────────────────────────────
|
||
const GPIO_BASE = 0x60004000;
|
||
const GPIO_SIZE = 0x200;
|
||
const GPIO_OUT = 0x04; // GPIO_OUT_REG — output value (read/write)
|
||
const GPIO_W1TS = 0x08; // GPIO_OUT_W1TS — set bits (write-only)
|
||
const GPIO_W1TC = 0x0c; // GPIO_OUT_W1TC — clear bits (write-only)
|
||
const GPIO_IN = 0x3c; // GPIO_IN_REG — input value (read-only)
|
||
const GPIO_ENABLE = 0x20; // GPIO_ENABLE_REG
|
||
|
||
// ── SYSTIMER @ 0x60023000 ────────────────────────────────────────────────────
|
||
// The SYSTIMER runs at 16 MHz (CPU_HZ / 10). FreeRTOS programs TARGET0 to
|
||
// fire every 1 ms (16 000 SYSTIMER ticks = 160 000 CPU cycles) and routes the
|
||
// alarm interrupt to CPU interrupt 1 via the interrupt matrix.
|
||
const SYSTIMER_BASE = 0x60023000;
|
||
const SYSTIMER_SIZE = 0x100;
|
||
// Register offsets (ESP32-C3 TRM)
|
||
const ST_INT_ENA = 0x04; // TARGET0/1/2 enable bits
|
||
const ST_INT_RAW = 0x08; // raw interrupt status
|
||
const ST_INT_CLR = 0x0c; // write-1-to-clear
|
||
const ST_INT_ST = 0x10; // masked status (RAW & ENA)
|
||
const ST_UNIT0_OP = 0x14; // write bit30 to snapshot counter
|
||
const ST_UNIT0_VAL_LO = 0x54; // snapshot value low 32 bits
|
||
const ST_UNIT0_VAL_HI = 0x58; // snapshot value high 32 bits
|
||
|
||
// ── SPI Flash Controllers ────────────────────────────────────────────────────
|
||
// SPI1 @ 0x60002000 — direct flash controller (boot-time flash access)
|
||
// SPI0 @ 0x60003000 — cache SPI controller (transparent flash cache)
|
||
// SPI_MEM_CMD_REG offset 0x00 bits [17–31] are "write 1 to start, HW clears when done".
|
||
const SPI1_BASE = 0x60002000;
|
||
const SPI0_BASE = 0x60003000;
|
||
const SPI_SIZE = 0x200;
|
||
const SPI_CMD = 0x00; // SPI_MEM_CMD_REG — command trigger / status
|
||
|
||
// ── EXTMEM (cache controller) @ 0x600C4000 ──────────────────────────────────
|
||
// Manages ICache enable, invalidation, preload, and MMU configuration.
|
||
const EXTMEM_BASE = 0x600c4000;
|
||
const EXTMEM_SIZE = 0x1000;
|
||
// Key register offsets with "done" status bits that must read as 1:
|
||
const EXTMEM_ICACHE_SYNC_CTRL = 0x28; // bit1=SYNC_DONE
|
||
const EXTMEM_ICACHE_PRELOAD_CTRL = 0x34; // bit1=PRELOAD_DONE
|
||
const EXTMEM_ICACHE_AUTOLOAD_CTRL = 0x40; // bit3=AUTOLOAD_DONE
|
||
const EXTMEM_ICACHE_LOCK_CTRL = 0x1c; // bit2=LOCK_DONE
|
||
|
||
// ── Interrupt Matrix @ 0x600C2000 ─────────────────────────────────────────
|
||
// The ESP32-C3 interrupt matrix routes 62 peripheral interrupt sources to
|
||
// up to 31 CPU interrupt lines (line 0 = disabled).
|
||
const INTMATRIX_BASE = 0x600c2000;
|
||
const INTMATRIX_SIZE = 0x800;
|
||
// Register layout (offsets from INTMATRIX_BASE):
|
||
// 0x000-0x0F4 : 62 SOURCE_MAP registers (5-bit mapping: source → CPU line)
|
||
// 0x104 : INTR_STATUS (pending lines bitmap, read-only)
|
||
// 0x108 : CLOCK_GATE (clock gating enable)
|
||
// 0x118-0x194 : PRIORITY for lines 1–31 (4-bit each)
|
||
// 0x198 : THRESH (interrupt threshold, 4-bit)
|
||
|
||
// ── SYSTEM/CLK registers @ 0x600C0000 ─────────────────────────────────────
|
||
// Contains FROM_CPU_INTR software interrupt triggers and misc system config.
|
||
const SYSCON_BASE = 0x600c0000;
|
||
const SYSCON_SIZE = 0x800;
|
||
|
||
// ── Interrupt source numbers (from ESP-IDF soc/esp32c3/interrupts.h) ─────
|
||
const ETS_SYSTIMER_TARGET0_SRC = 37;
|
||
const ETS_FROM_CPU_INTR0_SRC = 28; // FROM_CPU_INTR0..3 → sources 28-31
|
||
|
||
// ── ESP32-C3 ROM stub @ 0x40000000 ──────────────────────────────────────────
|
||
// ROM lives at 0x40000000-0x4001FFFF. Without a ROM image every ROM call
|
||
// fetches 0x0000 → CPU executes reserved C.ADDI4SPN and loops at 0x0.
|
||
// Stub: return C.JR ra (0x8082) so any ROM call immediately returns.
|
||
// Little-endian: even byte = 0x82, odd byte = 0x80.
|
||
const ROM_BASE = 0x40000000;
|
||
const ROM_SIZE = 0x60000; // 0x40000000-0x4005FFFF (first ROM + margin)
|
||
const ROM2_BASE = 0x40800000;
|
||
const ROM2_SIZE = 0x20000; // 0x40800000-0x4081FFFF (second ROM region)
|
||
|
||
// ── Clock ───────────────────────────────────────────────────────────────────
|
||
const CPU_HZ = 160_000_000;
|
||
const CYCLES_PER_FRAME = Math.round(CPU_HZ / 60);
|
||
/** CPU cycles per FreeRTOS tick (1 ms at 160 MHz). */
|
||
const CYCLES_PER_TICK = 160_000;
|
||
|
||
export class Esp32C3Simulator {
|
||
// Opt into connectDigitalInputsToMcu: after every SPICE solve the connector
|
||
// thresholds each input pin's net voltage and pushes the logic level into the
|
||
// GPIO_IN register via setPinState(). So digitalRead() reflects the real
|
||
// wiring (a switch/button tied to 3V3 reads HIGH) instead of a hardcoded part
|
||
// seed. The connector skips MCU-driven output pins and floating (non-sourced)
|
||
// nets, so event-driven parts with no SPICE model keep driving their pins via
|
||
// the part layer. Mirrors AVRSimulator / RP2040Simulator.
|
||
readonly spiceDrivenInputs = true;
|
||
private core: RiscVCore;
|
||
private flash: Uint8Array;
|
||
private dram: Uint8Array;
|
||
private iram: Uint8Array;
|
||
private running = false;
|
||
private animFrameId = 0;
|
||
private rxFifo: number[] = [];
|
||
private gpioOut = 0;
|
||
private gpioIn = 0;
|
||
|
||
// SYSTIMER emulation state
|
||
private _stIntEna = 0; // ST_INT_ENA register
|
||
private _stIntRaw = 0; // ST_INT_RAW register (bit0 = TARGET0 fired)
|
||
|
||
// ── ROM binary data (loaded asynchronously from /boards/esp32c3-rom.bin) ──
|
||
private _romData: Uint8Array | null = null;
|
||
|
||
// ── Interrupt matrix state ────────────────────────────────────────────────
|
||
/** Source→CPU-line mapping (62 sources, each 5-bit → 0-31). */
|
||
private _intSrcMap = new Uint8Array(62);
|
||
/** CPU interrupt line enable bitmap (bit N = line N enabled). */
|
||
private _intLineEnable = 0;
|
||
/** Per-line priority (lines 1–31, 4-bit each). Index 0 unused. */
|
||
private _intLinePrio = new Uint8Array(32);
|
||
/** Interrupt threshold — only lines with priority > threshold can fire. */
|
||
private _intThreshold = 0;
|
||
/** Pending interrupt bitmap (set when source is active but can't fire). */
|
||
private _intPending = 0;
|
||
/** Current level of each interrupt source (1=asserted, 0=deasserted). */
|
||
private _intSrcActive = new Uint8Array(62);
|
||
|
||
/**
|
||
* Shared peripheral register file — echo-back map.
|
||
* Peripheral MMIO writes that aren't handled by specific logic are stored
|
||
* here keyed by word-aligned address so that subsequent reads return the
|
||
* last written value. This makes common "write → read-back → verify"
|
||
* patterns in the ESP-IDF boot succeed without dedicated stubs.
|
||
*/
|
||
private _periRegs = new Map<number, number>();
|
||
|
||
/** CPU ticks per µs — updated by ets_update_cpu_frequency(). */
|
||
private _ticksPerUs = 160;
|
||
|
||
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;
|
||
|
||
// Flash is the primary (fast-path) memory region
|
||
this.flash = new Uint8Array(IROM_SIZE);
|
||
this.dram = new Uint8Array(DRAM_SIZE);
|
||
this.iram = new Uint8Array(IRAM_SIZE);
|
||
|
||
this.core = new RiscVCore(this.flash, IROM_BASE);
|
||
|
||
// DROM — read-only alias of the same flash buffer at a different virtual address
|
||
const flash = this.flash;
|
||
this.core.addMmio(
|
||
DROM_BASE,
|
||
IROM_SIZE,
|
||
(addr) => flash[addr - DROM_BASE] ?? 0,
|
||
() => {},
|
||
);
|
||
|
||
// DRAM (384 KB)
|
||
const dram = this.dram;
|
||
this.core.addMmio(
|
||
DRAM_BASE,
|
||
DRAM_SIZE,
|
||
(addr) => dram[addr - DRAM_BASE],
|
||
(addr, val) => {
|
||
dram[addr - DRAM_BASE] = val;
|
||
},
|
||
);
|
||
|
||
// IRAM (384 KB)
|
||
const iram = this.iram;
|
||
this.core.addMmio(
|
||
IRAM_BASE,
|
||
IRAM_SIZE,
|
||
(addr) => iram[addr - IRAM_BASE],
|
||
(addr, val) => {
|
||
iram[addr - IRAM_BASE] = val;
|
||
},
|
||
);
|
||
|
||
// Broad catch-all for all peripheral space must be registered FIRST (largest
|
||
// region) so that narrower, more specific handlers registered afterwards win
|
||
// via mmioFor's "smallest size wins" rule.
|
||
this._registerPeripheralCatchAll();
|
||
this._registerUart0();
|
||
this._registerGpio();
|
||
this._registerSysTimer();
|
||
this._registerIntMatrix();
|
||
this._registerSysCon();
|
||
this._registerRtcCntl();
|
||
// Timer Groups — stub RTCCALICFG1.cal_done for all known base addresses
|
||
// so rtc_clk_cal_internal() poll loop exits immediately.
|
||
this._registerTimerGroup(0x60026000); // TIMG0 (ESP-IDF v5 / arduino-esp32 3.x)
|
||
this._registerTimerGroup(0x60027000); // TIMG1
|
||
this._registerTimerGroup(0x6001f000); // TIMG0 alternative (older ESP-IDF)
|
||
this._registerTimerGroup(0x60020000); // TIMG1 alternative
|
||
this._registerSpiFlash(SPI1_BASE); // SPI1 — direct flash controller
|
||
this._registerSpiFlash(SPI0_BASE); // SPI0 — cache SPI controller
|
||
this._registerExtMem();
|
||
this._registerRomStub();
|
||
this._registerRomStub2();
|
||
|
||
// Wire MIE transition callback — when firmware re-enables interrupts,
|
||
// scan the interrupt matrix for pending sources and inject them.
|
||
this.core.onMieEnabled = () => this._onMieEnabled();
|
||
|
||
// NOTE: Real ROM binary loading disabled — the ROM code accesses many
|
||
// peripherals we don't fully emulate, causing the CPU to jump to invalid
|
||
// addresses. C.RET stubs returning a0=0 are sufficient for now.
|
||
// this._loadRom();
|
||
|
||
this.core.reset(IROM_BASE);
|
||
// Initialize SP to top of DRAM — MUST be after reset() which zeroes all regs
|
||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||
}
|
||
|
||
// ── MMIO registration ──────────────────────────────────────────────────────
|
||
|
||
private _registerUart0(): void {
|
||
this.core.addMmio(
|
||
UART0_BASE,
|
||
UART0_SIZE,
|
||
(addr) => {
|
||
const off = addr - UART0_BASE;
|
||
if (off === UART0_FIFO) return this.rxFifo.length > 0 ? this.rxFifo.shift()! & 0xff : 0;
|
||
if (off === UART0_STATUS) return 0; // TXFIFO always empty = ready to accept data
|
||
return 0;
|
||
},
|
||
(addr, val) => {
|
||
if (addr - UART0_BASE === UART0_FIFO) {
|
||
this.onSerialData?.(String.fromCharCode(val & 0xff));
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
private _registerGpio(): void {
|
||
this.core.addMmio(
|
||
GPIO_BASE,
|
||
GPIO_SIZE,
|
||
(addr) => {
|
||
const off = (addr - GPIO_BASE) & ~3; // word-align for register lookup
|
||
const byteIdx = (addr - GPIO_BASE) & 3;
|
||
if (off === GPIO_OUT) return (this.gpioOut >> (byteIdx * 8)) & 0xff;
|
||
if (off === GPIO_IN) return (this.gpioIn >> (byteIdx * 8)) & 0xff;
|
||
if (off === GPIO_ENABLE) return 0xff;
|
||
return 0;
|
||
},
|
||
(addr, val) => {
|
||
const off = (addr - GPIO_BASE) & ~3;
|
||
const byteIdx = (addr - GPIO_BASE) & 3;
|
||
const shift = byteIdx * 8;
|
||
const byteMask = 0xff << shift;
|
||
const prev = this.gpioOut;
|
||
|
||
if (off === GPIO_W1TS) {
|
||
// Set bits — each byte write sets corresponding bits
|
||
this.gpioOut |= (val & 0xff) << shift;
|
||
} else if (off === GPIO_W1TC) {
|
||
// Clear bits
|
||
this.gpioOut &= ~((val & 0xff) << shift);
|
||
} else if (off === GPIO_OUT) {
|
||
// Direct write — reconstruct 32-bit value byte by byte
|
||
this.gpioOut = (this.gpioOut & ~byteMask) | ((val & 0xff) << shift);
|
||
}
|
||
|
||
const changed = prev ^ this.gpioOut;
|
||
if (changed) {
|
||
const timeMs = (this.core.cycles / CPU_HZ) * 1000;
|
||
for (let bit = 0; bit < 22; bit++) {
|
||
// ESP32-C3 has GPIO0–GPIO21
|
||
if (changed & (1 << bit)) {
|
||
const state = !!(this.gpioOut & (1 << bit));
|
||
console.log(
|
||
`[ESP32-C3] GPIO${bit} → ${state ? 'HIGH' : 'LOW'} @ ${timeMs.toFixed(1)}ms`,
|
||
);
|
||
this.onPinChangeWithTime?.(bit, state, timeMs);
|
||
this.pinManager.setPinState(bit, state, 'mcu');
|
||
}
|
||
}
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
private _registerSysTimer(): void {
|
||
const peri = this._periRegs;
|
||
this.core.addMmio(
|
||
SYSTIMER_BASE,
|
||
SYSTIMER_SIZE,
|
||
(addr) => {
|
||
const off = addr - SYSTIMER_BASE;
|
||
const wordOff = off & ~3;
|
||
const byteIdx = off & 3;
|
||
let word = 0;
|
||
let handled = true;
|
||
switch (wordOff) {
|
||
case ST_INT_ENA:
|
||
word = this._stIntEna;
|
||
break;
|
||
case ST_INT_RAW:
|
||
word = this._stIntRaw;
|
||
break;
|
||
case ST_INT_ST:
|
||
word = this._stIntRaw & this._stIntEna;
|
||
break;
|
||
case ST_UNIT0_OP:
|
||
word = 1 << 29;
|
||
break; // VALID bit always set
|
||
case ST_UNIT0_VAL_LO:
|
||
word = (this.core.cycles / 10) >>> 0;
|
||
break;
|
||
case ST_UNIT0_VAL_HI:
|
||
word = 0;
|
||
break;
|
||
default:
|
||
handled = false;
|
||
break;
|
||
}
|
||
if (!handled) {
|
||
// Echo last written value for unknown offsets
|
||
const wordAddr = addr & ~3;
|
||
word = peri.get(wordAddr) ?? 0;
|
||
}
|
||
return (word >> (byteIdx * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const off = addr - SYSTIMER_BASE;
|
||
const wordOff = off & ~3;
|
||
const shift = (off & 3) * 8;
|
||
switch (wordOff) {
|
||
case ST_INT_ENA:
|
||
this._stIntEna = (this._stIntEna & ~(0xff << shift)) | ((val & 0xff) << shift);
|
||
break;
|
||
case ST_INT_CLR:
|
||
this._stIntRaw &= ~((val & 0xff) << shift);
|
||
// If TARGET0 was cleared, deassert the interrupt source
|
||
if (!(this._stIntRaw & 1)) {
|
||
this._lowerIntSource(ETS_SYSTIMER_TARGET0_SRC);
|
||
}
|
||
break;
|
||
default: {
|
||
// Echo-back: store the written value
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
peri.set(wordAddr, (prev & ~(0xff << shift)) | ((val & 0xff) << shift));
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
// ── Async ROM binary loader ──────────────────────────────────────────────
|
||
|
||
// @ts-expect-error kept for future use when more peripherals are emulated
|
||
private async _loadRom(): Promise<void> {
|
||
try {
|
||
const resp = await fetch('/boards/esp32c3-rom.bin');
|
||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||
const buf = await resp.arrayBuffer();
|
||
this._romData = new Uint8Array(buf);
|
||
console.log(`[ESP32-C3] ROM binary loaded (${this._romData.length} bytes)`);
|
||
} catch (e) {
|
||
console.warn('[ESP32-C3] Failed to load ROM binary, using C.RET stub:', e);
|
||
}
|
||
}
|
||
|
||
// ── Interrupt matrix ───────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Interrupt matrix (0x600C2000).
|
||
*
|
||
* 62 SOURCE_MAP registers route peripheral interrupt sources to CPU lines.
|
||
* The ENABLE bitmap, per-line PRIORITY, and THRESHOLD control which
|
||
* interrupts can fire. Reads/writes are echo-backed via _periRegs and
|
||
* internal state is updated on every write.
|
||
*/
|
||
private _registerIntMatrix(): void {
|
||
const peri = this._periRegs;
|
||
const BASE = INTMATRIX_BASE;
|
||
|
||
this.core.addMmio(
|
||
BASE,
|
||
INTMATRIX_SIZE,
|
||
(addr) => {
|
||
const off = (addr - BASE) & ~3;
|
||
const byteIdx = (addr - BASE) & 3;
|
||
let word = 0;
|
||
|
||
if (off <= 0x0f8) {
|
||
// SOURCE_MAP[0..62] (offsets 0x000-0x0F8)
|
||
const src = off >> 2;
|
||
word = src < 62 ? this._intSrcMap[src] & 0x1f : 0;
|
||
} else if (off === 0x104) {
|
||
// CPU_INT_ENABLE — which CPU interrupt lines are enabled (R/W)
|
||
word = this._intLineEnable;
|
||
} else if (off === 0x108) {
|
||
// CPU_INT_TYPE — edge/level per line (echo-back)
|
||
word = peri.get(addr & ~3) ?? 0;
|
||
} else if (off === 0x10c) {
|
||
// CPU_INT_EIP_STATUS — which lines have pending interrupts (read-only)
|
||
word = this._intPending;
|
||
} else if (off >= 0x114 && off <= 0x190) {
|
||
// CPU_INT_PRI_0..31 (offsets 0x114 + line*4)
|
||
const line = (off - 0x114) >> 2;
|
||
word = line < 32 ? this._intLinePrio[line] : 0;
|
||
} else if (off === 0x194) {
|
||
// CPU_INT_THRESH
|
||
word = this._intThreshold;
|
||
} else {
|
||
word = peri.get(addr & ~3) ?? 0;
|
||
}
|
||
return (word >>> (byteIdx * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
// Always store for echo-back
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
const newWord = (prev & ~(0xff << shift)) | ((val & 0xff) << shift);
|
||
peri.set(wordAddr, newWord);
|
||
|
||
// Update internal state from accumulated word
|
||
const off = wordAddr - BASE;
|
||
if (off <= 0x0f8) {
|
||
const src = off >> 2;
|
||
if (src < 62) {
|
||
this._intSrcMap[src] = newWord & 0x1f;
|
||
}
|
||
} else if (off === 0x104) {
|
||
this._intLineEnable = newWord;
|
||
} else if (off >= 0x114 && off <= 0x190) {
|
||
const line = (off - 0x114) >> 2;
|
||
if (line < 32) this._intLinePrio[line] = newWord & 0xf;
|
||
} else if (off === 0x194) {
|
||
this._intThreshold = newWord & 0xf;
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
/**
|
||
* SYSTEM/CLK registers (0x600C0000).
|
||
*
|
||
* Provides FROM_CPU_INTR software interrupt triggers (FreeRTOS uses these
|
||
* for cross-core signalling / context switch on single-core C3) and a
|
||
* random-number register.
|
||
*/
|
||
private _registerSysCon(): void {
|
||
const peri = this._periRegs;
|
||
const BASE = SYSCON_BASE;
|
||
|
||
this.core.addMmio(
|
||
BASE,
|
||
SYSCON_SIZE,
|
||
(addr) => {
|
||
const wordAddr = addr & ~3;
|
||
const byteIdx = (addr - BASE) & 3;
|
||
const word = peri.get(wordAddr) ?? 0;
|
||
return (word >>> (byteIdx * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
const newWord = (prev & ~(0xff << shift)) | ((val & 0xff) << shift);
|
||
peri.set(wordAddr, newWord);
|
||
|
||
// FROM_CPU_INTR: offsets 0x028, 0x02C, 0x030, 0x034
|
||
const off = wordAddr - BASE;
|
||
if (off >= 0x028 && off <= 0x034) {
|
||
const idx = (off - 0x028) >> 2; // 0–3
|
||
const src = ETS_FROM_CPU_INTR0_SRC + idx; // sources 28–31
|
||
if (newWord & 1) this._raiseIntSource(src);
|
||
else this._lowerIntSource(src);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
// ── Interrupt matrix dispatch helpers ──────────────────────────────────────
|
||
|
||
/**
|
||
* Assert a peripheral interrupt source. Looks up its CPU line via the
|
||
* source-map and either fires the interrupt (if MIE is set and priority
|
||
* meets threshold) or marks it pending.
|
||
*/
|
||
private _raiseIntSource(src: number): void {
|
||
if (src >= 62) return;
|
||
this._intSrcActive[src] = 1;
|
||
const line = this._intSrcMap[src] & 0x1f;
|
||
if (line === 0) return; // line 0 = disabled / not routed
|
||
|
||
// Mark pending for this line
|
||
this._intPending |= 1 << line;
|
||
|
||
// Can we deliver right now?
|
||
if (!(this._intLineEnable & (1 << line))) return;
|
||
const prio = this._intLinePrio[line];
|
||
if (prio <= this._intThreshold) return;
|
||
if (!(this.core.mstatusVal & 0x8)) return; // MIE not set — stay pending
|
||
|
||
this._intPending &= ~(1 << line);
|
||
this.core.triggerInterrupt(0x80000000 | line);
|
||
}
|
||
|
||
/** Deassert a peripheral interrupt source and clear its pending state. */
|
||
private _lowerIntSource(src: number): void {
|
||
if (src >= 62) return;
|
||
this._intSrcActive[src] = 0;
|
||
const line = this._intSrcMap[src] & 0x1f;
|
||
if (line === 0) return;
|
||
|
||
// Check if any OTHER active source also maps to this line
|
||
let stillActive = false;
|
||
for (let s = 0; s < 62; s++) {
|
||
if (s !== src && this._intSrcActive[s] && (this._intSrcMap[s] & 0x1f) === line) {
|
||
stillActive = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!stillActive) this._intPending &= ~(1 << line);
|
||
}
|
||
|
||
/**
|
||
* Scan pending interrupts and deliver the highest-priority one.
|
||
* Called when mstatus.MIE transitions 0→1 (MRET or CSR write).
|
||
*/
|
||
private _onMieEnabled(): void {
|
||
if (this._intPending === 0) return;
|
||
let bestLine = 0;
|
||
let bestPrio = 0;
|
||
for (let line = 1; line < 32; line++) {
|
||
if (!(this._intPending & (1 << line))) continue;
|
||
if (!(this._intLineEnable & (1 << line))) continue;
|
||
const prio = this._intLinePrio[line];
|
||
if (prio > this._intThreshold && prio > bestPrio) {
|
||
bestPrio = prio;
|
||
bestLine = line;
|
||
}
|
||
}
|
||
if (bestLine > 0) {
|
||
this._intPending &= ~(1 << bestLine);
|
||
this.core.triggerInterrupt(0x80000000 | bestLine);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ROM region (0x40000000-0x4005FFFF) — when the CPU fetches an instruction
|
||
* from a known ROM function address, we emulate it natively in JavaScript
|
||
* (memset, memcpy, __udivdi3, ets_delay_us, etc.), set the return value
|
||
* in a0/a1, and serve C.RET (0x8082) so the CPU returns to the caller.
|
||
* Unknown functions still get a0=0 + C.RET.
|
||
*/
|
||
private _registerRomStub(): void {
|
||
const core = this.core;
|
||
this.core.addMmio(
|
||
ROM_BASE,
|
||
ROM_SIZE,
|
||
(addr) => {
|
||
// If we have the real ROM binary, serve it
|
||
if (this._romData) {
|
||
const off = (addr >>> 0) - ROM_BASE;
|
||
if (off < this._romData.length) return this._romData[off];
|
||
}
|
||
// Detect instruction fetch and emulate known ROM functions
|
||
if ((addr & 1) === 0 && addr >>> 0 === core.pc >>> 0) {
|
||
this._emulateRomFunction(addr >>> 0);
|
||
}
|
||
return (addr & 1) === 0 ? 0x82 : 0x80;
|
||
},
|
||
() => {},
|
||
);
|
||
}
|
||
|
||
/** Second ROM region (0x40800000) — same emulation with fallback a0=0. */
|
||
private _registerRomStub2(): void {
|
||
const core = this.core;
|
||
this.core.addMmio(
|
||
ROM2_BASE,
|
||
ROM2_SIZE,
|
||
(addr) => {
|
||
if ((addr & 1) === 0 && addr >>> 0 === core.pc >>> 0) {
|
||
this._emulateRomFunction(addr >>> 0);
|
||
}
|
||
return (addr & 1) === 0 ? 0x82 : 0x80;
|
||
},
|
||
() => {},
|
||
);
|
||
}
|
||
|
||
// ── ROM function emulation ─────────────────────────────────────────────────
|
||
//
|
||
// The ESP32-C3 firmware links common C library functions (memset, memcpy,
|
||
// __udivdi3, …) and helper functions (ets_delay_us, rtc_get_reset_reason, …)
|
||
// to addresses in the ROM region (0x40000000+). Since we don't run the real
|
||
// ROM binary, we emulate these functions natively in JavaScript by reading
|
||
// arguments from registers a0–a7, performing the operation on emulated
|
||
// memory, and writing the result back to a0(/a1). The caller then hits our
|
||
// C.RET (0x8082) stub and returns normally.
|
||
//
|
||
// Address mappings from esp32c3.rom.ld / esp32c3.rom.libgcc.ld:
|
||
|
||
private _emulateRomFunction(addr: number): void {
|
||
const r = this.core.regs;
|
||
const c = this.core;
|
||
|
||
switch (addr) {
|
||
// ── C library functions ──────────────────────────────────────────
|
||
case 0x40000354: {
|
||
// memset(dest, val, n) → dest
|
||
const dest = r[10] >>> 0;
|
||
const val = r[11] & 0xff;
|
||
const n = r[12] >>> 0;
|
||
const safeN = Math.min(n, 0x100000); // cap at 1 MB safety
|
||
for (let i = 0; i < safeN; i++) c.writeByte(dest + i, val);
|
||
r[10] = dest | 0;
|
||
break;
|
||
}
|
||
case 0x40000358: {
|
||
// memcpy(dest, src, n) → dest
|
||
const dest = r[10] >>> 0;
|
||
const src = r[11] >>> 0;
|
||
const n = r[12] >>> 0;
|
||
const safeN = Math.min(n, 0x100000);
|
||
// Copy forward (like real memcpy, undefined for overlap)
|
||
for (let i = 0; i < safeN; i++) c.writeByte(dest + i, c.readByte(src + i));
|
||
r[10] = dest | 0;
|
||
break;
|
||
}
|
||
case 0x4000035c: {
|
||
// memmove(dest, src, n) → dest
|
||
const dest = r[10] >>> 0;
|
||
const src = r[11] >>> 0;
|
||
const n = r[12] >>> 0;
|
||
const safeN = Math.min(n, 0x100000);
|
||
if (dest < src) {
|
||
for (let i = 0; i < safeN; i++) c.writeByte(dest + i, c.readByte(src + i));
|
||
} else {
|
||
for (let i = safeN - 1; i >= 0; i--) c.writeByte(dest + i, c.readByte(src + i));
|
||
}
|
||
r[10] = dest | 0;
|
||
break;
|
||
}
|
||
case 0x40000360: {
|
||
// memcmp(a, b, n) → int
|
||
const a = r[10] >>> 0;
|
||
const b = r[11] >>> 0;
|
||
const n = r[12] >>> 0;
|
||
let result = 0;
|
||
for (let i = 0; i < n; i++) {
|
||
const diff = c.readByte(a + i) - c.readByte(b + i);
|
||
if (diff !== 0) {
|
||
result = diff > 0 ? 1 : -1;
|
||
break;
|
||
}
|
||
}
|
||
r[10] = result;
|
||
break;
|
||
}
|
||
case 0x40000364: {
|
||
// strcpy(dest, src) → dest
|
||
const dest = r[10] >>> 0;
|
||
const src = r[11] >>> 0;
|
||
let i = 0;
|
||
while (i < 0x100000) {
|
||
const ch = c.readByte(src + i);
|
||
c.writeByte(dest + i, ch);
|
||
if (ch === 0) break;
|
||
i++;
|
||
}
|
||
r[10] = dest | 0;
|
||
break;
|
||
}
|
||
case 0x4000036c: {
|
||
// strcmp(a, b) → int
|
||
const a = r[10] >>> 0;
|
||
const b = r[11] >>> 0;
|
||
let result = 0;
|
||
for (let i = 0; i < 0x100000; i++) {
|
||
const ca = c.readByte(a + i);
|
||
const cb = c.readByte(b + i);
|
||
if (ca !== cb) {
|
||
result = ca - cb;
|
||
break;
|
||
}
|
||
if (ca === 0) break;
|
||
}
|
||
r[10] = result;
|
||
break;
|
||
}
|
||
case 0x40000374: {
|
||
// strlen(s) → size_t
|
||
const s = r[10] >>> 0;
|
||
let len = 0;
|
||
while (len < 0x100000 && c.readByte(s + len) !== 0) len++;
|
||
r[10] = len;
|
||
break;
|
||
}
|
||
case 0x4000037c: {
|
||
// bzero(dest, n)
|
||
const dest = r[10] >>> 0;
|
||
const n = r[11] >>> 0;
|
||
const safeN = Math.min(n, 0x100000);
|
||
for (let i = 0; i < safeN; i++) c.writeByte(dest + i, 0);
|
||
break;
|
||
}
|
||
|
||
// ── libgcc 64-bit integer math ───────────────────────────────────
|
||
case 0x400008ac: {
|
||
// __udivdi3(a, b) → a / b (unsigned 64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const bLo = r[12] >>> 0,
|
||
bHi = r[13] >>> 0;
|
||
const a = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||
const b = BigInt(bLo) | (BigInt(bHi) << 32n);
|
||
const q = b !== 0n ? a / b : 0n;
|
||
r[10] = Number(q & 0xffffffffn) | 0;
|
||
r[11] = Number((q >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x400008b0: {
|
||
// __udivmoddi4(a, b, *rem) → a / b, *rem = a % b
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const bLo = r[12] >>> 0,
|
||
bHi = r[13] >>> 0;
|
||
const remPtr = r[14] >>> 0; // a4
|
||
const a = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||
const b = BigInt(bLo) | (BigInt(bHi) << 32n);
|
||
const q = b !== 0n ? a / b : 0n;
|
||
const rem = b !== 0n ? a % b : 0n;
|
||
r[10] = Number(q & 0xffffffffn) | 0;
|
||
r[11] = Number((q >> 32n) & 0xffffffffn) | 0;
|
||
if (remPtr !== 0) {
|
||
c.writeWord(remPtr, Number(rem & 0xffffffffn));
|
||
c.writeWord(remPtr + 4, Number((rem >> 32n) & 0xffffffffn));
|
||
}
|
||
break;
|
||
}
|
||
case 0x400008b4: {
|
||
// __udivsi3(a, b) → a / b (unsigned 32-bit)
|
||
const a = r[10] >>> 0,
|
||
b = r[11] >>> 0;
|
||
r[10] = b !== 0 ? (a / b) >>> 0 : 0;
|
||
break;
|
||
}
|
||
case 0x400008bc: {
|
||
// __umoddi3(a, b) → a % b (unsigned 64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const bLo = r[12] >>> 0,
|
||
bHi = r[13] >>> 0;
|
||
const a = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||
const b = BigInt(bLo) | (BigInt(bHi) << 32n);
|
||
const rem = b !== 0n ? a % b : 0n;
|
||
r[10] = Number(rem & 0xffffffffn) | 0;
|
||
r[11] = Number((rem >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x400008c0: {
|
||
// __umodsi3(a, b) → a % b (unsigned 32-bit)
|
||
const a = r[10] >>> 0,
|
||
b = r[11] >>> 0;
|
||
r[10] = b !== 0 ? (a % b) >>> 0 : 0;
|
||
break;
|
||
}
|
||
case 0x400007b4: {
|
||
// __divdi3(a, b) → a / b (signed 64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] | 0;
|
||
const bLo = r[12] >>> 0,
|
||
bHi = r[13] | 0;
|
||
const a = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
|
||
const b = BigInt.asIntN(64, BigInt(bLo) | (BigInt(bHi) << 32n));
|
||
const q = b !== 0n ? a / b : 0n;
|
||
const qu = BigInt.asUintN(64, q);
|
||
r[10] = Number(qu & 0xffffffffn) | 0;
|
||
r[11] = Number((qu >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x400007c0: {
|
||
// __divsi3(a, b) → a / b (signed 32-bit)
|
||
const a = r[10] | 0,
|
||
b = r[11] | 0;
|
||
r[10] = b !== 0 ? (a / b) | 0 : 0;
|
||
break;
|
||
}
|
||
case 0x4000083c: {
|
||
// __moddi3(a, b) → a % b (signed 64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] | 0;
|
||
const bLo = r[12] >>> 0,
|
||
bHi = r[13] | 0;
|
||
const a = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
|
||
const b = BigInt.asIntN(64, BigInt(bLo) | (BigInt(bHi) << 32n));
|
||
const rem = b !== 0n ? a % b : 0n;
|
||
const remu = BigInt.asUintN(64, rem);
|
||
r[10] = Number(remu & 0xffffffffn) | 0;
|
||
r[11] = Number((remu >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x40000840: {
|
||
// __modsi3(a, b) → a % b (signed 32-bit)
|
||
const a = r[10] | 0,
|
||
b = r[11] | 0;
|
||
r[10] = b !== 0 ? (a % b) | 0 : 0;
|
||
break;
|
||
}
|
||
case 0x4000084c: {
|
||
// __muldi3(a, b) → a * b (64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const bLo = r[12] >>> 0,
|
||
bHi = r[13] >>> 0;
|
||
const a = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||
const b = BigInt(bLo) | (BigInt(bHi) << 32n);
|
||
const p = BigInt.asUintN(64, a * b);
|
||
r[10] = Number(p & 0xffffffffn) | 0;
|
||
r[11] = Number((p >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x40000858: {
|
||
// __mulsi3(a, b) → a * b (32-bit)
|
||
r[10] = Math.imul(r[10], r[11]);
|
||
break;
|
||
}
|
||
|
||
// ── libgcc shift / bit operations ────────────────────────────────
|
||
case 0x4000077c: {
|
||
// __ashldi3(a, shift) → a << shift (64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const shift = r[12] & 63;
|
||
const val = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||
const res = BigInt.asUintN(64, val << BigInt(shift));
|
||
r[10] = Number(res & 0xffffffffn) | 0;
|
||
r[11] = Number((res >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x40000780: {
|
||
// __ashrdi3(a, shift) → a >> shift (signed/arithmetic 64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const shift = r[12] & 63;
|
||
const val = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
|
||
const res = BigInt.asUintN(64, val >> BigInt(shift));
|
||
r[10] = Number(res & 0xffffffffn) | 0;
|
||
r[11] = Number((res >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x40000830: {
|
||
// __lshrdi3(a, shift) → a >>> shift (unsigned/logical 64-bit)
|
||
const aLo = r[10] >>> 0,
|
||
aHi = r[11] >>> 0;
|
||
const shift = r[12] & 63;
|
||
const val = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||
const res = val >> BigInt(shift); // positive BigInt → logical shift
|
||
r[10] = Number(res & 0xffffffffn) | 0;
|
||
r[11] = Number((res >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x4000079c: {
|
||
// __clzsi2(a) → count leading zeros (32-bit)
|
||
r[10] = Math.clz32(r[10] >>> 0);
|
||
break;
|
||
}
|
||
case 0x40000798: {
|
||
// __clzdi2(a) → count leading zeros (64-bit)
|
||
const lo = r[10] >>> 0,
|
||
hi = r[11] >>> 0;
|
||
r[10] = hi !== 0 ? Math.clz32(hi) : 32 + Math.clz32(lo);
|
||
break;
|
||
}
|
||
case 0x400007a8: {
|
||
// __ctzsi2(a) → count trailing zeros (32-bit)
|
||
const v = r[10] >>> 0;
|
||
r[10] = v === 0 ? 32 : 31 - Math.clz32(v & -v);
|
||
break;
|
||
}
|
||
case 0x400007a4: {
|
||
// __ctzdi2(a) → count trailing zeros (64-bit)
|
||
const lo = r[10] >>> 0,
|
||
hi = r[11] >>> 0;
|
||
if (lo !== 0) {
|
||
r[10] = 31 - Math.clz32(lo & -lo);
|
||
} else if (hi !== 0) {
|
||
r[10] = 32 + (31 - Math.clz32(hi & -hi));
|
||
} else {
|
||
r[10] = 64;
|
||
}
|
||
break;
|
||
}
|
||
case 0x4000086c: {
|
||
// __negdi2(a) → -a (64-bit)
|
||
const lo = r[10] >>> 0,
|
||
hi = r[11] >>> 0;
|
||
const val = BigInt(lo) | (BigInt(hi) << 32n);
|
||
const neg = BigInt.asUintN(64, -val);
|
||
r[10] = Number(neg & 0xffffffffn) | 0;
|
||
r[11] = Number((neg >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
case 0x400007d0: {
|
||
// __ffsdi2(a) → find first set bit (64-bit), 0 if none
|
||
const lo = r[10] >>> 0,
|
||
hi = r[11] >>> 0;
|
||
if (lo !== 0) {
|
||
r[10] = 31 - Math.clz32(lo & -lo) + 1;
|
||
} else if (hi !== 0) {
|
||
r[10] = 32 + (31 - Math.clz32(hi & -hi)) + 1;
|
||
} else {
|
||
r[10] = 0;
|
||
}
|
||
break;
|
||
}
|
||
case 0x400007d4: {
|
||
// __ffssi2(a) → find first set bit (32-bit), 0 if none
|
||
const v = r[10] >>> 0;
|
||
r[10] = v === 0 ? 0 : 31 - Math.clz32(v & -v) + 1;
|
||
break;
|
||
}
|
||
case 0x40000784: {
|
||
// __bswapdi2(a) → byte-swap 64-bit
|
||
const lo = r[10] >>> 0,
|
||
hi = r[11] >>> 0;
|
||
const swapLo =
|
||
((lo >>> 24) | ((lo >>> 8) & 0xff00) | ((lo << 8) & 0xff0000) | (lo << 24)) >>> 0;
|
||
const swapHi =
|
||
((hi >>> 24) | ((hi >>> 8) & 0xff00) | ((hi << 8) & 0xff0000) | (hi << 24)) >>> 0;
|
||
r[10] = swapHi | 0; // swapped hi becomes new lo
|
||
r[11] = swapLo | 0; // swapped lo becomes new hi
|
||
break;
|
||
}
|
||
case 0x40000788: {
|
||
// __bswapsi2(a) → byte-swap 32-bit
|
||
const v = r[10] >>> 0;
|
||
r[10] = (v >>> 24) | ((v >>> 8) & 0xff00) | ((v << 8) & 0xff0000) | (v << 24) | 0;
|
||
break;
|
||
}
|
||
case 0x400007a0: {
|
||
// __cmpdi2(a, b) → 0 if a<b, 1 if a==b, 2 if a>b (signed 64-bit)
|
||
const a = BigInt.asIntN(64, BigInt(r[10] >>> 0) | (BigInt(r[11]) << 32n));
|
||
const b = BigInt.asIntN(64, BigInt(r[12] >>> 0) | (BigInt(r[13]) << 32n));
|
||
r[10] = a < b ? 0 : a === b ? 1 : 2;
|
||
break;
|
||
}
|
||
case 0x400008a8: {
|
||
// __ucmpdi2(a, b) → 0 if a<b, 1 if a==b, 2 if a>b (unsigned 64-bit)
|
||
const a = BigInt(r[10] >>> 0) | (BigInt(r[11] >>> 0) << 32n);
|
||
const b = BigInt(r[12] >>> 0) | (BigInt(r[13] >>> 0) << 32n);
|
||
r[10] = a < b ? 0 : a === b ? 1 : 2;
|
||
break;
|
||
}
|
||
case 0x40000764: {
|
||
// __absvdi2(a) → |a| (signed 64-bit, aborts on overflow)
|
||
const val = BigInt.asIntN(64, BigInt(r[10] >>> 0) | (BigInt(r[11]) << 32n));
|
||
const abs = val < 0n ? BigInt.asUintN(64, -val) : BigInt.asUintN(64, val);
|
||
r[10] = Number(abs & 0xffffffffn) | 0;
|
||
r[11] = Number((abs >> 32n) & 0xffffffffn) | 0;
|
||
break;
|
||
}
|
||
|
||
// ── ESP-IDF ROM helpers ──────────────────────────────────────────
|
||
case 0x40000018: {
|
||
// rtc_get_reset_reason() → 1 (POWERON_RESET)
|
||
r[10] = 1;
|
||
break;
|
||
}
|
||
case 0x40000050: {
|
||
// ets_delay_us(us) → void
|
||
// Burn the equivalent number of CPU cycles so timers advance
|
||
const us = r[10] >>> 0;
|
||
const burnCycles = Math.min(us * this._ticksPerUs, 1_000_000);
|
||
this.core.cycles += burnCycles;
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
case 0x40000548: {
|
||
// Cache_Set_IDROM_MMU_Size(...) → 0 (success)
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
case 0x40001960: {
|
||
// rom_i2c_writeReg_Mask(...) → 0 (success)
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
case 0x4000195c: {
|
||
// rom_i2c_writeReg(...) → 0 (success)
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
case 0x40000588: {
|
||
// ets_update_cpu_frequency(ticks_per_us)
|
||
// Store value so our ets_delay_us can use the right multiplier.
|
||
// Firmware calls this with e.g. 40 or 160.
|
||
this._ticksPerUs = r[10] >>> 0;
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
case 0x40000084: {
|
||
// uart_tx_wait_idle(uart_num) — no-op
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
case 0x400005f4: {
|
||
// intr_matrix_set(cpu_no, model_num, intr_num)
|
||
// Maps peripheral interrupt source a1 to CPU interrupt line a2.
|
||
// This directly programs the interrupt matrix hardware.
|
||
const source = r[11] >>> 0;
|
||
const line = r[12] & 0x1f;
|
||
if (source < 62) {
|
||
this._intSrcMap[source] = line;
|
||
// Also store in the MMIO echo-back register so firmware reads work
|
||
this._periRegs.set(INTMATRIX_BASE + source * 4, line);
|
||
}
|
||
break;
|
||
}
|
||
|
||
default: {
|
||
// Unknown ROM function — return a0=0 (ESP_OK for esp_err_t functions)
|
||
r[10] = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Timer Group stub (TIMG0 / TIMG1).
|
||
*
|
||
* Critical register: RTCCALICFG1 at offset 0x6C (confirmed from qemu-lcgamboa
|
||
* esp32c3_timg.h — offset 0x48 is TIMG_WDTCONFIG0, not the calibration result).
|
||
* Bit 31 = TIMG_RTC_CALI_DONE — must read as 1 or rtc_clk_cal_internal()
|
||
* spins forever waiting for calibration to complete.
|
||
* Bits [30:7] = cal_value — must be non-zero or the outer retry loop
|
||
* in esp_rtc_clk_init() keeps calling rtc_clk_cal() forever.
|
||
*
|
||
* Called for all known TIMG0/TIMG1 base addresses across ESP-IDF versions.
|
||
*/
|
||
private _registerTimerGroup(base: number): void {
|
||
const peri = this._periRegs;
|
||
this.core.addMmio(
|
||
base,
|
||
0x100,
|
||
(addr) => {
|
||
const off = addr - base;
|
||
const wOff = off & ~3;
|
||
if (wOff === 0x68) {
|
||
// TIMG_RTCCALICFG: bit15=TIMG_RTC_CALI_RDY=1 — calibration instantly done
|
||
// Also set bit31 (start bit echo) which some versions check
|
||
const word = (1 << 15) | (1 << 31);
|
||
return (word >>> ((off & 3) * 8)) & 0xff;
|
||
}
|
||
if (wOff === 0x6c) {
|
||
// TIMG_RTCCALICFG1: bits[31:7]=rtc_cali_value — non-zero so outer retry exits
|
||
const word = (136533 << 7) >>> 0; // typical 150kHz RTC vs 40MHz XTAL
|
||
return (word >>> ((off & 3) * 8)) & 0xff;
|
||
}
|
||
if (wOff === 0x80) {
|
||
// TIMG_RTCCALICFG2 (ESP-IDF v5): bit31=timeout(0), bits[24:7]=cali_value
|
||
// ESP-IDF v5 reads result HERE instead of RTCCALICFG1.
|
||
// Must be non-zero or rtc_clk_cal() retries forever.
|
||
const word = (136533 << 7) >>> 0;
|
||
return (word >>> ((off & 3) * 8)) & 0xff;
|
||
}
|
||
// Echo last written value for all other offsets
|
||
const wordAddr = addr & ~3;
|
||
const word = peri.get(wordAddr) ?? 0;
|
||
return (word >>> ((addr & 3) * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
peri.set(wordAddr, (prev & ~(0xff << shift)) | ((val & 0xff) << shift));
|
||
},
|
||
);
|
||
}
|
||
|
||
/**
|
||
* SPI flash controller stub (SPI0 / SPI1).
|
||
*
|
||
* SPI_MEM_CMD_REG (offset 0x00) bits [17–31] are "write 1 to start operation,
|
||
* hardware clears when done". The firmware polls these bits after triggering
|
||
* flash reads, writes, erases, etc. We auto‑clear them so every flash
|
||
* operation appears to complete instantly.
|
||
*
|
||
* Other registers use echo‑back so configuration writes can be read back.
|
||
*/
|
||
private _registerSpiFlash(base: number): void {
|
||
const peri = this._periRegs;
|
||
this.core.addMmio(
|
||
base,
|
||
SPI_SIZE,
|
||
(addr) => {
|
||
const off = addr - base;
|
||
const wordOff = off & ~3;
|
||
if (wordOff === SPI_CMD) {
|
||
// Always return 0 for CMD register — all operations are "done"
|
||
return 0;
|
||
}
|
||
// Echo last written value for all other offsets
|
||
const wordAddr = addr & ~3;
|
||
const word = peri.get(wordAddr) ?? 0;
|
||
return (word >>> ((addr & 3) * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
peri.set(wordAddr, (prev & ~(0xff << shift)) | ((val & 0xff) << shift));
|
||
},
|
||
);
|
||
}
|
||
|
||
/**
|
||
* EXTMEM cache controller stub (0x600C4000).
|
||
*
|
||
* The ESP-IDF boot enables ICache, then triggers cache invalidation / sync /
|
||
* preload operations and polls "done" bits. We return all "done" bits as 1
|
||
* so these operations appear to complete instantly.
|
||
*/
|
||
private _registerExtMem(): void {
|
||
const peri = this._periRegs;
|
||
this.core.addMmio(
|
||
EXTMEM_BASE,
|
||
EXTMEM_SIZE,
|
||
(addr) => {
|
||
const off = addr - EXTMEM_BASE;
|
||
const wordOff = off & ~3;
|
||
// Return "done" bits for operations that the boot polls:
|
||
let override: number | null = null;
|
||
switch (wordOff) {
|
||
case EXTMEM_ICACHE_SYNC_CTRL:
|
||
override = 1 << 1;
|
||
break; // SYNC_DONE
|
||
case EXTMEM_ICACHE_PRELOAD_CTRL:
|
||
override = 1 << 1;
|
||
break; // PRELOAD_DONE
|
||
case EXTMEM_ICACHE_AUTOLOAD_CTRL:
|
||
override = 1 << 3;
|
||
break; // AUTOLOAD_DONE
|
||
case EXTMEM_ICACHE_LOCK_CTRL:
|
||
override = 1 << 2;
|
||
break; // LOCK_DONE
|
||
}
|
||
if (override !== null) {
|
||
// Merge override bits with any written value so enable bits are preserved
|
||
const wordAddr = addr & ~3;
|
||
const word = (peri.get(wordAddr) ?? 0) | override;
|
||
return (word >>> ((addr & 3) * 8)) & 0xff;
|
||
}
|
||
// Echo last written value for all other offsets
|
||
const wordAddr = addr & ~3;
|
||
const word = peri.get(wordAddr) ?? 0;
|
||
return (word >>> ((addr & 3) * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
peri.set(wordAddr, (prev & ~(0xff << shift)) | ((val & 0xff) << shift));
|
||
},
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Broad catch-all for the entire ESP32-C3 peripheral address space
|
||
* (0x60000000–0x6FFFFFFF).
|
||
*
|
||
* Writes are stored in _periRegs so that the firmware's common
|
||
* "write config → read back → verify" pattern works for any peripheral
|
||
* register we haven't stubbed explicitly. All narrower, more specific
|
||
* handlers (UART0, GPIO, SYSTIMER, INTC, RTC_CNTL …) have smaller MMIO
|
||
* sizes and therefore take priority via mmioFor's "smallest-size-wins" rule.
|
||
*/
|
||
private _registerPeripheralCatchAll(): void {
|
||
const peri = this._periRegs;
|
||
this.core.addMmio(
|
||
0x60000000,
|
||
0x10000000,
|
||
(addr) => {
|
||
const wordAddr = addr & ~3;
|
||
const word = peri.get(wordAddr) ?? 0;
|
||
return (word >>> ((addr & 3) * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
peri.set(wordAddr, (prev & ~(0xff << shift)) | ((val & 0xff) << shift));
|
||
},
|
||
);
|
||
}
|
||
|
||
/**
|
||
* RTC_CNTL peripheral stub (0x60008000, 4 KB).
|
||
*
|
||
* Critical register: TIME_UPDATE_REG at offset 0x70 (address 0x60008070).
|
||
* Bit 30 = TIME_VALID — must read as 1 or the `rtc_clk_cal()` loop in
|
||
* esp-idf never exits and MIE is never enabled (FreeRTOS scheduler stalls).
|
||
* Also covers the eFUSE block at 0x60008800 (offset 0x800) — returns 0 for
|
||
* all eFuse words (chip-revision 0 / all features disabled = safe defaults).
|
||
*/
|
||
private _registerRtcCntl(): void {
|
||
const RTC_BASE = 0x60008000;
|
||
const peri = this._periRegs;
|
||
this.core.addMmio(
|
||
RTC_BASE,
|
||
0x1000,
|
||
(addr) => {
|
||
const off = addr - RTC_BASE;
|
||
const wordOff = off & ~3;
|
||
// offset 0x70 (RTC_CLK_CONF): TIME_VALID (bit 30) = 1 so rtc_clk_cal() exits.
|
||
// offset 0x38 (RESET_STATE): return 1 = ESP32C3_POWERON_RESET (matches QEMU).
|
||
if (wordOff === 0x70) {
|
||
const word = 1 << 30;
|
||
return (word >>> ((off & 3) * 8)) & 0xff;
|
||
}
|
||
if (wordOff === 0x38) {
|
||
return off === wordOff ? 1 : 0; // byte 0 = 1, rest = 0
|
||
}
|
||
// Echo last written value for all other offsets
|
||
const wordAddr = addr & ~3;
|
||
const word = peri.get(wordAddr) ?? 0;
|
||
return (word >>> ((addr & 3) * 8)) & 0xff;
|
||
},
|
||
(addr, val) => {
|
||
const wordAddr = addr & ~3;
|
||
const prev = peri.get(wordAddr) ?? 0;
|
||
const shift = (addr & 3) * 8;
|
||
peri.set(wordAddr, (prev & ~(0xff << shift)) | ((val & 0xff) << shift));
|
||
},
|
||
);
|
||
}
|
||
|
||
// ── HEX loading ────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Load an Intel HEX file. The hex addresses must be relative to IROM_BASE
|
||
* (0x42000000), or zero-based (the parser will treat them as flash offsets).
|
||
*/
|
||
loadHex(hexContent: string): void {
|
||
this.flash.fill(0);
|
||
const bytes = hexToUint8Array(hexContent);
|
||
|
||
// hexToUint8Array returns bytes indexed from address 0.
|
||
// If the hex records used IROM_BASE-relative addressing, the byte array
|
||
// will start at offset IROM_BASE within a huge buffer — we can't use that.
|
||
// Support both:
|
||
// a) Small array (< IROM_SIZE) → direct flash offset mapping
|
||
// b) Large array → slice from IROM_BASE offset if present
|
||
if (bytes.length <= IROM_SIZE) {
|
||
const maxCopy = Math.min(bytes.length, IROM_SIZE);
|
||
this.flash.set(bytes.subarray(0, maxCopy), 0);
|
||
} else {
|
||
// Try to extract data at IROM_BASE offset
|
||
const iromOffset = IROM_BASE;
|
||
if (bytes.length > iromOffset) {
|
||
const maxCopy = Math.min(bytes.length - iromOffset, IROM_SIZE);
|
||
this.flash.set(bytes.subarray(iromOffset, iromOffset + maxCopy), 0);
|
||
}
|
||
}
|
||
|
||
this.dram.fill(0);
|
||
this.iram.fill(0);
|
||
this.core.reset(IROM_BASE);
|
||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||
}
|
||
|
||
/**
|
||
* Load a raw binary image into flash at offset 0 (maps to IROM_BASE 0x42000000).
|
||
* Use this with binaries produced by:
|
||
* riscv32-esp-elf-objcopy -O binary firmware.elf firmware.bin
|
||
*/
|
||
loadBin(bin: Uint8Array): void {
|
||
this.flash.fill(0);
|
||
const maxCopy = Math.min(bin.length, IROM_SIZE);
|
||
this.flash.set(bin.subarray(0, maxCopy), 0);
|
||
this.dram.fill(0);
|
||
this.iram.fill(0);
|
||
this.rxFifo = [];
|
||
this.gpioOut = 0;
|
||
this.gpioIn = 0;
|
||
this._periRegs.clear();
|
||
this.core.reset(IROM_BASE);
|
||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||
}
|
||
|
||
/**
|
||
* Load a merged ESP32 flash image from the backend (base64-encoded).
|
||
*
|
||
* The backend produces a 4 MB merged image:
|
||
* 0x01000 — bootloader
|
||
* 0x08000 — partition table
|
||
* 0x10000 — application (ESP32 image format with segment headers)
|
||
*
|
||
* Each image segment is loaded at its virtual load address:
|
||
* IROM (0x42xxxxxx) → flash buffer (executed code)
|
||
* DROM (0x3Cxxxxxx) → flash buffer (read-only data alias)
|
||
* DRAM (0x3FCxxxxx) → dram buffer (initialised .data)
|
||
* IRAM (0x4037xxxx) → iram buffer (ISR / time-critical code)
|
||
*
|
||
* The CPU resets to the entry point declared in the image header.
|
||
*/
|
||
loadFlashImage(base64: string): void {
|
||
// Base64 decode
|
||
const binStr = atob(base64);
|
||
const data = new Uint8Array(binStr.length);
|
||
for (let i = 0; i < binStr.length; i++) data[i] = binStr.charCodeAt(i);
|
||
|
||
// Parse ESP32 image format
|
||
const parsed = parseMergedFlashImage(data);
|
||
|
||
// Clear all memory regions
|
||
this.flash.fill(0);
|
||
this.dram.fill(0);
|
||
this.iram.fill(0);
|
||
this.rxFifo = [];
|
||
this.gpioOut = 0;
|
||
this.gpioIn = 0;
|
||
this._periRegs.clear();
|
||
|
||
// Load each segment at its virtual address
|
||
for (const { loadAddr, data: seg } of parsed.segments) {
|
||
const uAddr = loadAddr >>> 0;
|
||
|
||
if (uAddr >= IROM_BASE && uAddr + seg.length <= IROM_BASE + IROM_SIZE) {
|
||
this.flash.set(seg, uAddr - IROM_BASE);
|
||
} else if (uAddr >= DROM_BASE && uAddr + seg.length <= DROM_BASE + IROM_SIZE) {
|
||
// DROM is a virtual alias of flash — store at same flash buffer
|
||
this.flash.set(seg, uAddr - DROM_BASE);
|
||
} else if (uAddr >= DRAM_BASE && uAddr + seg.length <= DRAM_BASE + DRAM_SIZE) {
|
||
this.dram.set(seg, uAddr - DRAM_BASE);
|
||
} else if (uAddr >= IRAM_BASE && uAddr + seg.length <= IRAM_BASE + IRAM_SIZE) {
|
||
this.iram.set(seg, uAddr - IRAM_BASE);
|
||
} else {
|
||
console.warn(
|
||
`[Esp32C3Simulator] Segment 0x${uAddr.toString(16)}` +
|
||
` (${seg.length} B) outside known regions — skipped`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// Boot CPU at image entry point
|
||
this.core.reset(parsed.entryPoint);
|
||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||
|
||
console.log(
|
||
`[Esp32C3Simulator] Loaded ${parsed.segments.length} segments,` +
|
||
` entry=0x${parsed.entryPoint.toString(16)}`,
|
||
);
|
||
}
|
||
|
||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||
|
||
start(): void {
|
||
if (this.running) return;
|
||
this._ticksPerUs = 160;
|
||
console.log(`[ESP32-C3] Simulation started, entry=0x${this.core.pc.toString(16)}`);
|
||
this.running = true;
|
||
this._loop();
|
||
}
|
||
|
||
stop(): void {
|
||
this.running = false;
|
||
cancelAnimationFrame(this.animFrameId);
|
||
}
|
||
|
||
reset(): void {
|
||
this.stop();
|
||
this.rxFifo = [];
|
||
this.gpioOut = 0;
|
||
this.gpioIn = 0;
|
||
this._stIntEna = 0;
|
||
this._stIntRaw = 0;
|
||
this._periRegs.clear();
|
||
this._ticksPerUs = 160;
|
||
this.dram.fill(0);
|
||
this.iram.fill(0);
|
||
this.core.reset(IROM_BASE);
|
||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||
}
|
||
|
||
serialWrite(text: string): void {
|
||
for (let i = 0; i < text.length; i++) {
|
||
this.rxFifo.push(text.charCodeAt(i));
|
||
}
|
||
}
|
||
|
||
setPinState(pin: number, state: boolean): void {
|
||
if (state) this.gpioIn |= 1 << pin;
|
||
else this.gpioIn &= ~(1 << pin);
|
||
}
|
||
|
||
isRunning(): boolean {
|
||
return this.running;
|
||
}
|
||
|
||
// ── Execution loop ─────────────────────────────────────────────────────────
|
||
|
||
private _loop(): void {
|
||
if (!this.running) return;
|
||
|
||
// Execute in 1 ms chunks so FreeRTOS tick interrupts fire at ~1 kHz.
|
||
let rem = CYCLES_PER_FRAME;
|
||
while (rem > 0) {
|
||
const n = rem < CYCLES_PER_TICK ? rem : CYCLES_PER_TICK;
|
||
for (let i = 0; i < n; i++) {
|
||
this.core.step();
|
||
}
|
||
rem -= n;
|
||
|
||
// Raise SYSTIMER TARGET0 alarm → routed through interrupt matrix.
|
||
this._stIntRaw |= 1;
|
||
if (this._stIntEna & 1) {
|
||
this._raiseIntSource(ETS_SYSTIMER_TARGET0_SRC);
|
||
}
|
||
}
|
||
|
||
this.animFrameId = requestAnimationFrame(() => this._loop());
|
||
}
|
||
}
|