#!/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: .image.hex iverilog $readmemh compatible. Uses @ 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). .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 @ 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 .image.hex + .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())