velxio/frontend/src/simulation/RiscVCore.ts

973 lines
32 KiB
TypeScript
Raw Normal View History

/**
* 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;
}
}