feat: enhance ESP32-C3 simulator with ROM stubs, timer group handling, and diagnostic logging
parent
220346f220
commit
507fa0671c
|
|
@ -60,6 +60,16 @@ const ST_UNIT0_VAL_HI = 0x58; // snapshot value high 32 bits
|
||||||
const INTC_BASE = 0x600C5000;
|
const INTC_BASE = 0x600C5000;
|
||||||
const INTC_SIZE = 0x800;
|
const INTC_SIZE = 0x800;
|
||||||
|
|
||||||
|
// ── 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 ───────────────────────────────────────────────────────────────────
|
// ── Clock ───────────────────────────────────────────────────────────────────
|
||||||
const CPU_HZ = 160_000_000;
|
const CPU_HZ = 160_000_000;
|
||||||
const CYCLES_PER_FRAME = Math.round(CPU_HZ / 60);
|
const CYCLES_PER_FRAME = Math.round(CPU_HZ / 60);
|
||||||
|
|
@ -81,6 +91,12 @@ export class Esp32C3Simulator {
|
||||||
private _stIntEna = 0; // ST_INT_ENA register
|
private _stIntEna = 0; // ST_INT_ENA register
|
||||||
private _stIntRaw = 0; // ST_INT_RAW register (bit0 = TARGET0 fired)
|
private _stIntRaw = 0; // ST_INT_RAW register (bit0 = TARGET0 fired)
|
||||||
|
|
||||||
|
// ── Diagnostic state ─────────────────────────────────────────────────────
|
||||||
|
private _dbgFrameCount = 0;
|
||||||
|
private _dbgTickCount = 0;
|
||||||
|
private _dbgLastMtvec = 0;
|
||||||
|
private _dbgMieEnabled = false;
|
||||||
|
|
||||||
public pinManager: PinManager;
|
public pinManager: PinManager;
|
||||||
public onSerialData: ((ch: string) => void) | null = null;
|
public onSerialData: ((ch: string) => void) | null = null;
|
||||||
public onBaudRateChange: ((baud: number) => void) | null = null;
|
public onBaudRateChange: ((baud: number) => void) | null = null;
|
||||||
|
|
@ -117,10 +133,23 @@ export class Esp32C3Simulator {
|
||||||
(addr, val) => { iram[addr - IRAM_BASE] = val; },
|
(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._registerUart0();
|
||||||
this._registerGpio();
|
this._registerGpio();
|
||||||
this._registerSysTimer();
|
this._registerSysTimer();
|
||||||
this._registerIntCtrl();
|
this._registerIntCtrl();
|
||||||
|
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._registerRomStub();
|
||||||
|
this._registerRomStub2();
|
||||||
|
|
||||||
this.core.reset(IROM_BASE);
|
this.core.reset(IROM_BASE);
|
||||||
// Initialize SP to top of DRAM — MUST be after reset() which zeroes all regs
|
// Initialize SP to top of DRAM — MUST be after reset() which zeroes all regs
|
||||||
|
|
@ -179,8 +208,9 @@ export class Esp32C3Simulator {
|
||||||
for (let bit = 0; bit < 22; bit++) { // ESP32-C3 has GPIO0–GPIO21
|
for (let bit = 0; bit < 22; bit++) { // ESP32-C3 has GPIO0–GPIO21
|
||||||
if (changed & (1 << bit)) {
|
if (changed & (1 << bit)) {
|
||||||
const state = !!(this.gpioOut & (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.onPinChangeWithTime?.(bit, state, timeMs);
|
||||||
this.pinManager.triggerPinChange(bit, state);
|
this.pinManager.setPinState(bit, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +261,107 @@ export class Esp32C3Simulator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ROM stub — makes calls into ESP32-C3 ROM (0x40000000-0x4005FFFF) return
|
||||||
|
* immediately. Without a ROM image the CPU would fetch 0x00 bytes and loop
|
||||||
|
* forever at address 0. We stub every 16-bit slot with C.JR ra (0x8082)
|
||||||
|
* so every ROM call acts as a no-op and returns to the call site.
|
||||||
|
*/
|
||||||
|
private _registerRomStub(): void {
|
||||||
|
this.core.addMmio(ROM_BASE, ROM_SIZE,
|
||||||
|
// C.JR ra = 0x8082, little-endian: even byte=0x82, odd byte=0x80
|
||||||
|
(addr) => (addr & 1) === 0 ? 0x82 : 0x80,
|
||||||
|
(_addr, _val) => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Second ROM region (0x40800000) — same stub. */
|
||||||
|
private _registerRomStub2(): void {
|
||||||
|
this.core.addMmio(ROM2_BASE, ROM2_SIZE,
|
||||||
|
(addr) => (addr & 1) === 0 ? 0x82 : 0x80,
|
||||||
|
(_addr, _val) => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 seen = new Set<number>();
|
||||||
|
this.core.addMmio(base, 0x100,
|
||||||
|
(addr) => {
|
||||||
|
const off = addr - base;
|
||||||
|
const wOff = off & ~3;
|
||||||
|
if (!seen.has(wOff)) {
|
||||||
|
seen.add(wOff);
|
||||||
|
console.log(`[TIMG@0x${base.toString(16)}] 1st read wOff=0x${wOff.toString(16)} pc=0x${this.core.pc.toString(16)}`);
|
||||||
|
}
|
||||||
|
if (wOff === 0x68) {
|
||||||
|
// TIMG_RTCCALICFG: bit15=TIMG_RTC_CALI_RDY=1 — calibration instantly done
|
||||||
|
const word = (1 << 15); // 0x00008000
|
||||||
|
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 = (1000000 << 7); // 0x07A12000
|
||||||
|
return (word >>> ((off & 3) * 8)) & 0xFF;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
(_addr, _val) => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broad catch-all for the entire ESP32-C3 peripheral address space
|
||||||
|
* (0x60000000–0x6FFFFFFF). Returns 0 for any unmapped peripheral register
|
||||||
|
* so that the CPU doesn't fault or log warnings for writes during init.
|
||||||
|
* 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 {
|
||||||
|
this.core.addMmio(0x60000000, 0x10000000,
|
||||||
|
() => 0,
|
||||||
|
(_addr, _val) => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
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).
|
||||||
|
const word = wordOff === 0x70 ? (1 << 30)
|
||||||
|
: wordOff === 0x38 ? 1
|
||||||
|
: 0;
|
||||||
|
return (word >>> ((off & 3) * 8)) & 0xFF;
|
||||||
|
},
|
||||||
|
(_addr, _val) => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── HEX loading ────────────────────────────────────────────────────────────
|
// ── HEX loading ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -351,6 +482,11 @@ export class Esp32C3Simulator {
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
if (this.running) return;
|
if (this.running) return;
|
||||||
|
this._dbgFrameCount = 0;
|
||||||
|
this._dbgTickCount = 0;
|
||||||
|
this._dbgLastMtvec = 0;
|
||||||
|
this._dbgMieEnabled = false;
|
||||||
|
console.log(`[ESP32-C3] Simulation started, entry=0x${this.core.pc.toString(16)}`);
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this._loop();
|
this._loop();
|
||||||
}
|
}
|
||||||
|
|
@ -362,11 +498,15 @@ export class Esp32C3Simulator {
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.rxFifo = [];
|
this.rxFifo = [];
|
||||||
this.gpioOut = 0;
|
this.gpioOut = 0;
|
||||||
this.gpioIn = 0;
|
this.gpioIn = 0;
|
||||||
this._stIntEna = 0;
|
this._stIntEna = 0;
|
||||||
this._stIntRaw = 0;
|
this._stIntRaw = 0;
|
||||||
|
this._dbgFrameCount = 0;
|
||||||
|
this._dbgTickCount = 0;
|
||||||
|
this._dbgLastMtvec = 0;
|
||||||
|
this._dbgMieEnabled = false;
|
||||||
this.dram.fill(0);
|
this.dram.fill(0);
|
||||||
this.iram.fill(0);
|
this.iram.fill(0);
|
||||||
this.core.reset(IROM_BASE);
|
this.core.reset(IROM_BASE);
|
||||||
|
|
@ -393,9 +533,45 @@ export class Esp32C3Simulator {
|
||||||
private _loop(): void {
|
private _loop(): void {
|
||||||
if (!this.running) return;
|
if (!this.running) return;
|
||||||
|
|
||||||
|
this._dbgFrameCount++;
|
||||||
|
|
||||||
|
// ── Per-frame diagnostics (check once, before heavy execution) ─────────
|
||||||
|
// Detect mtvec being set — FreeRTOS writes this during startup.
|
||||||
|
const mtvec = this.core.mtvecVal;
|
||||||
|
if (mtvec !== this._dbgLastMtvec) {
|
||||||
|
if (mtvec !== 0) {
|
||||||
|
console.log(
|
||||||
|
`[ESP32-C3] mtvec set → 0x${mtvec.toString(16)}` +
|
||||||
|
` (mode=${mtvec & 3}) @ frame ${this._dbgFrameCount}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._dbgLastMtvec = mtvec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect MIE 0→1 transition — FreeRTOS enables this when scheduler starts.
|
||||||
|
const mie = (this.core.mstatusVal & 0x8) !== 0;
|
||||||
|
if (mie && !this._dbgMieEnabled) {
|
||||||
|
console.log(
|
||||||
|
`[ESP32-C3] MIE enabled (interrupts ON) @ frame ${this._dbgFrameCount}` +
|
||||||
|
`, pc=0x${this.core.pc.toString(16)}`
|
||||||
|
);
|
||||||
|
this._dbgMieEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log PC + key state every ~1 second (60 frames).
|
||||||
|
if (this._dbgFrameCount % 60 === 0) {
|
||||||
|
console.log(
|
||||||
|
`[ESP32-C3] frame=${this._dbgFrameCount}` +
|
||||||
|
` pc=0x${this.core.pc.toString(16)}` +
|
||||||
|
` cycles=${this.core.cycles}` +
|
||||||
|
` ticks=${this._dbgTickCount}` +
|
||||||
|
` mtvec=0x${mtvec.toString(16)}` +
|
||||||
|
` MIE=${mie}` +
|
||||||
|
` GPIO=0x${this.gpioOut.toString(16)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Execute in 1 ms chunks so FreeRTOS tick interrupts fire at ~1 kHz.
|
// Execute in 1 ms chunks so FreeRTOS tick interrupts fire at ~1 kHz.
|
||||||
// Each chunk corresponds to one SYSTIMER TARGET0 period (160 000 CPU cycles
|
|
||||||
// at 160 MHz = 16 000 SYSTIMER ticks at 16 MHz).
|
|
||||||
let rem = CYCLES_PER_FRAME;
|
let rem = CYCLES_PER_FRAME;
|
||||||
while (rem > 0) {
|
while (rem > 0) {
|
||||||
const n = rem < CYCLES_PER_TICK ? rem : CYCLES_PER_TICK;
|
const n = rem < CYCLES_PER_TICK ? rem : CYCLES_PER_TICK;
|
||||||
|
|
@ -404,8 +580,47 @@ export class Esp32C3Simulator {
|
||||||
}
|
}
|
||||||
rem -= n;
|
rem -= n;
|
||||||
|
|
||||||
|
this._dbgTickCount++;
|
||||||
|
// Log every 100 ticks (0.1 s) while still early in boot.
|
||||||
|
if (this._dbgTickCount <= 1000 && this._dbgTickCount % 100 === 0) {
|
||||||
|
const spc = this.core.pc;
|
||||||
|
let instrInfo = '';
|
||||||
|
const iramOff = spc - IRAM_BASE;
|
||||||
|
const flashOff = spc - IROM_BASE;
|
||||||
|
let ib0 = 0, ib1 = 0, ib2 = 0, ib3 = 0;
|
||||||
|
if (iramOff >= 0 && iramOff + 4 <= this.iram.length) {
|
||||||
|
[ib0, ib1, ib2, ib3] = [this.iram[iramOff], this.iram[iramOff+1], this.iram[iramOff+2], this.iram[iramOff+3]];
|
||||||
|
} else if (flashOff >= 0 && flashOff + 4 <= this.flash.length) {
|
||||||
|
[ib0, ib1, ib2, ib3] = [this.flash[flashOff], this.flash[flashOff+1], this.flash[flashOff+2], this.flash[flashOff+3]];
|
||||||
|
}
|
||||||
|
const instr16 = ib0 | (ib1 << 8);
|
||||||
|
const instr32 = ((ib0 | (ib1<<8) | (ib2<<16) | (ib3<<24)) >>> 0);
|
||||||
|
const isC = (instr16 & 3) !== 3;
|
||||||
|
const hex = isC ? instr16.toString(16).padStart(4,'0') : instr32.toString(16).padStart(8,'0');
|
||||||
|
if (!isC) {
|
||||||
|
const op = instr32 & 0x7F;
|
||||||
|
const f3 = (instr32 >> 12) & 7;
|
||||||
|
const rs1 = (instr32 >> 15) & 31;
|
||||||
|
if (op === 0x73) {
|
||||||
|
const csr = (instr32 >> 20) & 0xFFF;
|
||||||
|
instrInfo = ` [SYSTEM csr=0x${csr.toString(16)} f3=${f3}]`;
|
||||||
|
} else if (op === 0x03) {
|
||||||
|
const imm = (instr32 >> 20) << 0 >> 0;
|
||||||
|
instrInfo = ` [LOAD x${rs1}+${imm} f3=${f3}]`;
|
||||||
|
} else if (op === 0x63) {
|
||||||
|
instrInfo = ` [BRANCH f3=${f3}]`;
|
||||||
|
} else if (op === 0x23) {
|
||||||
|
instrInfo = ` [STORE f3=${f3}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[ESP32-C3] tick #${this._dbgTickCount}` +
|
||||||
|
` pc=0x${spc.toString(16)} instr=0x${hex}${instrInfo}` +
|
||||||
|
` MIE=${(this.core.mstatusVal & 0x8) !== 0}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Raise SYSTIMER TARGET0 alarm → CPU interrupt 1 (FreeRTOS tick).
|
// Raise SYSTIMER TARGET0 alarm → CPU interrupt 1 (FreeRTOS tick).
|
||||||
// mcause = 0x80000001: bit31=interrupt, bits[4:0]=CPU interrupt number 1.
|
|
||||||
this._stIntRaw |= 1;
|
this._stIntRaw |= 1;
|
||||||
this.core.triggerInterrupt(0x80000001);
|
this.core.triggerInterrupt(0x80000001);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,14 @@ export class PinManager {
|
||||||
return this.pinStates.get(arduinoPin) || false;
|
return this.pinStates.get(arduinoPin) || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a single pin state and notify listeners.
|
||||||
|
* Alias for triggerPinChange — used by ESP32-C3, RISC-V, and RP2040 simulators.
|
||||||
|
*/
|
||||||
|
setPinState(pin: number, state: boolean): void {
|
||||||
|
this.triggerPinChange(pin, state);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directly fire pin change callbacks for a specific pin.
|
* Directly fire pin change callbacks for a specific pin.
|
||||||
* Used by RP2040Simulator which has individual GPIO listeners instead of PORT registers.
|
* Used by RP2040Simulator which has individual GPIO listeners instead of PORT registers.
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ export class RiscVCore {
|
||||||
private readonly mem: Uint8Array;
|
private readonly mem: Uint8Array;
|
||||||
private readonly memBase: number;
|
private readonly memBase: number;
|
||||||
private readonly mmioRegions: MmioRegion[] = [];
|
private readonly mmioRegions: MmioRegion[] = [];
|
||||||
|
/** Word-aligned addresses of unmapped peripheral reads logged so far (dedup). */
|
||||||
|
private readonly _seenUnmapped = new Set<number>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mem Flat memory buffer (flash + RAM mapped contiguously)
|
* @param mem Flat memory buffer (flash + RAM mapped contiguously)
|
||||||
|
|
@ -67,9 +69,15 @@ export class RiscVCore {
|
||||||
this.memBase = memBase;
|
this.memBase = memBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register an MMIO region. Reads/writes in [base, base+size) go to hooks. */
|
/**
|
||||||
|
* Register an MMIO region. Reads/writes in [base, base+size) go to hooks.
|
||||||
|
* Regions are kept sorted by base address so mmioFor() can use early exit.
|
||||||
|
*/
|
||||||
addMmio(base: number, size: number, read: MmioReadHook, write: MmioWriteHook): void {
|
addMmio(base: number, size: number, read: MmioReadHook, write: MmioWriteHook): void {
|
||||||
this.mmioRegions.push({ base, size, read, write });
|
const region = { base, size, read, write };
|
||||||
|
const idx = this.mmioRegions.findIndex(r => r.base > base);
|
||||||
|
if (idx === -1) this.mmioRegions.push(region);
|
||||||
|
else this.mmioRegions.splice(idx, 0, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(resetVector: number): void {
|
reset(resetVector: number): void {
|
||||||
|
|
@ -84,6 +92,7 @@ export class RiscVCore {
|
||||||
this.mcause = 0;
|
this.mcause = 0;
|
||||||
this.mtval = 0;
|
this.mtval = 0;
|
||||||
this.pendingInterrupt = null;
|
this.pendingInterrupt = null;
|
||||||
|
this._seenUnmapped.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,13 +134,26 @@ export class RiscVCore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public diagnostic accessors ─────────────────────────────────────────
|
||||||
|
/** Current value of mstatus (bit3=MIE, bit7=MPIE). */
|
||||||
|
get mstatusVal(): number { return this.mstatus; }
|
||||||
|
/** Current value of mtvec (trap-vector base + mode). */
|
||||||
|
get mtvecVal(): number { return this.mtvec; }
|
||||||
|
|
||||||
// ── Memory access helpers ───────────────────────────────────────────────
|
// ── Memory access helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
private mmioFor(addr: number): MmioRegion | null {
|
private mmioFor(addr: number): MmioRegion | null {
|
||||||
|
// Regions are sorted by base address; once addr < r.base no later region can match.
|
||||||
|
// Among all matching regions, pick the MOST SPECIFIC (smallest size) so that
|
||||||
|
// narrow handlers take priority over a broad catch-all region.
|
||||||
|
let best: MmioRegion | null = null;
|
||||||
for (const r of this.mmioRegions) {
|
for (const r of this.mmioRegions) {
|
||||||
if (addr >= r.base && addr < r.base + r.size) return r;
|
if (addr < r.base) break;
|
||||||
|
if (addr < r.base + r.size) {
|
||||||
|
if (best === null || r.size < best.size) best = r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
readByte(addr: number): number {
|
readByte(addr: number): number {
|
||||||
|
|
@ -139,6 +161,16 @@ export class RiscVCore {
|
||||||
if (mmio) return mmio.read(addr) & 0xff;
|
if (mmio) return mmio.read(addr) & 0xff;
|
||||||
const off = addr - this.memBase;
|
const off = addr - this.memBase;
|
||||||
if (off >= 0 && off < this.mem.length) return this.mem[off];
|
if (off >= 0 && off < this.mem.length) return this.mem[off];
|
||||||
|
// Log first access to each unique unmapped peripheral word address so we
|
||||||
|
// can identify spin-wait targets that need a stub to return "ready".
|
||||||
|
const uAddr = addr >>> 0;
|
||||||
|
if (uAddr >= 0x60000000 && uAddr < 0x80000000) {
|
||||||
|
const wordAddr = uAddr & ~3;
|
||||||
|
if (!this._seenUnmapped.has(wordAddr)) {
|
||||||
|
this._seenUnmapped.add(wordAddr);
|
||||||
|
console.warn(`[RiscV] unmapped peripheral read @ 0x${wordAddr.toString(16)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,16 +429,33 @@ export class RiscVCore {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RV32C: if bits [1:0] != 0b11, it's a 16-bit compressed instruction
|
// ── Instruction fetch ──────────────────────────────────────────────────
|
||||||
const half = this.readHalf(this.pc);
|
// Fast path: flat memory (IROM / flash) — avoids MMIO scan entirely.
|
||||||
|
const pc = this.pc;
|
||||||
let instr: number;
|
let instr: number;
|
||||||
let instrLen: number;
|
let instrLen: number;
|
||||||
if ((half & 0x3) !== 0x3) {
|
const off0 = pc - this.memBase;
|
||||||
instr = this.decompressC(half);
|
if (off0 >= 0 && off0 + 4 <= this.mem.length) {
|
||||||
instrLen = 2;
|
const b0 = this.mem[off0], b1 = this.mem[off0 + 1];
|
||||||
|
const half0 = (b0 | (b1 << 8)) & 0xffff;
|
||||||
|
if ((half0 & 0x3) !== 0x3) {
|
||||||
|
instr = this.decompressC(half0);
|
||||||
|
instrLen = 2;
|
||||||
|
} else {
|
||||||
|
instr = (half0 | (this.mem[off0 + 2] << 16) | (this.mem[off0 + 3] << 24)) >>> 0;
|
||||||
|
instrLen = 4;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
instr = this.readWord(this.pc);
|
// Slow path: MMIO (IRAM, ROM stub, peripheral-mapped code)
|
||||||
instrLen = 4;
|
const half = this.readHalf(pc);
|
||||||
|
if ((half & 0x3) !== 0x3) {
|
||||||
|
instr = this.decompressC(half);
|
||||||
|
instrLen = 2;
|
||||||
|
} else {
|
||||||
|
const upper = this.readHalf(pc + 2);
|
||||||
|
instr = (half | (upper << 16)) >>> 0;
|
||||||
|
instrLen = 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const opcode = instr & 0x7f;
|
const opcode = instr & 0x7f;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue