init commit

This commit is contained in:
2026-09-17 16:52:07 -04:00
commit b71bc6a777
79 changed files with 3978 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Assemble one tests/<name>/program.asm and generate its golden (Expected outputs)."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import struct
import subprocess
from rv32_model import RV32Model, read_words, write_words
REPO_ROOT = Path(__file__).resolve().parents[1]
def find_assembler(explicit: str | None) -> str:
candidates = [
explicit,
os.environ.get("RISCV_AS"),
REPO_ROOT / "scripts" / "riscv-none-elf-as",
"riscv-none-elf-as",
"riscv32-unknown-elf-as",
"riscv64-unknown-elf-as",
]
for candidate in candidates:
if not candidate:
continue
candidate = str(candidate)
resolved = shutil.which(candidate)
if resolved:
return resolved
path = Path(candidate)
try:
if path.is_file() and os.access(path, os.X_OK):
return str(path)
except OSError:
continue
raise RuntimeError(
"RISC-V GNU assembler not found; "
"or set RISCV_AS=/path/to/riscv-*-as"
)
def elf_text(path: Path) -> bytes:
image = path.read_bytes()
if image[:4] != b"\x7fELF" or image[4] != 1 or image[5] != 1:
raise RuntimeError(f"{path} is not a little-endian ELF32 object")
section_offset = struct.unpack_from("<I", image, 32)[0]
section_size = struct.unpack_from("<H", image, 46)[0]
section_count = struct.unpack_from("<H", image, 48)[0]
names_index = struct.unpack_from("<H", image, 50)[0]
names_header = section_offset + names_index * section_size
names_offset, names_size = struct.unpack_from("<II", image, names_header + 16)
names = image[names_offset:names_offset + names_size]
for index in range(section_count):
header = section_offset + index * section_size
name_index = struct.unpack_from("<I", image, header)[0]
end = names.find(b"\0", name_index)
name = names[name_index:end].decode()
if name == ".text":
offset, size = struct.unpack_from("<II", image, header + 16)
return image[offset:offset + size]
raise RuntimeError(f"{path} has no .text section")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("test_dir", type=Path)
parser.add_argument("--assembler")
parser.add_argument("--max-instructions", type=int, default=10000)
args = parser.parse_args()
test_dir = args.test_dir.resolve()
source = test_dir / "program.asm"
if not source.is_file():
raise RuntimeError(f"missing required {source}")
assembler = find_assembler(args.assembler)
obj = test_dir / "program.o"
subprocess.run(
[assembler, "-march=rv32i", "-mabi=ilp32", "-mno-relax", str(source), "-o", str(obj)],
check=True,
)
text = elf_text(obj)
if len(text) % 4:
raise RuntimeError(".text size is not a multiple of four bytes")
program = [int.from_bytes(text[i:i + 4], "little") for i in range(0, len(text), 4)]
program_path = test_dir / "program.hex"
write_words(program_path, program)
data_path = test_dir / "data.hex"
initial_data = read_words(data_path if data_path.exists() else None)
model = RV32Model(program, initial_data)
count = model.run(args.max_instructions)
write_words(test_dir / "expected_regs.hex", model.regs)
write_words(test_dir / "expected_data.hex", model.data_words())
print(f"Built {test_dir.name}: {len(program)} words, halted after {count} instructions")
if __name__ == "__main__":
main()
BIN
View File
Binary file not shown.
+241
View File
@@ -0,0 +1,241 @@
#!/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()