From b264037e8281f5c6458c0b21c5fc11e46fac73b0 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 17:42:58 +1000 Subject: [PATCH] further digging and basic emulator --- ROM/rcp-txd-idle-only.txt | 54 +++ build/rom_report_sources.json | 256 ++++++++++++++ build/rom_report_sources.txt | 32 ++ h8536/emulator.py | 480 ++++++++++++++++++++++++++ h8536/protocol_capture.py | 7 +- h8536/report_source_trace.py | 548 ++++++++++++++++++++++++++++++ h8536_emulator.py | 8 + h8536_report_source_trace.py | 8 + tests/test_emulator.py | 76 +++++ tests/test_protocol_capture.py | 27 ++ tests/test_report_source_trace.py | 135 ++++++++ 11 files changed, 1628 insertions(+), 3 deletions(-) create mode 100644 ROM/rcp-txd-idle-only.txt create mode 100644 build/rom_report_sources.json create mode 100644 build/rom_report_sources.txt create mode 100644 h8536/emulator.py create mode 100644 h8536/report_source_trace.py create mode 100644 h8536_emulator.py create mode 100644 h8536_report_source_trace.py create mode 100644 tests/test_emulator.py create mode 100644 tests/test_report_source_trace.py diff --git a/ROM/rcp-txd-idle-only.txt b/ROM/rcp-txd-idle-only.txt new file mode 100644 index 0000000..de98559 --- /dev/null +++ b/ROM/rcp-txd-idle-only.txt @@ -0,0 +1,54 @@ +11:54:40.567 frame 006 00 00 00 00 80 DA +11:54:41.368 frame 006 00 00 00 00 80 DA +11:54:41.970 frame 006 00 00 00 00 80 DA +11:54:42.772 frame 006 00 00 00 00 80 DA +11:54:43.373 frame 006 00 00 00 00 80 DA +11:54:44.176 frame 006 00 00 00 00 80 DA +11:54:44.778 frame 006 00 00 00 00 80 DA +11:54:45.580 frame 006 00 00 00 00 80 DA +11:54:46.183 frame 006 00 00 00 00 80 DA +11:54:46.984 frame 006 00 00 00 00 80 DA +11:54:47.586 frame 006 00 00 00 00 80 DA +11:54:48.387 frame 006 00 00 00 00 80 DA +11:54:48.988 frame 006 00 00 00 00 80 DA +11:54:49.790 frame 006 00 00 00 00 80 DA +11:54:50.393 frame 006 00 00 00 00 80 DA +11:54:51.196 frame 006 00 00 00 00 80 DA +11:54:51.797 frame 006 00 00 00 00 80 DA +11:54:52.599 frame 006 00 00 00 00 80 DA +11:54:53.201 frame 006 00 00 00 00 80 DA +11:54:54.003 frame 006 00 00 00 00 80 DA +11:54:54.604 frame 006 00 00 00 00 80 DA +11:54:55.406 frame 006 00 00 00 00 80 DA +11:54:56.009 frame 006 00 00 00 00 80 DA +11:54:56.812 frame 006 00 00 00 00 80 DA +11:54:57.413 frame 006 00 00 00 00 80 DA +11:54:58.215 frame 006 00 00 00 00 80 DA +11:54:58.816 frame 006 00 00 00 00 80 DA +11:54:59.619 frame 006 00 00 00 00 80 DA +11:55:00.220 frame 006 00 00 00 00 80 DA +11:55:01.023 frame 006 00 00 00 00 80 DA +11:55:01.625 frame 006 00 00 00 00 80 DA +11:55:02.426 frame 006 00 00 00 00 80 DA +11:55:03.027 frame 006 00 00 00 00 80 DA +11:55:03.829 frame 006 00 00 00 00 80 DA +11:55:04.430 frame 006 00 00 00 00 80 DA +11:55:05.231 frame 006 00 00 00 00 80 DA +11:55:05.832 frame 006 00 00 00 00 80 DA +11:55:06.634 frame 006 00 00 00 00 80 DA +11:55:07.235 frame 006 00 00 00 00 80 DA +11:55:07.837 frame 006 00 00 00 00 80 DA +11:55:08.638 frame 006 00 00 00 00 80 DA +11:55:09.239 frame 006 00 00 00 00 80 DA +11:55:10.040 frame 006 00 00 00 00 80 DA +11:55:10.642 frame 006 00 00 00 00 80 DA +11:55:11.443 frame 006 00 00 00 00 80 DA +11:55:12.045 frame 006 00 00 00 00 80 DA +11:55:12.848 frame 006 00 00 00 00 80 DA +11:55:13.450 frame 006 00 00 00 00 80 DA +11:55:14.253 frame 006 00 00 00 00 80 DA +11:55:14.854 frame 006 00 00 00 00 80 DA +11:55:15.657 frame 006 00 00 00 00 80 DA +11:55:16.259 frame 006 00 00 00 00 80 DA +11:55:17.061 frame 006 00 00 00 00 80 DA +11:55:17.663 frame 006 00 00 00 00 80 DA diff --git a/build/rom_report_sources.json b/build/rom_report_sources.json new file mode 100644 index 0000000..9446440 --- /dev/null +++ b/build/rom_report_sources.json @@ -0,0 +1,256 @@ +{ + "calls": [ + { + "address": 5622, + "address_hex": "H'15F6", + "assessment": "Direct static enqueue source for 0x0081, not 0x0007.", + "can_directly_enqueue_report_index": false, + "dataflow_block": 5609, + "function_label": "loc_15E0", + "function_start": 5600, + "function_start_hex": "H'15E0", + "instruction": "BSR loc_3E54", + "r2": { + "bit7": true, + "classification": "constant", + "evidence": { + "address": 5617, + "address_hex": "H'15F1", + "instruction": "MOV:E.B #H'80, R2", + "mnemonic": "MOV:E.B", + "operands": "#H'80, R2" + }, + "reason": "immediate load", + "register": "R2", + "value": 128, + "value_hex": "0x80" + }, + "r3": { + "classification": "constant", + "evidence": { + "address": 5619, + "address_hex": "H'15F3", + "instruction": "MOV:I.W #H'0081, R3", + "mnemonic": "MOV:I.W", + "operands": "#H'0081, R3" + }, + "reason": "immediate load", + "register": "R3", + "value": 129, + "value_hex": "0x0081" + }, + "table_hints": [], + "target": 15956, + "target_hex": "H'3E54", + "window_instruction_count": 4, + "window_start": 5609, + "window_start_hex": "H'15E9" + }, + { + "address": 6673, + "address_hex": "H'1A11", + "assessment": "No static 0x0007 constant here; R3 is dynamic/table-derived.", + "can_directly_enqueue_report_index": false, + "dataflow_block": 6665, + "function_label": "loc_19DB", + "function_start": 6619, + "function_start_hex": "H'19DB", + "instruction": "BSR loc_3E54", + "r2": { + "bit7": true, + "classification": "constant", + "evidence": { + "address": 6669, + "address_hex": "H'1A0D", + "instruction": "MOV:E.B #H'80, R2", + "mnemonic": "MOV:E.B", + "operands": "#H'80, R2" + }, + "reason": "immediate load", + "register": "R2", + "value": 128, + "value_hex": "0x80" + }, + "r3": { + "classification": "dynamic/table-derived", + "evidence": { + "address": 6671, + "address_hex": "H'1A0F", + "instruction": "MOV:G.W R5, R3", + "mnemonic": "MOV:G.W", + "operands": "R5, R3" + }, + "reason": "copied from unresolved R5", + "register": "R3", + "value": null, + "value_hex": null + }, + "table_hints": [ + { + "address": 6665, + "address_hex": "H'1A09", + "instruction": "MOV:G.W R1, @(-H'1800,R3)", + "operand": "@(-H'1800,R3)", + "table": "current_value_table_candidate" + } + ], + "target": 15956, + "target_hex": "H'3E54", + "window_instruction_count": 3, + "window_start": 6665, + "window_start_hex": "H'1A09" + }, + { + "address": 6777, + "address_hex": "H'1A79", + "assessment": "No static 0x0007 constant here; R3 is dynamic/table-derived.", + "can_directly_enqueue_report_index": false, + "dataflow_block": 6769, + "function_label": "loc_1A35", + "function_start": 6709, + "function_start_hex": "H'1A35", + "instruction": "BSR loc_3E54", + "r2": { + "bit7": true, + "classification": "constant", + "evidence": { + "address": 6773, + "address_hex": "H'1A75", + "instruction": "MOV:E.B #H'80, R2", + "mnemonic": "MOV:E.B", + "operands": "#H'80, R2" + }, + "reason": "immediate load", + "register": "R2", + "value": 128, + "value_hex": "0x80" + }, + "r3": { + "classification": "dynamic/table-derived", + "evidence": { + "address": 6775, + "address_hex": "H'1A77", + "instruction": "MOV:G.W R5, R3", + "mnemonic": "MOV:G.W", + "operands": "R5, R3" + }, + "reason": "copied from unresolved R5", + "register": "R3", + "value": null, + "value_hex": null + }, + "table_hints": [ + { + "address": 6769, + "address_hex": "H'1A71", + "instruction": "MOV:G.W R0, @(-H'1800,R3)", + "operand": "@(-H'1800,R3)", + "table": "current_value_table_candidate" + } + ], + "target": 15956, + "target_hex": "H'3E54", + "window_instruction_count": 3, + "window_start": 6769, + "window_start_hex": "H'1A71" + }, + { + "address": 9896, + "address_hex": "H'26A8", + "assessment": "No static 0x0007 constant here; R3 is unknown.", + "can_directly_enqueue_report_index": false, + "dataflow_block": 9896, + "function_label": "loc_2650", + "function_start": 9808, + "function_start_hex": "H'2650", + "instruction": "BSR loc_3E54", + "r2": { + "bit7": null, + "classification": "unknown", + "evidence": null, + "reason": "decompiler dataflow: block_entry", + "register": "R2", + "value": null, + "value_hex": null + }, + "r3": { + "classification": "unknown", + "evidence": null, + "reason": "decompiler dataflow: block_entry", + "register": "R3", + "value": null, + "value_hex": null + }, + "table_hints": [], + "target": 15956, + "target_hex": "H'3E54", + "window_instruction_count": 0, + "window_start": 9896, + "window_start_hex": "H'26A8" + }, + { + "address": 18726, + "address_hex": "H'4926", + "assessment": "Direct static enqueue source for 0x00f6, not 0x0007.", + "can_directly_enqueue_report_index": false, + "dataflow_block": 18709, + "function_label": "loc_48FA", + "function_start": 18682, + "function_start_hex": "H'48FA", + "instruction": "BSR loc_3E54", + "r2": { + "bit7": true, + "classification": "constant", + "evidence": { + "address": 18721, + "address_hex": "H'4921", + "instruction": "MOV:E.B #H'80, R2", + "mnemonic": "MOV:E.B", + "operands": "#H'80, R2" + }, + "reason": "immediate load", + "register": "R2", + "value": 128, + "value_hex": "0x80" + }, + "r3": { + "classification": "constant", + "evidence": { + "address": 18723, + "address_hex": "H'4923", + "instruction": "MOV:I.W #H'00F6, R3", + "mnemonic": "MOV:I.W", + "operands": "#H'00F6, R3" + }, + "reason": "immediate load", + "register": "R3", + "value": 246, + "value_hex": "0x00F6" + }, + "table_hints": [], + "target": 15956, + "target_hex": "H'3E54", + "window_instruction_count": 5, + "window_start": 18709, + "window_start_hex": "H'4915" + } + ], + "caveats": [ + "This is a bounded local static trace, not an emulator run.", + "R3 values classified as dynamic/table-derived may still become 0x0007 at runtime.", + "Indirect dispatch, table handlers, interrupt interleavings, or callers absent from the JSON may still enqueue 0x0007.", + "The generic queue-to-TX path only emits queued entries; this tracer looks for direct report-index sources at loc_3E54 callers." + ], + "kind": "report_source_trace", + "queue_function": 15956, + "queue_function_hex": "H'3E54", + "report_index_of_interest": 7, + "report_index_of_interest_hex": "0x0007", + "summary": { + "conclusion": "No direct loc_3E54 caller in this JSON statically loads report index 0x0007. 0x0007 remains an observed runtime/capture value unless another indirect or table-dispatch path is proven.", + "direct_call_count": 5, + "direct_static_hit_count": 0, + "dynamic_or_unknown_candidate_count": 3, + "status": "not_statically_proven" + } +} diff --git a/build/rom_report_sources.txt b/build/rom_report_sources.txt new file mode 100644 index 0000000..0d59df2 --- /dev/null +++ b/build/rom_report_sources.txt @@ -0,0 +1,32 @@ +H8/536 loc_3E54 Report Source Trace + +Queue function: H'3E54 +Report index of interest: 0x0007 +Direct callers: 5 +Direct static 0x0007 hits: 0 +Dynamic/unknown candidates: 3 + +Conclusion: No direct loc_3E54 caller in this JSON statically loads report index 0x0007. 0x0007 remains an observed runtime/capture value unless another indirect or table-dispatch path is proven. + +Call sites: +- H'15F6 in loc_15E0: R2.bit7=set, R3=0x0081 (constant); direct_0x0007=False + R2 evidence: H'15F1 MOV:E.B #H'80, R2 + R3 evidence: H'15F3 MOV:I.W #H'0081, R3 +- H'1A11 in loc_19DB: R2.bit7=set, R3= (dynamic/table-derived); direct_0x0007=False + R2 evidence: H'1A0D MOV:E.B #H'80, R2 + R3 evidence: H'1A0F MOV:G.W R5, R3 + table/context hints: H'1A09 current_value_table_candidate via @(-H'1800,R3) +- H'1A79 in loc_1A35: R2.bit7=set, R3= (dynamic/table-derived); direct_0x0007=False + R2 evidence: H'1A75 MOV:E.B #H'80, R2 + R3 evidence: H'1A77 MOV:G.W R5, R3 + table/context hints: H'1A71 current_value_table_candidate via @(-H'1800,R3) +- H'26A8 in loc_2650: R2.bit7=unknown, R3= (unknown); direct_0x0007=False +- H'4926 in loc_48FA: R2.bit7=set, R3=0x00F6 (constant); direct_0x0007=False + R2 evidence: H'4921 MOV:E.B #H'80, R2 + R3 evidence: H'4923 MOV:I.W #H'00F6, R3 + +Caveats: +- This is a bounded local static trace, not an emulator run. +- R3 values classified as dynamic/table-derived may still become 0x0007 at runtime. +- Indirect dispatch, table handlers, interrupt interleavings, or callers absent from the JSON may still enqueue 0x0007. +- The generic queue-to-TX path only emits queued entries; this tracer looks for direct report-index sources at loc_3E54 callers. diff --git a/h8536/emulator.py b/h8536/emulator.py new file mode 100644 index 0000000..03eee0d --- /dev/null +++ b/h8536/emulator.py @@ -0,0 +1,480 @@ +from __future__ import annotations + +import argparse +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable + +from .decoder import H8536Decoder +from .formatting import h8, h16 +from .memory import MEMORY_REGIONS, MemoryRegion, region_for +from .rom import DecodeError, Rom +from .vectors import read_vectors_min + + +SCI1_SMR = 0xFED8 +SCI1_BRR = 0xFED9 +SCI1_SCR = 0xFEDA +SCI1_TDR = 0xFEDB +SCI1_SSR = 0xFEDC +SCI1_RDR = 0xFEDD + +SCI_SCR_TIE = 0x80 +SCI_SCR_RIE = 0x40 +SCI_SCR_TE = 0x20 +SCI_SCR_RE = 0x10 +SCI_SSR_TDRE = 0x80 +SCI_SSR_RDRF = 0x40 +SCI_SSR_ORER = 0x20 +SCI_SSR_FER = 0x10 +SCI_SSR_PER = 0x08 + +ON_CHIP_RAM_START = 0xF680 +ON_CHIP_RAM_END = 0xFE7F +REGISTER_FIELD_START = 0xFE80 +REGISTER_FIELD_END = 0xFFFF +RAMCR = 0xFF11 +HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) + + +class EmulatorError(Exception): + pass + + +class UnsupportedInstruction(EmulatorError): + def __init__(self, pc: int, raw: bytes, text: str) -> None: + raw_text = " ".join(f"{byte:02X}" for byte in raw) + super().__init__(f"unsupported instruction at {h16(pc)}: {raw_text} {text}".rstrip()) + self.pc = pc + self.raw = raw + self.text = text + + +@dataclass +class SciTxEvent: + address: int + value: int + scr: int + ssr: int + emitted: bool + + +@dataclass +class SCI1: + """Small SCI1 model for the H8/536 serial path. + + Manual anchors: + - RDR/TDR/SCR/SSR live at H'FEDD/H'FEDB/H'FEDA/H'FEDC for SCI1. + - SCR bit 7 TIE, bit 6 RIE, bit 5 TE, bit 4 RE. + - SSR bit 7 TDRE, bit 6 RDRF, bits 5..3 ORER/FER/PER. + - Software normally writes TDR after TDRE=1, then clears SSR.TDRE. + """ + + smr: int = 0x00 + brr: int = 0xFF + scr: int = 0x0C + tdr: int = 0xFF + ssr: int = 0x87 + rdr: int = 0x00 + tx_bytes: list[int] = field(default_factory=list) + tx_events: list[SciTxEvent] = field(default_factory=list) + tx_frames: list[bytes] = field(default_factory=list) + _frame_buffer: bytearray = field(default_factory=bytearray) + + def read(self, address: int) -> int: + if address == SCI1_SMR: + return self.smr + if address == SCI1_BRR: + return self.brr + if address == SCI1_SCR: + return self.scr + if address == SCI1_TDR: + return self.tdr + if address == SCI1_SSR: + return self.ssr + if address == SCI1_RDR: + return self.rdr + raise KeyError(address) + + def write(self, address: int, value: int) -> None: + value &= 0xFF + if address == SCI1_SMR: + self.smr = value + elif address == SCI1_BRR: + self.brr = value + elif address == SCI1_SCR: + self.scr = value + elif address == SCI1_TDR: + self.tdr = value + self._write_tdr(value) + elif address == SCI1_SSR: + # The real SSR is R/(W)*: writable zeroes clear latched flags after + # the required read sequence. This scaffold applies zero bits + # directly so ROM BCLR/BSET style accesses can be modeled. + self.ssr = value + elif address == SCI1_RDR: + self.rdr = value + else: + raise KeyError(address) + + def _write_tdr(self, value: int) -> None: + emitted = bool(self.scr & SCI_SCR_TE) + if emitted: + self.tx_bytes.append(value) + self._frame_buffer.append(value) + if len(self._frame_buffer) == len(HEARTBEAT_FRAME): + self.tx_frames.append(bytes(self._frame_buffer)) + self._frame_buffer.clear() + self.ssr |= SCI_SSR_TDRE + self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) + + def inject_rx(self, value: int) -> None: + self.rdr = value & 0xFF + self.ssr |= SCI_SSR_RDRF + + def saw_heartbeat(self) -> bool: + return HEARTBEAT_FRAME in self.tx_frames + + +@dataclass +class MemoryAccess: + address: int + size: int + value: int + kind: str + region: str + + +class MemoryMap: + def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None: + self.rom = Rom(rom_bytes, base=0) + self.sci1 = sci1 if sci1 is not None else SCI1() + self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1) + self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1) + self.external: dict[int, int] = {} + self.access_log: list[MemoryAccess] = [] + + self._set_register(SCI1_SMR, self.sci1.smr) + self._set_register(SCI1_BRR, self.sci1.brr) + self._set_register(SCI1_SCR, self.sci1.scr) + self._set_register(SCI1_TDR, self.sci1.tdr) + self._set_register(SCI1_SSR, self.sci1.ssr) + self._set_register(SCI1_RDR, self.sci1.rdr) + + def region(self, address: int) -> MemoryRegion: + return region_for(address & 0xFFFF) + + def read8(self, address: int) -> int: + address &= 0xFFFF + if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): + value = self.sci1.read(address) + self._set_register(address, value) + elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: + value = self.ram[address - ON_CHIP_RAM_START] + elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: + value = self.registers[address - REGISTER_FIELD_START] + elif self.rom.contains(address): + value = self.rom.u8(address) + else: + value = self.external.get(address, 0xFF) + self._log("read", address, 1, value) + return value + + def read16(self, address: int) -> int: + high = self.read8(address) + low = self.read8((address + 1) & 0xFFFF) + return (high << 8) | low + + def write8(self, address: int, value: int) -> None: + address &= 0xFFFF + value &= 0xFF + if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): + self.sci1.write(address, value) + self._set_register(address, self.sci1.read(address)) + elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: + self.ram[address - ON_CHIP_RAM_START] = value + elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: + self._set_register(address, value) + elif self.rom.contains(address): + # ROM writes are logged but ignored. This keeps the scaffold useful + # for accidental writes without mutating the loaded image. + pass + else: + self.external[address] = value + self._log("write", address, 1, value) + + def write16(self, address: int, value: int) -> None: + self.write8(address, (value >> 8) & 0xFF) + self.write8((address + 1) & 0xFFFF, value & 0xFF) + + def _set_register(self, address: int, value: int) -> None: + self.registers[address - REGISTER_FIELD_START] = value & 0xFF + + def _log(self, kind: str, address: int, size: int, value: int) -> None: + self.access_log.append(MemoryAccess(address, size, value, kind, self.region(address).name)) + + +@dataclass +class CPUState: + pc: int = 0 + sr: int = 0 + br: int = 0 + regs: list[int] = field(default_factory=lambda: [0] * 8) + cycles: int = 0 + steps: int = 0 + z: bool = False + + +@dataclass +class RunReport: + steps: int + cycles: int + pc: int + stopped_reason: str + tx_bytes: bytes + tx_frames: list[bytes] + heartbeat_seen: bool + unsupported: str | None = None + trace: list[str] = field(default_factory=list) + + def summary_lines(self) -> list[str]: + lines = [ + f"steps={self.steps}", + f"cycles={self.cycles}", + f"pc={h16(self.pc)}", + f"stopped={self.stopped_reason}", + "tx_bytes=" + self.tx_bytes.hex(" ").upper(), + "tx_frames=" + ", ".join(frame.hex(" ").upper() for frame in self.tx_frames), + f"heartbeat_seen={self.heartbeat_seen}", + ] + if self.unsupported: + lines.append(f"unsupported={self.unsupported}") + lines.append("next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow") + return lines + + +class H8536Emulator: + def __init__(self, rom_bytes: bytes) -> None: + if not rom_bytes: + raise ValueError("ROM image is empty") + self.sci1 = SCI1() + self.memory = MemoryMap(rom_bytes, self.sci1) + self.cpu = CPUState() + self.vectors = read_vectors_min(self.memory.rom) + self.reset() + + def reset(self) -> None: + self.cpu = CPUState(pc=self.reset_vector()) + + def reset_vector(self) -> int: + if self.memory.rom.contains(0, 2): + return self.memory.rom.u16(0) + raise DecodeError("ROM does not contain a reset vector at H'0000") + + def step(self) -> str: + pc = self.cpu.pc + decoder = H8536Decoder(self.memory.rom, br=self.cpu.br) + ins = decoder.decode(pc) + if not ins.valid: + raise UnsupportedInstruction(pc, ins.raw, ins.text) + + next_pc = (pc + ins.size) & 0xFFFF + raw = ins.raw + text = ins.text + + if raw[0] == 0x00: + pass + elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3: + self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big") + elif raw[:2] == bytes([0x0C, 0x07]) and len(raw) == 4: + self.cpu.sr = int.from_bytes(raw[2:4], "big") + elif raw[0] in (0x15, 0x1D) and len(raw) >= 4: + next_pc = self._execute_general_abs(raw, pc, next_pc) + elif raw[0] in range(0x20, 0x30) and len(raw) == 2: + next_pc = self._branch8(raw, pc, next_pc) + elif raw[0] in range(0x30, 0x40) and len(raw) == 3: + next_pc = self._branch16(raw, pc, next_pc) + else: + raise UnsupportedInstruction(pc, raw, text) + + self.cpu.pc = next_pc + self.cpu.steps += 1 + self.cpu.cycles += self._rough_cycles(raw) + return f"{h16(pc)}: {' '.join(f'{byte:02X}' for byte in raw):<17} {text}" + + def run(self, max_steps: int, trace: bool = False, stop_on_heartbeat: bool = False) -> RunReport: + trace_lines: list[str] = [] + stopped_reason = "max_steps" + unsupported: str | None = None + for _ in range(max_steps): + try: + line = self.step() + except UnsupportedInstruction as exc: + stopped_reason = "unsupported_instruction" + unsupported = str(exc) + break + if trace: + trace_lines.append(line) + if stop_on_heartbeat and self.sci1.saw_heartbeat(): + stopped_reason = "heartbeat" + break + return RunReport( + steps=self.cpu.steps, + cycles=self.cpu.cycles, + pc=self.cpu.pc, + stopped_reason=stopped_reason, + tx_bytes=bytes(self.sci1.tx_bytes), + tx_frames=list(self.sci1.tx_frames), + heartbeat_seen=self.sci1.saw_heartbeat(), + unsupported=unsupported, + trace=trace_lines, + ) + + def _execute_general_abs(self, raw: bytes, pc: int, next_pc: int) -> int: + size = "W" if raw[0] == 0x1D else "B" + address = int.from_bytes(raw[1:3], "big") + op = raw[3] + + if op == 0x06: + value = raw[4] + self.memory.write8(address, value) + elif op == 0x07: + value = int.from_bytes(raw[4:6], "big") + self.memory.write16(address, value) + elif 0x90 <= op <= 0x97: + reg = self.cpu.regs[op & 0x07] + if size == "W": + self.memory.write16(address, reg) + else: + self.memory.write8(address, reg & 0xFF) + elif 0x80 <= op <= 0x87: + reg = op & 0x07 + self.cpu.regs[reg] = self.memory.read16(address) if size == "W" else self.memory.read8(address) + elif 0xC0 <= op <= 0xCF: + bit = op & 0x0F + self.memory.write8(address, self.memory.read8(address) | (1 << bit)) + elif 0xD0 <= op <= 0xDF: + bit = op & 0x0F + self.memory.write8(address, self.memory.read8(address) & ~(1 << bit)) + elif 0xF0 <= op <= 0xFF: + bit = op & 0x0F + self.cpu.z = not bool(self.memory.read8(address) & (1 << bit)) + elif op in (0x04, 0x05): + if op == 0x04: + value = raw[4] + actual = self.memory.read8(address) + else: + value = int.from_bytes(raw[4:6], "big") + actual = self.memory.read16(address) + self.cpu.z = actual == value + elif op == 0x08: + value = self.memory.read16(address) if size == "W" else self.memory.read8(address) + value = (value + 1) & (0xFFFF if size == "W" else 0xFF) + if size == "W": + self.memory.write16(address, value) + else: + self.memory.write8(address, value) + self.cpu.z = value == 0 + else: + raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text) + return next_pc + + def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int: + cond = raw[0] & 0x0F + disp = _s8(raw[1]) + target = (pc + 2 + disp) & 0xFFFF + if cond == 0x0: + return target + if cond == 0x7 and self.cpu.z: + return target + if cond == 0x6 and not self.cpu.z: + return target + return next_pc + + def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int: + cond = raw[0] & 0x0F + disp = _s16(int.from_bytes(raw[1:3], "big")) + target = (pc + 3 + disp) & 0xFFFF + if cond == 0x0: + return target + if cond == 0x7 and self.cpu.z: + return target + if cond == 0x6 and not self.cpu.z: + return target + return next_pc + + def _rough_cycles(self, raw: bytes) -> int: + if raw[0] in (0x15, 0x1D): + return 9 + if raw[0] in range(0x20, 0x40): + return 8 if (raw[0] & 0x0F) == 0 else 4 + return 3 + + +def _s8(value: int) -> int: + return value - 0x100 if value & 0x80 else value + + +def _s16(value: int) -> int: + return value - 0x10000 if value & 0x8000 else value + + +def discover_rom_path(root: Path) -> Path | None: + candidates = [ + root / "ROM" / "M27C512@DIP28_1.BIN", + root / "rom.bin", + ] + candidates.extend(sorted((root / "ROM").glob("*.BIN")) if (root / "ROM").exists() else []) + candidates.extend(sorted((root / "ROM").glob("*.bin")) if (root / "ROM").exists() else []) + for candidate in candidates: + if candidate.is_file(): + return candidate + return None + + +def load_rom(path: Path | None = None, root: Path | None = None) -> tuple[bytes, Path]: + root = root if root is not None else Path.cwd() + rom_path = path if path is not None else discover_rom_path(root) + if rom_path is None: + raise FileNotFoundError( + "could not discover ROM bytes; pass --rom PATH, expected ROM/M27C512@DIP28_1.BIN or another ROM/*.BIN" + ) + return rom_path.read_bytes(), rom_path + + +def describe_regions(regions: Iterable[MemoryRegion] = MEMORY_REGIONS) -> str: + return "\n".join(f"{h16(region.start)}-{h16(region.end)} {region.name} {region.kind}" for region in regions) + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Minimal H8/536 emulation harness scaffold") + parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present") + parser.add_argument("--max-steps", type=int, default=64, help="maximum CPU steps to execute") + parser.add_argument("--trace", action="store_true", help="print decoded/executed instruction trace") + parser.add_argument("--stop-on-heartbeat", action="store_true", help="stop only when 00 00 00 00 80 DA is emitted through SCI1 TDR") + parser.add_argument("--memory-map", action="store_true", help="print the scaffolded memory map before running") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_arg_parser().parse_args(argv) + try: + rom_bytes, rom_path = load_rom(args.rom) + except FileNotFoundError as exc: + print(str(exc)) + return 2 + + emulator = H8536Emulator(rom_bytes) + print(f"rom={rom_path}") + print(f"reset_vector={h16(emulator.reset_vector())}") + if args.memory_map: + print(describe_regions()) + report = emulator.run(args.max_steps, trace=args.trace, stop_on_heartbeat=args.stop_on_heartbeat) + if args.trace: + for line in report.trace: + print(line) + for line in report.summary_lines(): + print(line) + if not report.heartbeat_seen: + print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR") + return 0 diff --git a/h8536/protocol_capture.py b/h8536/protocol_capture.py index 0620977..8d1ca53 100644 --- a/h8536/protocol_capture.py +++ b/h8536/protocol_capture.py @@ -18,8 +18,8 @@ CHECKSUM_SEED = getattr(_protocol_trace, "CHECKSUM_SEED", 0x5A) FRAME_LENGTH = getattr(_protocol_trace, "FRAME_LENGTH", 6) CAPTURE_LINE_RE = re.compile( r"^\s*(?P