294 lines
9.9 KiB
TypeScript
294 lines
9.9 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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 x0–x31 (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 (register–register)
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
}
|