velxio/frontend/src/simulation/RiscVCore.ts

294 lines
9.9 KiB
TypeScript
Raw Normal View History

/**
* RiscVCore Minimal RV32I base ISA interpreter in TypeScript.
*
* Supports the complete RV32I instruction set (40 instructions):
* LUI, AUIPC, JAL, JALR, BRANCH, LOAD, STORE, OP-IMM, OP, FENCE, SYSTEM
*
* Memory model: flat Uint8Array, caller supplies base address mappings.
* MMIO: caller installs read/write hooks at specific address ranges.
*
* Limitations (acceptable for educational emulation):
* - No privilege levels / CSR side-effects (CSR reads return 0)
* - No interrupts / exceptions (ECALL/EBREAK are no-ops)
* - No misalignment exceptions
* - No compressed (RV32C) or multiply (RV32M) 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;
private readonly mem: Uint8Array;
private readonly memBase: number;
private readonly mmioRegions: MmioRegion[] = [];
/**
* @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. */
addMmio(base: number, size: number, read: MmioReadHook, write: MmioWriteHook): void {
this.mmioRegions.push({ base, size, read, write });
}
reset(resetVector: number): void {
this.regs.fill(0);
this.pc = resetVector;
this.cycles = 0;
}
// ── Memory access helpers ───────────────────────────────────────────────
private mmioFor(addr: number): MmioRegion | null {
for (const r of this.mmioRegions) {
if (addr >= r.base && addr < r.base + r.size) return r;
}
return null;
}
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];
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; }
// ── 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 {
const instr = this.readWord(this.pc);
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 + 4) >>> 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)
case 0x33: {
const a = this.reg(rs1);
const b = this.reg(rs2);
let val: number;
switch ((funct7 << 3) | funct3) {
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
default: val = 0;
}
this.setReg(rd, val);
break;
}
// MISC-MEM (FENCE — no-op in single-hart emulator)
case 0x0f:
break;
// SYSTEM (ECALL, EBREAK, CSR* — treat as no-op)
case 0x73:
break;
default:
// Unknown opcode — skip instruction to avoid infinite loop
break;
}
this.pc = nextPc;
this.cycles++;
return 1;
}
}