chore: clean up empty code change sections in the changes log
parent
dcea546e45
commit
0e7ef38104
Binary file not shown.
|
|
@ -74,10 +74,26 @@ 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 Controller (no-op passthrough) @ 0x600C5000 ───────────────────
|
||||
// FreeRTOS configures source→CPU-int routing here; we handle routing ourselves.
|
||||
const INTC_BASE = 0x600C5000;
|
||||
const INTC_SIZE = 0x800;
|
||||
// ── 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
|
||||
|
|
@ -110,6 +126,23 @@ export class Esp32C3Simulator {
|
|||
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
|
||||
|
|
@ -128,6 +161,12 @@ export class Esp32C3Simulator {
|
|||
private _dbgPrevTickPc = -1;
|
||||
private _dbgSamePcCount = 0;
|
||||
private _dbgStuckDumped = false;
|
||||
/** Ring buffer of sampled PCs — dumped when stuck detector fires. */
|
||||
private _pcTrace = new Uint32Array(128);
|
||||
private _pcTraceIdx = 0;
|
||||
private _pcTraceStep = 0;
|
||||
/** Count of ROM function calls (for logging first N). */
|
||||
private _romCallCount = 0;
|
||||
|
||||
public pinManager: PinManager;
|
||||
public onSerialData: ((ch: string) => void) | null = null;
|
||||
|
|
@ -172,7 +211,8 @@ export class Esp32C3Simulator {
|
|||
this._registerUart0();
|
||||
this._registerGpio();
|
||||
this._registerSysTimer();
|
||||
this._registerIntCtrl();
|
||||
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.
|
||||
|
|
@ -186,6 +226,15 @@ export class Esp32C3Simulator {
|
|||
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;
|
||||
|
|
@ -288,6 +337,10 @@ export class Esp32C3Simulator {
|
|||
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
|
||||
|
|
@ -301,45 +354,258 @@ export class Esp32C3Simulator {
|
|||
);
|
||||
}
|
||||
|
||||
/** Interrupt-controller MMIO — FreeRTOS writes source→CPU-int routing here.
|
||||
* We handle routing via direct triggerInterrupt() calls; unknown offsets
|
||||
* echo back the last written value so that read-back verification succeeds. */
|
||||
private _registerIntCtrl(): void {
|
||||
// ── 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;
|
||||
this.core.addMmio(INTC_BASE, INTC_SIZE,
|
||||
const BASE = INTMATRIX_BASE;
|
||||
let logCount = 0;
|
||||
|
||||
this.core.addMmio(BASE, INTMATRIX_SIZE,
|
||||
(addr) => {
|
||||
const wordAddr = addr & ~3;
|
||||
const word = peri.get(wordAddr) ?? 0;
|
||||
return (word >>> ((addr & 3) * 8)) & 0xFF;
|
||||
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;
|
||||
peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
|
||||
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) {
|
||||
const oldLine = this._intSrcMap[src];
|
||||
this._intSrcMap[src] = newWord & 0x1F;
|
||||
if ((newWord & 0x1F) !== oldLine && logCount < 30) {
|
||||
logCount++;
|
||||
console.log(`[INTMATRIX] src ${src} → CPU line ${newWord & 0x1F}`);
|
||||
}
|
||||
}
|
||||
} else if (off === 0x104) {
|
||||
this._intLineEnable = newWord;
|
||||
if (logCount < 30) {
|
||||
logCount++;
|
||||
console.log(`[INTMATRIX] ENABLE = 0x${newWord.toString(16)}`);
|
||||
}
|
||||
} 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;
|
||||
if (logCount < 30) {
|
||||
logCount++;
|
||||
console.log(`[INTMATRIX] THRESH = ${newWord & 0xF}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 _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) => {},
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Second ROM region (0x40800000) — same stub. */
|
||||
// ── 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) — serves the real ROM binary when
|
||||
* loaded, or falls back to C.RET (0x8082) with a0=0 if the binary is
|
||||
* unavailable.
|
||||
*/
|
||||
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];
|
||||
}
|
||||
// Fallback: detect instruction fetch and set a0=0 for C.RET stub
|
||||
if ((addr & 1) === 0 && (addr >>> 0) === (core.pc >>> 0)) {
|
||||
core.regs[10] = 0; // a0 = 0 (ESP_OK)
|
||||
if (++this._romCallCount <= 50) {
|
||||
console.log(
|
||||
`[ROM] stub call #${this._romCallCount} → 0x${(addr >>> 0).toString(16)}` +
|
||||
` ra=0x${(core.regs[1] >>> 0).toString(16)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return (addr & 1) === 0 ? 0x82 : 0x80;
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
/** Second ROM region (0x40800000) — same stub with a0=0. */
|
||||
private _registerRomStub2(): void {
|
||||
const core = this.core;
|
||||
this.core.addMmio(ROM2_BASE, ROM2_SIZE,
|
||||
(addr) => (addr & 1) === 0 ? 0x82 : 0x80,
|
||||
(_addr, _val) => {},
|
||||
(addr) => {
|
||||
if ((addr & 1) === 0 && (addr >>> 0) === (core.pc >>> 0)) {
|
||||
core.regs[10] = 0;
|
||||
if (++this._romCallCount <= 50) {
|
||||
console.log(
|
||||
`[ROM2] call #${this._romCallCount} → 0x${(addr >>> 0).toString(16)}` +
|
||||
` ra=0x${(core.regs[1] >>> 0).toString(16)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return (addr & 1) === 0 ? 0x82 : 0x80;
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -368,12 +634,20 @@ export class Esp32C3Simulator {
|
|||
}
|
||||
if (wOff === 0x68) {
|
||||
// TIMG_RTCCALICFG: bit15=TIMG_RTC_CALI_RDY=1 — calibration instantly done
|
||||
const word = (1 << 15); // 0x00008000
|
||||
// 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 = (1000000 << 7); // 0x07A12000
|
||||
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
|
||||
|
|
@ -657,6 +931,13 @@ export class Esp32C3Simulator {
|
|||
this._dbgTickCount = 0;
|
||||
this._dbgLastMtvec = 0;
|
||||
this._dbgMieEnabled = false;
|
||||
this._dbgPrevTickPc = -1;
|
||||
this._dbgSamePcCount = 0;
|
||||
this._dbgStuckDumped = false;
|
||||
this._pcTrace.fill(0);
|
||||
this._pcTraceIdx = 0;
|
||||
this._pcTraceStep = 0;
|
||||
this._romCallCount = 0;
|
||||
console.log(`[ESP32-C3] Simulation started, entry=0x${this.core.pc.toString(16)}`);
|
||||
this.running = true;
|
||||
this._loop();
|
||||
|
|
@ -679,6 +960,13 @@ export class Esp32C3Simulator {
|
|||
this._dbgTickCount = 0;
|
||||
this._dbgLastMtvec = 0;
|
||||
this._dbgMieEnabled = false;
|
||||
this._dbgPrevTickPc = -1;
|
||||
this._dbgSamePcCount = 0;
|
||||
this._dbgStuckDumped = false;
|
||||
this._pcTrace.fill(0);
|
||||
this._pcTraceIdx = 0;
|
||||
this._pcTraceStep = 0;
|
||||
this._romCallCount = 0;
|
||||
this.dram.fill(0);
|
||||
this.iram.fill(0);
|
||||
this.core.reset(IROM_BASE);
|
||||
|
|
@ -749,21 +1037,34 @@ export class Esp32C3Simulator {
|
|||
const n = rem < CYCLES_PER_TICK ? rem : CYCLES_PER_TICK;
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.core.step();
|
||||
// Sample PC every 500 steps into a ring buffer for post-mortem analysis
|
||||
if (++this._pcTraceStep >= 500) {
|
||||
this._pcTraceStep = 0;
|
||||
this._pcTrace[this._pcTraceIdx] = this.core.pc >>> 0;
|
||||
this._pcTraceIdx = (this._pcTraceIdx + 1) & 127;
|
||||
}
|
||||
}
|
||||
rem -= n;
|
||||
|
||||
this._dbgTickCount++;
|
||||
// Log every 100 ticks (0.1 s) while still early in boot.
|
||||
if (this._dbgTickCount <= 1000 && this._dbgTickCount % 100 === 0) {
|
||||
// Log frequently early in boot (every 10 ticks for first 50, then every 100)
|
||||
const shouldLog = this._dbgTickCount <= 50
|
||||
? this._dbgTickCount % 10 === 0
|
||||
: this._dbgTickCount <= 1000 && this._dbgTickCount % 100 === 0;
|
||||
if (shouldLog) {
|
||||
const spc = this.core.pc;
|
||||
let instrInfo = '';
|
||||
const iramOff = spc - IRAM_BASE;
|
||||
const flashOff = spc - IROM_BASE;
|
||||
const romOff = spc - ROM_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]];
|
||||
} else if (romOff >= 0 && romOff < ROM_SIZE) {
|
||||
// PC is in ROM stub region — show 0x8082 (C.RET)
|
||||
ib0 = 0x82; ib1 = 0x80;
|
||||
}
|
||||
const instr16 = ib0 | (ib1 << 8);
|
||||
const instr32 = ((ib0 | (ib1<<8) | (ib2<<16) | (ib3<<24)) >>> 0);
|
||||
|
|
@ -816,6 +1117,31 @@ export class Esp32C3Simulator {
|
|||
console.warn(` x${i.toString().padStart(2)}(${regNames[i].padEnd(4)}) = 0x${(this.core.regs[i] >>> 0).toString(16).padStart(8, '0')}`);
|
||||
}
|
||||
console.warn(` mstatus=0x${(this.core.mstatusVal >>> 0).toString(16)} mtvec=0x${(this.core.mtvecVal >>> 0).toString(16)}`);
|
||||
// Dump sampled PC trace (oldest → newest)
|
||||
const traceEntries: string[] = [];
|
||||
for (let j = 0; j < 128; j++) {
|
||||
const idx = (this._pcTraceIdx + j) & 127;
|
||||
const tpc = this._pcTrace[idx];
|
||||
if (tpc !== 0) traceEntries.push(`0x${tpc.toString(16).padStart(8,'0')}`);
|
||||
}
|
||||
if (traceEntries.length > 0) {
|
||||
// Deduplicate consecutive entries for readability
|
||||
const deduped: string[] = [];
|
||||
let prev = '';
|
||||
let count = 0;
|
||||
for (const e of traceEntries) {
|
||||
if (e === prev) { count++; }
|
||||
else {
|
||||
if (count > 1) deduped.push(` (×${count})`);
|
||||
deduped.push(e);
|
||||
prev = e;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
if (count > 1) deduped.push(` (×${count})`);
|
||||
console.warn(` PC trace (sampled every 500 steps, ${deduped.length} entries):`);
|
||||
console.warn(' ' + deduped.join(', '));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._dbgSamePcCount = 0;
|
||||
|
|
@ -824,9 +1150,11 @@ export class Esp32C3Simulator {
|
|||
this._dbgPrevTickPc = curPc;
|
||||
}
|
||||
|
||||
// Raise SYSTIMER TARGET0 alarm → CPU interrupt 1 (FreeRTOS tick).
|
||||
// Raise SYSTIMER TARGET0 alarm → routed through interrupt matrix.
|
||||
this._stIntRaw |= 1;
|
||||
this.core.triggerInterrupt(0x80000001);
|
||||
if (this._stIntEna & 1) {
|
||||
this._raiseIntSource(ETS_SYSTIMER_TARGET0_SRC);
|
||||
}
|
||||
}
|
||||
|
||||
this.animFrameId = requestAnimationFrame(() => this._loop());
|
||||
|
|
|
|||
|
|
@ -54,6 +54,19 @@ export class RiscVCore {
|
|||
/** Pending async interrupt cause (bit31=1). Null when none pending. */
|
||||
pendingInterrupt: number | null = null;
|
||||
|
||||
/**
|
||||
* Callback fired whenever mstatus.MIE transitions from 0 → 1.
|
||||
* The interrupt matrix uses this to scan pending sources and inject the
|
||||
* highest-priority interrupt immediately after re-enable.
|
||||
*/
|
||||
onMieEnabled: (() => void) | null = null;
|
||||
|
||||
// ── RV32A reservation state ───────────────────────────────────────────────
|
||||
/** Address of the load-reserved (lr.w) reservation, or -1 if none. */
|
||||
private _resAddr = -1;
|
||||
/** Whether the reservation is valid (cleared on sc.w, trap, or context switch). */
|
||||
private _resValid = false;
|
||||
|
||||
private readonly mem: Uint8Array;
|
||||
private readonly memBase: number;
|
||||
private readonly mmioRegions: MmioRegion[] = [];
|
||||
|
|
@ -92,6 +105,8 @@ export class RiscVCore {
|
|||
this.mcause = 0;
|
||||
this.mtval = 0;
|
||||
this.pendingInterrupt = null;
|
||||
this._resAddr = -1;
|
||||
this._resValid = false;
|
||||
this._seenUnmapped.clear();
|
||||
}
|
||||
|
||||
|
|
@ -109,21 +124,32 @@ export class RiscVCore {
|
|||
private readCsr(addr: number): number {
|
||||
switch (addr) {
|
||||
case 0x300: return this.mstatus;
|
||||
case 0x301: return 0x40001105; // misa: RV32IMAC (MXL=01, I+M+A+C)
|
||||
case 0x304: return this.mie;
|
||||
case 0x305: return this.mtvec;
|
||||
case 0x340: return this.mscratch;
|
||||
case 0x341: return this.mepc;
|
||||
case 0x342: return this.mcause;
|
||||
case 0x343: return this.mtval;
|
||||
case 0x344: return this.pendingInterrupt !== null ? (1 << 11) : 0; // mip: MEIP
|
||||
case 0xB00: case 0xC00: return this.cycles >>> 0; // mcycle / cycle (low 32)
|
||||
case 0xB80: case 0xC80: return 0; // mcycleh / cycleh
|
||||
case 0xF11: return 0; // mvendorid
|
||||
case 0xF12: return 0; // marchid
|
||||
case 0xF13: return 0; // mimpid
|
||||
case 0xF14: return 0; // mhartid (always 0 — single-hart)
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private writeCsr(addr: number, val: number): void {
|
||||
switch (addr) {
|
||||
case 0x300: this.mstatus = val; break;
|
||||
case 0x300: {
|
||||
const oldMie = this.mstatus & 0x8;
|
||||
this.mstatus = val;
|
||||
if (!oldMie && (val & 0x8) && this.onMieEnabled) this.onMieEnabled();
|
||||
break;
|
||||
}
|
||||
case 0x304: this.mie = val; break;
|
||||
case 0x305: this.mtvec = val; break;
|
||||
case 0x340: this.mscratch = val; break;
|
||||
|
|
@ -414,6 +440,7 @@ export class RiscVCore {
|
|||
if (this.pendingInterrupt !== null && (this.mstatus & 0x8)) {
|
||||
const cause = this.pendingInterrupt;
|
||||
this.pendingInterrupt = null;
|
||||
this._resValid = false; // Clear reservation on trap
|
||||
const mieOld = (this.mstatus >> 3) & 1; // current MIE
|
||||
this.mstatus = (this.mstatus & ~0x88) // clear MPIE (bit7) and MIE (bit3)
|
||||
| (mieOld << 7); // MPIE = old MIE
|
||||
|
|
@ -598,6 +625,92 @@ export class RiscVCore {
|
|||
case 0x0f:
|
||||
break;
|
||||
|
||||
// ATOMIC (RV32A) — opcode 0x2F
|
||||
case 0x2F: {
|
||||
if (funct3 === 2) { // .W (word) operations
|
||||
const funct5 = funct7 >> 2;
|
||||
const addr = this.reg(rs1) >>> 0;
|
||||
switch (funct5) {
|
||||
case 0x02: { // LR.W — Load-Reserved
|
||||
const val = this.readWord(addr) | 0;
|
||||
this.setReg(rd, val);
|
||||
this._resAddr = addr;
|
||||
this._resValid = true;
|
||||
break;
|
||||
}
|
||||
case 0x03: { // SC.W — Store-Conditional
|
||||
if (this._resValid && this._resAddr === addr) {
|
||||
this.writeWord(addr, this.reg(rs2));
|
||||
this.setReg(rd, 0); // 0 = success
|
||||
} else {
|
||||
this.setReg(rd, 1); // 1 = failure
|
||||
}
|
||||
this._resValid = false;
|
||||
break;
|
||||
}
|
||||
case 0x01: { // AMOSWAP.W
|
||||
const old = this.readWord(addr) | 0;
|
||||
this.writeWord(addr, this.reg(rs2));
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x00: { // AMOADD.W
|
||||
const old = this.readWord(addr) | 0;
|
||||
this.writeWord(addr, (old + this.reg(rs2)) | 0);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x04: { // AMOXOR.W
|
||||
const old = this.readWord(addr) | 0;
|
||||
this.writeWord(addr, (old ^ this.reg(rs2)) | 0);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x0C: { // AMOAND.W
|
||||
const old = this.readWord(addr) | 0;
|
||||
this.writeWord(addr, (old & this.reg(rs2)) | 0);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x08: { // AMOOR.W
|
||||
const old = this.readWord(addr) | 0;
|
||||
this.writeWord(addr, (old | this.reg(rs2)) | 0);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x10: { // AMOMIN.W (signed)
|
||||
const old = this.readWord(addr) | 0;
|
||||
const b = this.reg(rs2);
|
||||
this.writeWord(addr, old < b ? old : b);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x14: { // AMOMAX.W (signed)
|
||||
const old = this.readWord(addr) | 0;
|
||||
const b = this.reg(rs2);
|
||||
this.writeWord(addr, old > b ? old : b);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x18: { // AMOMINU.W (unsigned)
|
||||
const old = this.readWord(addr) | 0;
|
||||
const b = this.reg(rs2);
|
||||
this.writeWord(addr, (old >>> 0) < (b >>> 0) ? old : b);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
case 0x1C: { // AMOMAXU.W (unsigned)
|
||||
const old = this.readWord(addr) | 0;
|
||||
const b = this.reg(rs2);
|
||||
this.writeWord(addr, (old >>> 0) > (b >>> 0) ? old : b);
|
||||
this.setReg(rd, old);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// SYSTEM — CSR instructions, MRET, ECALL, EBREAK, WFI
|
||||
case 0x73: {
|
||||
const funct12 = (instr >> 20) & 0xfff;
|
||||
|
|
@ -605,13 +718,16 @@ export class RiscVCore {
|
|||
// Privileged instructions (not CSR)
|
||||
if (funct12 === 0x302) {
|
||||
// MRET — return from machine trap
|
||||
const oldMie = this.mstatus & 0x8;
|
||||
const mpie = (this.mstatus >> 7) & 1;
|
||||
this.mstatus = (this.mstatus & ~0x8) | (mpie << 3); // MIE = MPIE
|
||||
this.mstatus |= (1 << 7); // MPIE = 1
|
||||
nextPc = this.mepc >>> 0;
|
||||
if (!oldMie && (this.mstatus & 0x8) && this.onMieEnabled) this.onMieEnabled();
|
||||
} else if (funct12 === 0x000) {
|
||||
// ECALL — synchronous exception (cause=11 for M-mode)
|
||||
// Used by FreeRTOS portYIELD to trigger a context switch.
|
||||
this._resValid = false; // Clear reservation on trap
|
||||
const mieOld = (this.mstatus >> 3) & 1;
|
||||
this.mstatus = (this.mstatus & ~0x88) | (mieOld << 7);
|
||||
this.mepc = this.pc; // points at ecall; trap handler adds +4
|
||||
|
|
|
|||
Loading…
Reference in New Issue