ec82764bef
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>
170 lines
7.3 KiB
Python
170 lines
7.3 KiB
Python
#!/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())
|