diff --git a/frontend/public/boards/esp32c3-rom.bin b/frontend/public/boards/esp32c3-rom.bin new file mode 100644 index 0000000..3f59017 Binary files /dev/null and b/frontend/public/boards/esp32c3-rom.bin differ diff --git a/frontend/src/simulation/Esp32C3Simulator.ts b/frontend/src/simulation/Esp32C3Simulator.ts index 8d5ae6b..f32be96 100644 --- a/frontend/src/simulation/Esp32C3Simulator.ts +++ b/frontend/src/simulation/Esp32C3Simulator.ts @@ -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 { + 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()); diff --git a/frontend/src/simulation/RiscVCore.ts b/frontend/src/simulation/RiscVCore.ts index c9b7bfa..515f046 100644 --- a/frontend/src/simulation/RiscVCore.ts +++ b/frontend/src/simulation/RiscVCore.ts @@ -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