velxio/frontend/src/simulation/RiscVCore.ts

973 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* RiscVCore — Minimal RV32IMC interpreter in TypeScript.
*
* Supports the complete RV32I base ISA (40 instructions) plus:
* RV32M — multiply/divide extension (MUL, MULH, MULHSU, MULHU, DIV, DIVU, REM, REMU)
* RV32C — compressed 16-bit instruction extension (all ~40 instructions, decompressed to 32-bit)
*
* Memory model: flat Uint8Array, caller supplies base address mappings.
* MMIO: caller installs read/write hooks at specific address ranges.
*
* Machine-mode CSR support:
* mstatus, mie, mtvec, mscratch, mepc, mcause, mtval, mcycle/cycle
* MRET, ECALL (cause=11), external interrupt dispatch via triggerInterrupt()
*
* Limitations (acceptable for educational emulation):
* - No privilege levels below M-mode
* - No misalignment exceptions
* - No RV32A (atomic) or floating-point extensions
*/
export type MmioReadHook = (addr: number) => number;
export type MmioWriteHook = (addr: number, value: number) => void;
interface MmioRegion {
base: number;
size: number;
read: MmioReadHook;
write: MmioWriteHook;
}
export class RiscVCore {
/** General-purpose registers x0x31 (x0 is always 0) */
readonly regs = new Int32Array(32);
/** Program counter */
pc = 0x0800_0000;
/** CPU cycle counter */
cycles = 0;
// ── Machine-mode CSR registers ────────────────────────────────────────────
/** 0x300 mstatus — bit3=MIE (global enable), bit7=MPIE, bits[12:11]=MPP */
private mstatus = 0;
/** 0x304 mie — per-source interrupt enable mask */
private mie = 0;
/** 0x305 mtvec — trap-vector base address + mode (bit0=vectored) */
private mtvec = 0;
/** 0x340 mscratch — scratch register (used by FreeRTOS context switch) */
private mscratch = 0;
/** 0x341 mepc — address of interrupted/excepting instruction */
private mepc = 0;
/** 0x342 mcause — trap cause; bit31=interrupt, bits[4:0]=cause number */
private mcause = 0;
/** 0x343 mtval — trap value (fault address / instruction bits) */
private mtval = 0;
/** 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[] = [];
/** 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 memBase Physical base address of `mem` (e.g. 0x08000000 for flash)
*/
constructor(mem: Uint8Array, memBase: number) {
this.mem = mem;
this.memBase = memBase;
}
/**
* 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 {
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 {
this.regs.fill(0);
this.pc = resetVector;
this.cycles = 0;
this.mstatus = 0;
this.mie = 0;
this.mtvec = 0;
this.mscratch = 0;
this.mepc = 0;
this.mcause = 0;
this.mtval = 0;
this.pendingInterrupt = null;
this._resAddr = -1;
this._resValid = false;
this._seenUnmapped.clear();
}
/**
* Raise a machine-level interrupt. The cause is stored and will be taken at
* the next instruction boundary when mstatus.MIE (bit3) is set.
* Bit31=1 for asynchronous interrupts; bits[4:0] = CPU interrupt number.
*/
triggerInterrupt(cause: number): void {
this.pendingInterrupt = cause >>> 0;
}
// ── CSR helpers ─────────────────────────────────────────────────────────
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: {
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;
case 0x341:
this.mepc = val;
break;
case 0x342:
this.mcause = val;
break;
case 0x343:
this.mtval = val;
break;
// cycle counters are read-only; ignore writes
}
}
// ── 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 ───────────────────────────────────────────────
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) {
if (addr < r.base) break;
if (addr < r.base + r.size) {
if (best === null || r.size < best.size) best = r;
}
}
return best;
}
readByte(addr: number): number {
const mmio = this.mmioFor(addr);
if (mmio) return mmio.read(addr) & 0xff;
const off = addr - this.memBase;
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;
}
readHalf(addr: number): number {
return this.readByte(addr) | (this.readByte(addr + 1) << 8);
}
readWord(addr: number): number {
return (
(this.readByte(addr) |
(this.readByte(addr + 1) << 8) |
(this.readByte(addr + 2) << 16) |
(this.readByte(addr + 3) << 24)) >>>
0
);
}
writeByte(addr: number, value: number): void {
const mmio = this.mmioFor(addr);
if (mmio) {
mmio.write(addr, value & 0xff);
return;
}
const off = addr - this.memBase;
if (off >= 0 && off < this.mem.length) this.mem[off] = value & 0xff;
}
writeHalf(addr: number, value: number): void {
this.writeByte(addr, value & 0xff);
this.writeByte(addr + 1, (value >> 8) & 0xff);
}
writeWord(addr: number, value: number): void {
this.writeByte(addr, value & 0xff);
this.writeByte(addr + 1, (value >> 8) & 0xff);
this.writeByte(addr + 2, (value >> 16) & 0xff);
this.writeByte(addr + 3, (value >> 24) & 0xff);
}
// ── Immediate decoders ──────────────────────────────────────────────────
private iImm(instr: number): number {
return ((instr >> 20) << 0) >> 0; // sign-extend [31:20]
}
private sImm(instr: number): number {
const imm = ((instr >> 25) << 5) | ((instr >> 7) & 0x1f);
return (imm << 20) >> 20; // sign-extend 12-bit
}
private bImm(instr: number): number {
const imm =
((instr >> 31) << 12) |
(((instr >> 7) & 1) << 11) |
(((instr >> 25) & 0x3f) << 5) |
(((instr >> 8) & 0xf) << 1);
return (imm << 19) >> 19; // sign-extend 13-bit
}
private uImm(instr: number): number {
return (instr & 0xffff_f000) | 0;
}
private jImm(instr: number): number {
const imm =
((instr >> 31) << 20) |
(((instr >> 12) & 0xff) << 12) |
(((instr >> 20) & 1) << 11) |
(((instr >> 21) & 0x3ff) << 1);
return (imm << 11) >> 11; // sign-extend 21-bit
}
// ── Register helpers ────────────────────────────────────────────────────
private reg(r: number): number {
return r === 0 ? 0 : this.regs[r];
}
private setReg(r: number, v: number): void {
if (r !== 0) this.regs[r] = v;
}
// ── RV32C decompressor ──────────────────────────────────────────────────
/**
* Decompress a 16-bit RV32C instruction to its 32-bit RV32I/M equivalent.
* Returns the equivalent 32-bit instruction word.
*/
private decompressC(half: number): number {
const op = half & 0x3;
const funct3 = (half >> 13) & 0x7;
const bit12 = (half >> 12) & 0x1;
// Sign-extend val from bits bits
const sext = (val: number, bits: number) => (val << (32 - bits)) >> (32 - bits);
// Instruction encoders
const encI = (imm: number, rs1: number, f3: number, rd: number, oc: number) =>
((imm & 0xfff) << 20) |
((rs1 & 0x1f) << 15) |
((f3 & 0x7) << 12) |
((rd & 0x1f) << 7) |
(oc & 0x7f);
const encR = (f7: number, rs2: number, rs1: number, f3: number, rd: number, oc: number) =>
((f7 & 0x7f) << 25) |
((rs2 & 0x1f) << 20) |
((rs1 & 0x1f) << 15) |
((f3 & 0x7) << 12) |
((rd & 0x1f) << 7) |
(oc & 0x7f);
const encS = (imm: number, rs2: number, rs1: number, f3: number, oc: number) =>
(((imm >> 5) & 0x7f) << 25) |
((rs2 & 0x1f) << 20) |
((rs1 & 0x1f) << 15) |
((f3 & 0x7) << 12) |
((imm & 0x1f) << 7) |
(oc & 0x7f);
const encJ = (imm: number, rd: number) => {
const b20 = (imm >> 20) & 1;
const b19_12 = (imm >> 12) & 0xff;
const b11 = (imm >> 11) & 1;
const b10_1 = (imm >> 1) & 0x3ff;
return (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12) | ((rd & 0x1f) << 7) | 0x6f;
};
const encB = (imm: number, rs2: number, rs1: number, f3: number) => {
const b12 = (imm >> 12) & 1;
const b11 = (imm >> 11) & 1;
const b10_5 = (imm >> 5) & 0x3f;
const b4_1 = (imm >> 1) & 0xf;
return (
(b12 << 31) |
(b10_5 << 25) |
((rs2 & 0x1f) << 20) |
((rs1 & 0x1f) << 15) |
((f3 & 7) << 12) |
(b4_1 << 8) |
(b11 << 7) |
0x63
);
};
// CJ-format 11-bit signed offset (scrambled bit positions per spec Table 16.6)
const cjOff = () =>
sext(
(bit12 << 11) |
(((half >> 11) & 1) << 4) |
(((half >> 9) & 3) << 8) |
(((half >> 8) & 1) << 10) |
(((half >> 7) & 1) << 6) |
(((half >> 6) & 1) << 7) |
(((half >> 3) & 7) << 1) |
(((half >> 2) & 1) << 5),
12,
);
// CB-format 8-bit signed offset
const cbOff = () =>
sext(
(bit12 << 8) |
(((half >> 10) & 3) << 3) |
(((half >> 5) & 3) << 6) |
(((half >> 3) & 3) << 1) |
(((half >> 2) & 1) << 5),
9,
);
// ── Quadrant 0 (op=00) ──────────────────────────────────────────────
if (op === 0) {
const rdp = ((half >> 2) & 7) + 8; // rd' → x(8..15)
const rs1p = ((half >> 7) & 7) + 8; // rs1' → x(8..15)
switch (funct3) {
case 0: {
// C.ADDI4SPN → ADDI rd', sp, nzuimm
const nzuimm =
(((half >> 7) & 0xf) << 6) |
(((half >> 11) & 0x3) << 4) |
(((half >> 5) & 1) << 3) |
(((half >> 6) & 1) << 2);
return encI(nzuimm, 2, 0, rdp, 0x13);
}
case 2: {
// C.LW → LW rd', offset(rs1')
const off =
(((half >> 10) & 7) << 3) | (((half >> 6) & 1) << 2) | (((half >> 5) & 1) << 6);
return encI(off, rs1p, 2, rdp, 0x03);
}
case 6: {
// C.SW → SW rs2', offset(rs1')
const off =
(((half >> 10) & 7) << 3) | (((half >> 6) & 1) << 2) | (((half >> 5) & 1) << 6);
return encS(off, rdp, rs1p, 2, 0x23); // rdp plays role of rs2' in CS format
}
default:
return 0x00000013; // reserved → NOP
}
}
// ── Quadrant 1 (op=01) ──────────────────────────────────────────────
if (op === 1) {
const rd = (half >> 7) & 0x1f;
const rs1p = ((half >> 7) & 7) + 8;
const rs2p = ((half >> 2) & 7) + 8;
const imm6 = sext((bit12 << 5) | ((half >> 2) & 0x1f), 6);
switch (funct3) {
case 0: // C.NOP / C.ADDI → ADDI rd, rd, imm
return encI(imm6, rd, 0, rd, 0x13);
case 1: // C.JAL (RV32C only) → JAL x1, offset
return encJ(cjOff(), 1);
case 2: // C.LI → ADDI rd, x0, imm
return encI(imm6, 0, 0, rd, 0x13);
case 3: {
if (rd === 2) {
// C.ADDI16SP → ADDI sp, sp, nzimm
const nzimm = sext(
(bit12 << 9) |
(((half >> 6) & 1) << 4) |
(((half >> 5) & 1) << 6) |
(((half >> 3) & 3) << 7) |
(((half >> 2) & 1) << 5),
10,
);
return encI(nzimm, 2, 0, 2, 0x13);
} else {
// C.LUI → LUI rd, nzimm
const nzimm = sext((bit12 << 17) | (((half >> 2) & 0x1f) << 12), 18);
return (nzimm & 0xfffff000) | ((rd & 0x1f) << 7) | 0x37;
}
}
case 4: {
const f2 = (half >> 10) & 0x3;
const sh = (bit12 << 5) | ((half >> 2) & 0x1f);
if (f2 === 0) return encI(sh, rs1p, 5, rs1p, 0x13); // C.SRLI → SRLI
if (f2 === 1) return encI(0x400 | sh, rs1p, 5, rs1p, 0x13); // C.SRAI → SRAI (bit10=1)
if (f2 === 2) return encI(imm6, rs1p, 7, rs1p, 0x13); // C.ANDI → ANDI
// f2 === 3: C.SUB / C.XOR / C.OR / C.AND
const op2 = (half >> 5) & 3;
if (!bit12) {
switch (op2) {
case 0:
return encR(0x20, rs2p, rs1p, 0, rs1p, 0x33); // C.SUB (funct7=0x20)
case 1:
return encR(0, rs2p, rs1p, 4, rs1p, 0x33); // C.XOR
case 2:
return encR(0, rs2p, rs1p, 6, rs1p, 0x33); // C.OR
case 3:
return encR(0, rs2p, rs1p, 7, rs1p, 0x33); // C.AND
}
}
return 0x00000013; // C.SUBW etc. (RV64 only) → NOP
}
case 5: // C.J → JAL x0, offset
return encJ(cjOff(), 0);
case 6: // C.BEQZ → BEQ rs1', x0, offset
return encB(cbOff(), 0, rs1p, 0);
case 7: // C.BNEZ → BNE rs1', x0, offset
return encB(cbOff(), 0, rs1p, 1);
default:
return 0x00000013;
}
}
// ── Quadrant 2 (op=10) ──────────────────────────────────────────────
if (op === 2) {
const rd = (half >> 7) & 0x1f;
const rs2 = (half >> 2) & 0x1f;
switch (funct3) {
case 0: {
// C.SLLI → SLLI rd, rd, shamt
const sh = (bit12 << 5) | rs2;
return encI(sh, rd, 1, rd, 0x13);
}
case 2: {
// C.LWSP → LW rd, offset(sp)
// uimm[7:6]=bits[3:2], uimm[5]=bit12, uimm[4:2]=bits[6:4]
const off = (((half >> 2) & 3) << 6) | (bit12 << 5) | (((half >> 4) & 7) << 2);
return encI(off, 2, 2, rd, 0x03);
}
case 4: {
if (!bit12) {
if (rs2 === 0) return encI(0, rd, 0, 0, 0x67); // C.JR → JALR x0, 0(rd)
return encR(0, rs2, 0, 0, rd, 0x33); // C.MV → ADD rd, x0, rs2
} else {
if (rd === 0 && rs2 === 0) return 0x00100073; // C.EBREAK
if (rs2 === 0) return encI(0, rd, 0, 1, 0x67); // C.JALR → JALR x1, 0(rd)
return encR(0, rs2, rd, 0, rd, 0x33); // C.ADD → ADD rd, rd, rs2
}
}
case 6: {
// C.SWSP → SW rs2, offset(sp)
// uimm[7:6]=bits[8:7], uimm[5:2]=bits[12:9]
const off = (((half >> 7) & 3) << 6) | (((half >> 9) & 0xf) << 2);
return encS(off, rs2, 2, 2, 0x23);
}
default:
return 0x00000013;
}
}
return 0x00000013; // should not reach (op=11 means 32-bit instruction)
}
// ── Single instruction step ─────────────────────────────────────────────
/**
* Execute one instruction. Returns the number of cycles consumed (always 1
* for this simple model — real chips have variable latency).
*/
step(): number {
// ── Interrupt check ───────────────────────────────────────────────────
// Take a pending interrupt if global interrupt enable (mstatus.MIE) is set.
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
this.mepc = this.pc;
this.mcause = cause;
const intNum = cause & 0x1f;
// Vectored mode (mtvec[1:0]==1): PC = base + 4*intNum
// Direct mode (mtvec[1:0]==0): PC = base
this.pc =
(this.mtvec & 3) === 1
? ((this.mtvec & ~3) >>> 0) + (intNum << 2)
: (this.mtvec & ~3) >>> 0;
this.cycles++;
return 1;
}
// ── Instruction fetch ──────────────────────────────────────────────────
// Fast path: flat memory (IROM / flash) — avoids MMIO scan entirely.
const pc = this.pc;
let instr: number;
let instrLen: number;
const off0 = pc - this.memBase;
if (off0 >= 0 && off0 + 4 <= this.mem.length) {
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 {
// Slow path: MMIO (IRAM, ROM stub, peripheral-mapped code)
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 rd = (instr >> 7) & 0x1f;
const funct3 = (instr >> 12) & 0x07;
const rs1 = (instr >> 15) & 0x1f;
const rs2 = (instr >> 20) & 0x1f;
const funct7 = (instr >> 25) & 0x7f;
let nextPc = (this.pc + instrLen) >>> 0;
switch (opcode) {
// LUI
case 0x37:
this.setReg(rd, this.uImm(instr));
break;
// AUIPC
case 0x17:
this.setReg(rd, (this.pc + this.uImm(instr)) | 0);
break;
// JAL
case 0x6f: {
const target = (this.pc + this.jImm(instr)) >>> 0;
this.setReg(rd, nextPc);
nextPc = target;
break;
}
// JALR
case 0x67: {
const target = (this.reg(rs1) + this.iImm(instr)) & ~1;
this.setReg(rd, nextPc);
nextPc = target >>> 0;
break;
}
// BRANCH
case 0x63: {
const a = this.reg(rs1);
const b = this.reg(rs2);
let taken = false;
switch (funct3) {
case 0x0:
taken = a === b;
break; // BEQ
case 0x1:
taken = a !== b;
break; // BNE
case 0x4:
taken = a < b;
break; // BLT (signed)
case 0x5:
taken = a >= b;
break; // BGE (signed)
case 0x6:
taken = a >>> 0 < b >>> 0;
break; // BLTU
case 0x7:
taken = a >>> 0 >= b >>> 0;
break; // BGEU
}
if (taken) nextPc = (this.pc + this.bImm(instr)) >>> 0;
break;
}
// LOAD
case 0x03: {
const addr = (this.reg(rs1) + this.iImm(instr)) >>> 0;
let val: number;
switch (funct3) {
case 0x0:
val = (this.readByte(addr) << 24) >> 24;
break; // LB
case 0x1:
val = (this.readHalf(addr) << 16) >> 16;
break; // LH
case 0x2:
val = this.readWord(addr) | 0;
break; // LW
case 0x4:
val = this.readByte(addr);
break; // LBU
case 0x5:
val = this.readHalf(addr);
break; // LHU
default:
val = 0;
}
this.setReg(rd, val);
break;
}
// STORE
case 0x23: {
const addr = (this.reg(rs1) + this.sImm(instr)) >>> 0;
const val = this.reg(rs2);
switch (funct3) {
case 0x0:
this.writeByte(addr, val);
break; // SB
case 0x1:
this.writeHalf(addr, val);
break; // SH
case 0x2:
this.writeWord(addr, val);
break; // SW
}
break;
}
// OP-IMM
case 0x13: {
const a = this.reg(rs1);
const imm = this.iImm(instr);
let val: number;
switch (funct3) {
case 0x0:
val = a + imm;
break; // ADDI
case 0x1:
val = a << (imm & 0x1f);
break; // SLLI
case 0x2:
val = a < imm ? 1 : 0;
break; // SLTI
case 0x3:
val = a >>> 0 < imm >>> 0 ? 1 : 0;
break; // SLTIU
case 0x4:
val = a ^ imm;
break; // XORI
case 0x5:
val =
funct7 === 0x20 // SRLI/SRAI
? a >> (imm & 0x1f)
: a >>> (imm & 0x1f);
break;
case 0x6:
val = a | imm;
break; // ORI
case 0x7:
val = a & imm;
break; // ANDI
default:
val = 0;
}
this.setReg(rd, val);
break;
}
// OP (registerregister) — includes RV32M multiply/divide (funct7=1)
case 0x33: {
const a = this.reg(rs1);
const b = this.reg(rs2);
let val: number;
switch ((funct7 << 3) | funct3) {
// RV32I
case 0x000:
val = a + b;
break; // ADD
case 0x100:
val = a - b;
break; // SUB
case 0x001:
val = a << (b & 0x1f);
break; // SLL
case 0x002:
val = a < b ? 1 : 0;
break; // SLT
case 0x003:
val = a >>> 0 < b >>> 0 ? 1 : 0;
break; // SLTU
case 0x004:
val = a ^ b;
break; // XOR
case 0x005:
val = a >>> (b & 0x1f);
break; // SRL
case 0x105:
val = a >> (b & 0x1f);
break; // SRA
case 0x006:
val = a | b;
break; // OR
case 0x007:
val = a & b;
break; // AND
// RV32M (funct7=1 → cases 0x0080x00f)
case 0x008:
val = Math.imul(a, b);
break; // MUL (lower 32 bits)
case 0x009:
val = Number((BigInt(a) * BigInt(b)) >> 32n) | 0;
break; // MULH (s×s upper)
case 0x00a:
val = Number((BigInt(a) * BigInt(b >>> 0)) >> 32n) | 0;
break; // MULHSU (s×u upper)
case 0x00b:
val = Number((BigInt(a >>> 0) * BigInt(b >>> 0)) >> 32n) >>> 0;
break; // MULHU (u×u upper)
case 0x00c:
val = b === 0 ? -1 : (a / b) | 0;
break; // DIV
case 0x00d:
val = b === 0 ? -1 : ((a >>> 0) / (b >>> 0)) | 0;
break; // DIVU
case 0x00e:
val = b === 0 ? a : (a % b) | 0;
break; // REM
case 0x00f:
val = b === 0 ? a | 0 : ((a >>> 0) % (b >>> 0)) | 0;
break; // REMU
default:
val = 0;
}
this.setReg(rd, val);
break;
}
// MISC-MEM (FENCE — no-op in single-hart emulator)
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;
if (funct3 === 0) {
// 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
this.mcause = 11; // ecall from M-mode
nextPc = (this.mtvec & ~3) >>> 0; // always direct for exceptions
}
// EBREAK (0x001), WFI (0x105) → no-op (advance PC normally)
break;
}
// CSR instructions (funct3 != 0)
const csrAddr = funct12;
const csrOld = this.readCsr(csrAddr);
const isImm = (funct3 & 4) !== 0; // CSRRWI / CSRRSI / CSRRCI
const operand = isImm ? rs1 : this.reg(rs1); // zimm (5-bit) or register
this.setReg(rd, csrOld);
switch (funct3 & 3) {
case 1:
this.writeCsr(csrAddr, operand);
break; // CSRRW/I
case 2:
if (operand !== 0) this.writeCsr(csrAddr, csrOld | operand);
break; // CSRRS/I
case 3:
if (operand !== 0) this.writeCsr(csrAddr, csrOld & ~operand);
break; // CSRRC/I
}
break;
}
default:
// Unknown opcode — skip instruction to avoid infinite loop
break;
}
this.pc = nextPc;
this.cycles++;
return 1;
}
}