#!/usr/bin/env python3 """Small RV32I reference model used to generate processor test goldens.""" from __future__ import annotations import argparse from pathlib import Path MASK32 = 0xFFFF_FFFF ISRAM_BASE = 0x0000 DSRAM_BASE = 0x1000 MEMORY_BYTES = 4096 EBREAK = 0x0010_0073 def sign_extend(value: int, bits: int) -> int: sign = 1 << (bits - 1) return (value & (sign - 1)) - (value & sign) def signed32(value: int) -> int: return sign_extend(value & MASK32, 32) def read_words(path: Path | None) -> list[int]: if path is None or not path.exists(): return [] words = [] for line_number, line in enumerate(path.read_text().splitlines(), 1): token = line.split("#", 1)[0].strip() if token: try: words.append(int(token, 16) & MASK32) except ValueError as error: raise ValueError(f"{path}:{line_number}: invalid hex word") from error return words def write_words(path: Path, words: list[int | None]) -> None: lines = ( "xxxxxxxx\n" if word is None else f"{word & MASK32:08x}\n" for word in words ) path.write_text("".join(lines)) class RV32Model: def __init__(self, program: list[int], initial_data: list[int]): if len(program) > MEMORY_BYTES // 4: raise ValueError("program exceeds the 1024-word ISRAM") if len(initial_data) > MEMORY_BYTES // 4: raise ValueError("initial data exceeds the 1024-word DSRAM") self.program = program self.data = bytearray(MEMORY_BYTES) self.defined = bytearray(MEMORY_BYTES) for index, word in enumerate(initial_data): self.data[index * 4:index * 4 + 4] = word.to_bytes(4, "little") self.defined[index * 4:index * 4 + 4] = b"\x01\x01\x01\x01" self.regs = [0] * 32 self.pc = ISRAM_BASE self.highest_data_byte = len(initial_data) * 4 def reg(self, index: int) -> int: return 0 if index == 0 else self.regs[index] def set_reg(self, index: int, value: int) -> None: if index: self.regs[index] = value & MASK32 def data_offset(self, address: int, size: int) -> int: # The CPU has a direct, local DSRAM port, so program addresses are # offsets 0x000-0xfff. DSRAM_BASE is used only by the external debug # crossbar; accepting it here is also useful for future mapped tests. offset = address if address < MEMORY_BYTES else address - DSRAM_BASE if offset < 0 or offset + size > MEMORY_BYTES: raise RuntimeError(f"DSRAM access outside 0x1000-0x1fff: 0x{address:08x}") return offset def load(self, address: int, size: int, signed: bool) -> int: offset = self.data_offset(address, size) value = int.from_bytes(self.data[offset:offset + size], "little") return sign_extend(value, size * 8) & MASK32 if signed else value def store(self, address: int, size: int, value: int) -> None: offset = self.data_offset(address, size) self.data[offset:offset + size] = (value & ((1 << (size * 8)) - 1)).to_bytes(size, "little") self.defined[offset:offset + size] = bytes([1]) * size self.highest_data_byte = max(self.highest_data_byte, offset + size) def step(self) -> bool: if self.pc & 3: raise RuntimeError(f"misaligned instruction PC 0x{self.pc:08x}") index = (self.pc - ISRAM_BASE) // 4 if index < 0 or index >= len(self.program): raise RuntimeError(f"instruction fetch outside program at 0x{self.pc:08x}") insn = self.program[index] if insn == EBREAK: return False opcode = insn & 0x7F rd = (insn >> 7) & 0x1F funct3 = (insn >> 12) & 7 rs1 = (insn >> 15) & 0x1F rs2 = (insn >> 20) & 0x1F funct7 = (insn >> 25) & 0x7F next_pc = (self.pc + 4) & MASK32 imm_i = sign_extend(insn >> 20, 12) imm_s = sign_extend(((insn >> 25) << 5) | ((insn >> 7) & 0x1F), 12) imm_b = sign_extend( ((insn >> 31) << 12) | (((insn >> 7) & 1) << 11) | (((insn >> 25) & 0x3F) << 5) | (((insn >> 8) & 0xF) << 1), 13) imm_u = insn & 0xFFFFF000 imm_j = sign_extend( ((insn >> 31) << 20) | (((insn >> 12) & 0xFF) << 12) | (((insn >> 20) & 1) << 11) | (((insn >> 21) & 0x3FF) << 1), 21) a, b = self.reg(rs1), self.reg(rs2) if opcode == 0x37: # LUI self.set_reg(rd, imm_u) elif opcode == 0x17: # AUIPC self.set_reg(rd, self.pc + imm_u) elif opcode == 0x6F: # JAL self.set_reg(rd, next_pc) next_pc = (self.pc + imm_j) & MASK32 elif opcode == 0x67 and funct3 == 0: # JALR self.set_reg(rd, next_pc) next_pc = (a + imm_i) & ~1 & MASK32 elif opcode == 0x63: # branches conditions = { 0: a == b, 1: a != b, 4: signed32(a) < signed32(b), 5: signed32(a) >= signed32(b), 6: a < b, 7: a >= b, } if funct3 not in conditions: raise RuntimeError(f"unsupported branch funct3 {funct3}") if conditions[funct3]: next_pc = (self.pc + imm_b) & MASK32 elif opcode == 0x03: # loads formats = {0: (1, True), 1: (2, True), 2: (4, True), 4: (1, False), 5: (2, False)} if funct3 not in formats: raise RuntimeError(f"unsupported load funct3 {funct3}") size, signed = formats[funct3] self.set_reg(rd, self.load((a + imm_i) & MASK32, size, signed)) elif opcode == 0x23: # stores sizes = {0: 1, 1: 2, 2: 4} if funct3 not in sizes: raise RuntimeError(f"unsupported store funct3 {funct3}") self.store((a + imm_s) & MASK32, sizes[funct3], b) elif opcode == 0x13: # immediate ALU shamt = rs2 if funct3 == 0: result = a + imm_i elif funct3 == 2: result = int(signed32(a) < imm_i) elif funct3 == 3: result = int(a < (imm_i & MASK32)) elif funct3 == 4: result = a ^ imm_i elif funct3 == 6: result = a | imm_i elif funct3 == 7: result = a & imm_i elif funct3 == 1 and funct7 == 0: result = a << shamt elif funct3 == 5 and funct7 == 0: result = a >> shamt elif funct3 == 5 and funct7 == 0x20: result = signed32(a) >> shamt else: raise RuntimeError(f"unsupported OP-IMM instruction 0x{insn:08x}") self.set_reg(rd, result) elif opcode == 0x33: # register ALU key = (funct7, funct3) operations = { (0x00, 0): lambda: a + b, (0x20, 0): lambda: a - b, (0x00, 1): lambda: a << (b & 31), (0x00, 2): lambda: int(signed32(a) < signed32(b)), (0x00, 3): lambda: int(a < b), (0x00, 4): lambda: a ^ b, (0x00, 5): lambda: a >> (b & 31), (0x20, 5): lambda: signed32(a) >> (b & 31), (0x00, 6): lambda: a | b, (0x00, 7): lambda: a & b, } if key not in operations: raise RuntimeError(f"unsupported OP instruction 0x{insn:08x}") self.set_reg(rd, operations[key]()) else: raise RuntimeError(f"unsupported instruction 0x{insn:08x} at PC 0x{self.pc:08x}") self.pc = next_pc self.regs[0] = 0 return True def run(self, max_instructions: int) -> int: for count in range(1, max_instructions + 1): if not self.step(): return count raise RuntimeError(f"program did not execute EBREAK within {max_instructions} instructions") def data_words(self) -> list[int | None]: count = (self.highest_data_byte + 3) // 4 words: list[int | None] = [] for i in range(count): offset = i * 4 if all(self.defined[offset:offset + 4]): words.append(int.from_bytes(self.data[offset:offset + 4], "little")) else: words.append(None) return words def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--program", type=Path, required=True) parser.add_argument("--data", type=Path) parser.add_argument("--regs-out", type=Path, required=True) parser.add_argument("--data-out", type=Path, required=True) parser.add_argument("--max-instructions", type=int, default=10000) args = parser.parse_args() model = RV32Model(read_words(args.program), read_words(args.data)) count = model.run(args.max_instructions) write_words(args.regs_out, model.regs) write_words(args.data_out, model.data_words()) print(f"Reference model halted after {count} instructions") if __name__ == "__main__": main()