Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression (272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps, and all dump-derived textures/traces) is excluded via .gitignore and stays local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a synthetic EE-RAM image + manifest for Ch270's ELF runner TB.
|
||||
|
||||
Produces two files at the requested output prefix:
|
||||
|
||||
<prefix>.image.hex iverilog $readmemh compatible. Uses @<hex_qw_idx>
|
||||
directives so only the populated 128-bit qwords
|
||||
appear (the TB pre-zeros the array before reading).
|
||||
Each line is 32 hex chars = one 128-bit qword,
|
||||
MSB-first (byte 15 leftmost, byte 0 rightmost).
|
||||
|
||||
<prefix>.manifest.hex Two lines:
|
||||
line 0: ELF entry point (32-bit hex)
|
||||
line 1: stack-top hint (32-bit hex; unused
|
||||
by current TB but reserved)
|
||||
|
||||
The synthetic program lives at PHYS 0x00100000. Entry is given as a
|
||||
kseg0 address (0x80100008) because the ee_memory_map stub routes
|
||||
useg (top bit = 0) to a separate useg_shadow region, not ee_ram —
|
||||
real PS2 ELFs use kseg0 entries for the same reason (cached text):
|
||||
|
||||
PHYS 0x00100000 / kseg0 0x80100000: nop pad
|
||||
PHYS 0x00100004 / kseg0 0x80100004: nop pad
|
||||
PHYS 0x00100008 / kseg0 0x80100008: addiu $v0,$0,0x1234 *** entry ***
|
||||
PHYS 0x0010000C / kseg0 0x8010000C: addiu $v1,$0,0x5678
|
||||
PHYS 0x00100010 / kseg0 0x80100010: j 0x80100010 loop-to-self
|
||||
PHYS 0x00100014 / kseg0 0x80100014: nop delay slot
|
||||
|
||||
The J encoding (0x08040004) is PC-relative: at runtime, j_tgt =
|
||||
{PC+4[31:28], imm26<<2}, so the high 4 bits come from the PC.
|
||||
PC=0x80100010 ⇒ j_tgt = 0x80100010 (self) — same encoding works for
|
||||
both kseg0 and kuseg views.
|
||||
|
||||
Expected TB verdict: `elf_timeout_with_hot_pc` with hot_pc near
|
||||
0x80100010. That confirms the ELF-load + entry-bootstrap + strict-
|
||||
trace pipeline is sound (no traps, no halts, no unmapped MMIO, EE
|
||||
reaches and executes real code).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import argparse
|
||||
|
||||
|
||||
def encode_addiu(rt: int, rs: int, imm: int) -> int:
|
||||
"""ADDIU rt, rs, imm. op=0x09."""
|
||||
return (0x09 << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | (imm & 0xFFFF)
|
||||
|
||||
|
||||
def encode_j(target: int) -> int:
|
||||
"""J target. op=0x02. Target must be word-aligned."""
|
||||
assert target & 3 == 0, "J target must be word-aligned"
|
||||
return (0x02 << 26) | ((target >> 2) & 0x03FFFFFF)
|
||||
|
||||
|
||||
def encode_lui(rt: int, imm: int) -> int:
|
||||
"""LUI rt, imm. op=0x0F."""
|
||||
return (0x0F << 26) | ((rt & 0x1F) << 16) | (imm & 0xFFFF)
|
||||
|
||||
|
||||
def encode_ori(rt: int, rs: int, imm: int) -> int:
|
||||
"""ORI rt, rs, imm. op=0x0D."""
|
||||
return (0x0D << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | (imm & 0xFFFF)
|
||||
|
||||
|
||||
def encode_jr(rs: int) -> int:
|
||||
"""JR rs. SPECIAL/funct=0x08."""
|
||||
return ((rs & 0x1F) << 21) | 0x08
|
||||
|
||||
|
||||
def write_word_le(image: bytearray, phys_addr: int, word: int) -> None:
|
||||
"""Write a 32-bit word little-endian into the EE-RAM image."""
|
||||
assert phys_addr + 4 <= len(image), "phys_addr out of image bounds"
|
||||
image[phys_addr + 0] = (word >> 0) & 0xFF
|
||||
image[phys_addr + 1] = (word >> 8) & 0xFF
|
||||
image[phys_addr + 2] = (word >> 16) & 0xFF
|
||||
image[phys_addr + 3] = (word >> 24) & 0xFF
|
||||
|
||||
|
||||
def qword_to_hex(image: bytearray, qw_phys: int) -> str:
|
||||
"""Return the 32-char hex string for the qword at byte offset qw_phys.
|
||||
|
||||
iverilog $readmemh expects the leftmost hex char to be the highest
|
||||
bit of the 128-bit value. Byte 15 is the most significant byte;
|
||||
byte 0 is the least.
|
||||
"""
|
||||
assert qw_phys + 16 <= len(image)
|
||||
bytes16 = image[qw_phys:qw_phys + 16]
|
||||
# Reverse to MSB-first for the hex string.
|
||||
return bytes16[::-1].hex()
|
||||
|
||||
|
||||
def emit_image_hex(image: bytearray, path: str, qw_size: int) -> None:
|
||||
"""Emit a $readmemh-compatible hex file using @<idx> directives for
|
||||
every populated (non-zero) qword. Empty qwords are skipped — the TB
|
||||
pre-zeros the array before reading.
|
||||
"""
|
||||
with open(path, "w") as f:
|
||||
f.write("// Ch270 synthetic EE-RAM image\n")
|
||||
f.write(f"// {len(image)} bytes / {len(image)//qw_size} qwords\n")
|
||||
f.write("// Populated qwords only; TB zero-inits before $readmemh.\n\n")
|
||||
any_emitted = False
|
||||
for qw_idx in range(0, len(image) // qw_size):
|
||||
qw_byte = qw_idx * qw_size
|
||||
qw_bytes = image[qw_byte:qw_byte + qw_size]
|
||||
if any(b != 0 for b in qw_bytes):
|
||||
f.write(f"@{qw_idx:08x}\n")
|
||||
f.write(qword_to_hex(image, qw_byte) + "\n")
|
||||
any_emitted = True
|
||||
if not any_emitted:
|
||||
# iverilog $readmemh errors on empty file; emit a benign entry.
|
||||
f.write("@00000000\n00000000000000000000000000000000\n")
|
||||
|
||||
|
||||
def emit_manifest_hex(path: str, entry: int, stack_top: int) -> None:
|
||||
"""Emit the manifest as two 32-bit hex lines."""
|
||||
with open(path, "w") as f:
|
||||
f.write("// Ch270 manifest\n")
|
||||
f.write(f"// line 0 = entry, line 1 = stack_top hint\n")
|
||||
f.write(f"{entry:08x}\n")
|
||||
f.write(f"{stack_top:08x}\n")
|
||||
|
||||
|
||||
def build_synthetic_image(image_bytes: int, entry_phys: int) -> bytearray:
|
||||
"""Build the EE-RAM image with the synthetic program at entry_phys."""
|
||||
image = bytearray(image_bytes)
|
||||
# Pad before entry so PC starts on real instructions:
|
||||
write_word_le(image, entry_phys - 8, 0x00000000) # nop
|
||||
write_word_le(image, entry_phys - 4, 0x00000000) # nop
|
||||
# Body:
|
||||
write_word_le(image, entry_phys + 0, encode_addiu(2, 0, 0x1234)) # $v0 = 0x1234
|
||||
write_word_le(image, entry_phys + 4, encode_addiu(3, 0, 0x5678)) # $v1 = 0x5678
|
||||
write_word_le(image, entry_phys + 8, encode_j(entry_phys + 8)) # j self
|
||||
write_word_le(image, entry_phys + 12, 0x00000000) # nop delay slot
|
||||
return image
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--out-prefix", required=True,
|
||||
help="output file prefix (writes <prefix>.image.hex + <prefix>.manifest.hex)")
|
||||
p.add_argument("--entry", type=lambda s: int(s, 0), default=0x80100008,
|
||||
help="entry point VIRTUAL address (kseg0 default 0x80100008; "
|
||||
"physical placement of the code segment is entry & 0x1FFFFFFF)")
|
||||
p.add_argument("--ee-ram-bytes", type=lambda s: int(s, 0), default=2 * 1024 * 1024,
|
||||
help="EE RAM size in bytes (default 2 MiB; must be >= entry+16)")
|
||||
p.add_argument("--stack-top", type=lambda s: int(s, 0), default=0x801FFFF0,
|
||||
help="stack top hint stored in manifest (default 0x801FFFF0 kseg0)")
|
||||
args = p.parse_args()
|
||||
|
||||
entry_phys = args.entry & 0x1FFFFFFF
|
||||
if entry_phys < 8 or entry_phys + 16 > args.ee_ram_bytes:
|
||||
p.error(f"entry 0x{args.entry:08x} (phys 0x{entry_phys:08x}) "
|
||||
f"doesn't fit into 0x{args.ee_ram_bytes:x}-byte EE RAM")
|
||||
|
||||
image = build_synthetic_image(args.ee_ram_bytes, entry_phys)
|
||||
emit_image_hex(image, f"{args.out_prefix}.image.hex", qw_size=16)
|
||||
emit_manifest_hex(f"{args.out_prefix}.manifest.hex",
|
||||
entry=args.entry, stack_top=args.stack_top)
|
||||
print(f"[generate_synthetic_image] wrote {args.out_prefix}.image.hex "
|
||||
f"+ {args.out_prefix}.manifest.hex (entry=0x{args.entry:08x}, "
|
||||
f"phys=0x{entry_phys:08x}, ee_ram={args.ee_ram_bytes} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user