From 85732f8754428ea409a1aa8b46e07cd71458129f Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 26 May 2026 12:33:51 +1000 Subject: [PATCH] RX side improvements --- README.md | 5 +- .../bench-sync-cmd0-readback-zero-eeprom.txt | 31 + build/bench-sync-cmd0-readback-zero.bin | Bin 0 -> 4096 bytes build/bench-sync-cmd0-seed-zero-eeprom.txt | 31 + build/bench-sync-cmd0-seed-zero.bin | Bin 0 -> 4096 bytes h8536/emulator/rx_divergence.py | 529 ++++++++++++++++++ h8536_emulator_rx_divergence.py | 5 + tests/test_emulator_rx_divergence.py | 80 +++ 8 files changed, 680 insertions(+), 1 deletion(-) create mode 100644 build/bench-sync-cmd0-readback-zero-eeprom.txt create mode 100644 build/bench-sync-cmd0-readback-zero.bin create mode 100644 build/bench-sync-cmd0-seed-zero-eeprom.txt create mode 100644 build/bench-sync-cmd0-seed-zero.bin create mode 100644 h8536/emulator/rx_divergence.py create mode 100644 h8536_emulator_rx_divergence.py create mode 100644 tests/test_emulator_rx_divergence.py diff --git a/README.md b/README.md index e47f6a4..53da223 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ To start the current emulator harness: .\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 4000000 --stop-on-tx .\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 1000000 --stop-on-tx --p9-fast-path .\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --preset connect-lcd +.\.venv\Scripts\python.exe h8536_emulator_rx_divergence.py --default-frames --uart-timing --wait-heartbeats 2 --summary-only .\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen .\.venv\Scripts\python.exe scripts\serial_ack_probe.py --ack-frame "05 00 40 00 00 1F" .\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json @@ -237,6 +238,7 @@ For the emulator harness: python h8536_emulator.py --help python h8536_emulator_probe.py --help python h8536_emulator_rx_probe.py --help +python h8536_emulator_rx_divergence.py --help ``` - `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`. @@ -258,6 +260,7 @@ python h8536_emulator_rx_probe.py --help - `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses. - `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with 8N1 wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`. - `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates. +- `h8536_emulator_rx_divergence.py --default-frames --uart-timing --wait-heartbeats 2 --summary-only`: run the focused RX divergence trace for the bench mismatch. It flags whether a frame reached cmd0 `BC69`, cmd1 `BCD7`, retry echo, command-7 replay, autonomous `BAF2` report output, or the TX/RX overlap-collapse path. - `scripts\serial_table_dump.py --port COM5 --relay-port COM6 --start 0x000 --count 0x200 --log captures\table-read.txt`: read-only command-1 sweep of the firmware-exposed serial table state for EEPROM/shadow inference. - `scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json`: run the focused `0x000 -> 0x001` retry probe with immediate reactive ACK and a 2 ms poll interval, to test whether command 5 can arrive before the second `07 80 40 20 90 2D` retry. - `scripts\serial_scenario.py scenarios\early-ack-000-001.json --log captures\early-ack-000-001.txt --result-json captures\early-ack-000-001-result.json`: send the same command-1 pair, then send command-5 ACK immediately without waiting for the retry frame. @@ -316,7 +319,7 @@ python h8536_emulator_rx_probe.py --help - `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper. - `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers. - `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_eeprom_layout.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers. -- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers. +- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_rx_divergence.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers. - `h8536_emulator_state_search.py`: emulator CONNECT state-search CLI wrapper. - `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence. - `scripts/serial_table_dump.py`: read-only COM5/COM6 command-1 table sweep for inferring live EEPROM-backed parameter state. diff --git a/build/bench-sync-cmd0-readback-zero-eeprom.txt b/build/bench-sync-cmd0-readback-zero-eeprom.txt new file mode 100644 index 0000000..826be88 --- /dev/null +++ b/build/bench-sync-cmd0-readback-zero-eeprom.txt @@ -0,0 +1,31 @@ +Emulator EEPROM Snapshot + +size=0x1000 sha256=4bed7704e1ea085487ca325c43bd60da75d37b6ae6f8292544e069a8825c64c6 +writes: bytes=0 words=0 factory_diff_words=0 + +Persistent Records: +- page 0x0 EEPROM 0x000-0x007 bytes=00 00 6B 6F FE 00 00 00 text='..ko....' +- page 0x1 EEPROM 0x100-0x107 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x2 EEPROM 0x200-0x207 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x3 EEPROM 0x300-0x307 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x4 EEPROM 0x400-0x407 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x5 EEPROM 0x500-0x507 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x6 EEPROM 0x600-0x607 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x7 EEPROM 0x700-0x707 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x8 EEPROM 0x800-0x807 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x9 EEPROM 0x900-0x907 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xA EEPROM 0xA00-0xA07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xB EEPROM 0xB00-0xB07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xC EEPROM 0xC00-0xC07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xD EEPROM 0xD00-0xD07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xE EEPROM 0xE00-0xE07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xF EEPROM 0xF00-0xF07 bytes=20 20 20 20 20 20 20 20 text=' ' + +EEPROM Word Writes: +- none since EEPROM setup/load + +Factory Diffs: +- current EEPROM image matches ROM factory/default image + +F400 Shadow Diffs: +- H'F4AA offset=0xAA expected=0x8000 actual=0x5500 (factory_shadow_offset; selectors=0x112) diff --git a/build/bench-sync-cmd0-readback-zero.bin b/build/bench-sync-cmd0-readback-zero.bin new file mode 100644 index 0000000000000000000000000000000000000000..337213dec5865114454d8464cd10362b788e24ae GIT binary patch literal 4096 zcmeHKF%pC@2;9QLXXGjQ&lUFto_wX1cWk^3R#sNdl0k?JjBZFE!0WvOh-5wk>pIjq zM*|k*%TD_@dP-{K8lTkLX)eo_ZqiF*1d*&)5;KOp?vm|KF#_@%{Qre5@$mXP|Gocx z{`vf86>|M^{bzm5x|m$U{g3;Q7Mf(NF=1LBxVeG-6h@s9|*(%lxyJo MPvsWzc=rA02g0`rCjbBd literal 0 HcmV?d00001 diff --git a/build/bench-sync-cmd0-seed-zero-eeprom.txt b/build/bench-sync-cmd0-seed-zero-eeprom.txt new file mode 100644 index 0000000..826be88 --- /dev/null +++ b/build/bench-sync-cmd0-seed-zero-eeprom.txt @@ -0,0 +1,31 @@ +Emulator EEPROM Snapshot + +size=0x1000 sha256=4bed7704e1ea085487ca325c43bd60da75d37b6ae6f8292544e069a8825c64c6 +writes: bytes=0 words=0 factory_diff_words=0 + +Persistent Records: +- page 0x0 EEPROM 0x000-0x007 bytes=00 00 6B 6F FE 00 00 00 text='..ko....' +- page 0x1 EEPROM 0x100-0x107 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x2 EEPROM 0x200-0x207 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x3 EEPROM 0x300-0x307 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x4 EEPROM 0x400-0x407 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x5 EEPROM 0x500-0x507 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x6 EEPROM 0x600-0x607 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x7 EEPROM 0x700-0x707 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x8 EEPROM 0x800-0x807 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0x9 EEPROM 0x900-0x907 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xA EEPROM 0xA00-0xA07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xB EEPROM 0xB00-0xB07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xC EEPROM 0xC00-0xC07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xD EEPROM 0xD00-0xD07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xE EEPROM 0xE00-0xE07 bytes=20 20 20 20 20 20 20 20 text=' ' +- page 0xF EEPROM 0xF00-0xF07 bytes=20 20 20 20 20 20 20 20 text=' ' + +EEPROM Word Writes: +- none since EEPROM setup/load + +Factory Diffs: +- current EEPROM image matches ROM factory/default image + +F400 Shadow Diffs: +- H'F4AA offset=0xAA expected=0x8000 actual=0x5500 (factory_shadow_offset; selectors=0x112) diff --git a/build/bench-sync-cmd0-seed-zero.bin b/build/bench-sync-cmd0-seed-zero.bin new file mode 100644 index 0000000000000000000000000000000000000000..337213dec5865114454d8464cd10362b788e24ae GIT binary patch literal 4096 zcmeHKF%pC@2;9QLXXGjQ&lUFto_wX1cWk^3R#sNdl0k?JjBZFE!0WvOh-5wk>pIjq zM*|k*%TD_@dP-{K8lTkLX)eo_ZqiF*1d*&)5;KOp?vm|KF#_@%{Qre5@$mXP|Gocx z{`vf86>|M^{bzm5x|m$U{g3;Q7Mf(NF=1LBxVeG-6h@s9|*(%lxyJo MPvsWzc=rA02g0`rCjbBd literal 0 HcmV?d00001 diff --git a/h8536/emulator/rx_divergence.py b/h8536/emulator/rx_divergence.py new file mode 100644 index 0000000..024e8a6 --- /dev/null +++ b/h8536/emulator/rx_divergence.py @@ -0,0 +1,529 @@ +from __future__ import annotations + +import argparse +from collections import Counter +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable + +from ..formatting import h16, parse_int +from .cli import load_rom +from .constants import HEARTBEAT_FRAME +from .memory import MemoryAccess +from .runner import H8536Emulator +from .rx_probe import ( + UartTiming, + _inject_frame_uart_timed, + _interrupt_mask, + _rx_byte_consumed, + _rx_ready, + _run_until, + _sci1_priority, + format_frame, + frame_checksum_ok, + parse_frame, +) + + +DEFAULT_EEPROM_LOAD = Path("build") / "bench-sync-after-dip-reset.bin" +DEFAULT_FRAMES = ( + bytes.fromhex("0000008000DA"), + bytes.fromhex("01000000005B"), + bytes.fromhex("07000000005D"), +) + +BENCH_SIGNATURES = { + bytes.fromhex("04000080805E"): "bench_cmd0_echo_80", + bytes.fromhex("02000200005A"): "bench_connect_ok", + bytes.fromhex("07804040A07D"): "bench_40a0_retry_echo_candidate_40", + bytes.fromhex("07808040A0BD"): "bench_40a0_retry_echo_candidate_80", + bytes.fromhex("0780C040A0FD"): "bench_40a0_retry_echo_candidate_c0", +} + +WATCH_PCS = { + 0x3FD3: "report_scheduler_gate", + 0x3FEF: "report_scheduler_return", + 0x4007: "resend_or_session_gate", + 0xBBF0: "rx_checksum_compare", + 0xBE29: "rx_checksum_retry_error", + 0xBE4D: "retry_echo_stage", + 0xBC0F: "faa2_split", + 0xBC15: "initial_latch_faa2_bit7", + 0xBC33: "initial_dispatch_fallthrough", + 0xBC3A: "continuation_dispatch", + 0xBC5C: "continuation_clear_queued_ack", + 0xBC67: "continuation_reenter_initial", + 0xBC69: "cmd0_set_value", + 0xBCD7: "cmd1_read_value", + 0xBE05: "cmd7_copy_or_retry", + 0xBE70: "selector_queue_be70", + 0xBA26: "tx_builder_ba26", + 0xBA72: "tx_first_byte", + 0xBA84: "txi_entry_ba84", + 0xBA8A: "txi_faa2_bit3_test", + 0xBA90: "txi_rx_index_test", + 0xBA96: "txi_overlap_collapse", + 0xBA9A: "txi_clear_pending_mask", + 0xBAA2: "txi_disable_tie", + 0xBAB5: "txi_next_byte", + 0xBAF2: "report_dequeue_baf2", + 0xBB00: "report_session_latch", + 0xBB08: "report_selector_read", + 0xBB1C: "report_selector_encode", + 0xBB20: "report_selector_encode_hi", + 0xBB2B: "report_payload_read", + 0xBB35: "report_value_stage", + 0xBB43: "report_send", + 0xBE9E: "session_resend_gate", + 0xBED5: "session_resend_send", +} + +WATCH_GROUPS = { + "cmd0_reached_BC69": (0xBC69,), + "cmd1_reached_BCD7": (0xBCD7,), + "retry_echo": (0xBE29, 0xBE4D), + "cmd7_replay": (0xBE05,), + "autonomous_report": (0xBAF2, 0xBB00, 0xBB35, 0xBB43), + "tx_rx_overlap_collapse": (0xBA96, 0xBA9A, 0xBAA2), +} + +WATCH_WRITE_RANGES = ( + (0xF860, 0xF865, "rx_validation_F860_F865"), + (0xF868, 0xF86D, "rx_capture_F868_F86D"), + (0xF850, 0xF85D, "tx_staging_F850_F85D"), + (0xF870, 0xF96F, "report_queue_F870_F96F"), + (0xF970, 0xF9AF, "selector_queue_F970_F9AF"), + (0xF9B0, 0xF9B0, "report_head_F9B0"), + (0xF9B4, 0xF9B4, "selector_tail_F9B4"), + (0xF9B5, 0xF9B5, "report_tail_F9B5"), + (0xF9C0, 0xF9C0, "tx_gate_F9C0"), + (0xF9C1, 0xF9C1, "rx_interbyte_timeout_F9C1"), + (0xF9C3, 0xF9C3, "rx_index_F9C3"), + (0xF9C5, 0xF9C5, "rx_session_timeout_F9C5"), + (0xFAA2, 0xFAA6, "serial_latches_FAA2_FAA6"), + (0xE000, 0xE001, "primary_table_E000"), + (0xE800, 0xE801, "current_table_E800"), + (0xEC00, 0xEC01, "flag_table_EC00"), +) + +STATE_BYTES = { + 0xF9B0: "F9B0_report_head", + 0xF9B4: "F9B4_selector_tail", + 0xF9B5: "F9B5_report_tail", + 0xF9C0: "F9C0_tx_gate", + 0xF9C1: "F9C1_rx_interbyte_timeout", + 0xF9C3: "F9C3_rx_index", + 0xF9C5: "F9C5_rx_session_timeout", + 0xFAA2: "FAA2_session_flags", + 0xFAA3: "FAA3_pending_mask", + 0xFAA4: "FAA4_rx_error_latch", + 0xFAA5: "FAA5_retry_gate_flags", + 0xFAA6: "FAA6_retry_counter", +} + +STATE_BUFFERS = { + "F850_F85D_tx_staging": (0xF850, 14), + "F860_F865_rx_validation": (0xF860, 6), + "F868_F86D_rx_capture": (0xF868, 6), + "F870_F87F_report_queue_head": (0xF870, 16), + "F970_F97F_selector_queue_head": (0xF970, 16), + "E000_E001_primary_0000": (0xE000, 2), + "E800_E801_current_0000": (0xE800, 2), + "EC00_EC01_flags_0000": (0xEC00, 2), + "E880_E881_current_0040": (0xE880, 2), + "E900_E901_current_0080": (0xE900, 2), + "E980_E981_current_00C0": (0xE980, 2), +} + + +@dataclass +class DivergenceContext: + pc_hits: Counter[int] = field(default_factory=Counter) + first_pcs: list[int] = field(default_factory=list) + unsupported: str | None = None + + def record_pc(self, pc: int) -> None: + if pc not in WATCH_PCS: + return + self.pc_hits[pc] += 1 + if len(self.first_pcs) < 32: + self.first_pcs.append(pc) + + +@dataclass(frozen=True) +class RxDivergenceConfig: + boot_steps: int = 250_000 + wait_heartbeats: int = 0 + wait_heartbeat_steps: int = 500_000 + post_frame_steps: int = 80_000 + per_byte_steps: int = 5_000 + interval_steps: int = 512 + frt1_ocia_steps: int | None = None + frt2_ocia_steps: int | None = None + clock_hz: int = 10_000_000 + uart_timing: bool = False + uart_baud: int = 38_400 + p7_input: int = 0xFF + p9_fast_path: bool = True + p9_fast_input: int = 0xFF + p9_fast_optimistic_wrapper: bool = False + eeprom_seed: str = "blank" + eeprom_load: Path | None = DEFAULT_EEPROM_LOAD + stop_after_tx_frame: bool = False + + +@dataclass(frozen=True) +class FrameTrace: + frame: bytes + checksum_ok: bool + rx_injection: str + steps: int + stopped_reason: str + tx_frames: tuple[bytes, ...] + state_changes: tuple[str, ...] + writes: tuple[str, ...] + context: DivergenceContext + + def outcome(self) -> str: + flags = classify_hits(self.context.pc_hits) + return " ".join(f"{name}={int(value)}" for name, value in flags.items()) + + def lines(self, index: int, *, summary_only: bool = False) -> list[str]: + lines = [ + ( + f"frame[{index}] host={format_frame(self.frame)} checksum_ok={int(self.checksum_ok)} " + f"steps={self.steps} stopped={self.stopped_reason} {self.outcome()}" + ) + ] + if self.tx_frames: + lines.append(" tx=" + " | ".join(label_frame(frame) for frame in self.tx_frames)) + else: + lines.append(" tx=none") + if summary_only: + return lines + lines.append(f" rx_injection={self.rx_injection}") + hit_lines = format_pc_hits(self.context) + if hit_lines: + lines.append(" pcs=" + " ".join(hit_lines)) + if self.state_changes: + lines.append(" state=" + "; ".join(self.state_changes)) + if self.writes: + lines.append(" writes:") + lines.extend(f" {line}" for line in self.writes) + if self.context.unsupported: + lines.append(f" unsupported={self.context.unsupported}") + return lines + + +@dataclass(frozen=True) +class RxDivergenceResult: + rom_path: Path + eeprom_load: Path | None + reset_vector: int + boot_summary: str + heartbeat_summary: str | None + traces: tuple[FrameTrace, ...] + all_tx_frames: tuple[bytes, ...] + + def lines(self, *, summary_only: bool = False) -> list[str]: + lines = [ + f"rom={self.rom_path}", + f"eeprom_loaded={self.eeprom_load if self.eeprom_load else 'none'}", + f"reset_vector={h16(self.reset_vector)}", + self.boot_summary, + bench_signature_line(), + ] + if self.heartbeat_summary is not None: + lines.append(self.heartbeat_summary) + for index, trace in enumerate(self.traces): + lines.extend(trace.lines(index, summary_only=summary_only)) + lines.append("all_tx=" + _format_frame_list(self.all_tx_frames)) + return lines + + +def run_rx_divergence( + frames: Iterable[bytes], + *, + rom_path: Path | None = None, + config: RxDivergenceConfig = RxDivergenceConfig(), +) -> RxDivergenceResult: + rom_bytes, discovered_rom_path = load_rom(rom_path) + emulator = H8536Emulator( + rom_bytes, + interval_steps=config.interval_steps, + frt1_ocia_steps=config.frt1_ocia_steps, + frt2_ocia_steps=config.frt2_ocia_steps, + clock_hz=config.clock_hz, + p9_fast_path_enabled=config.p9_fast_path, + p9_fast_default_input_byte=config.p9_fast_input, + p9_fast_default_wrapper_success=config.p9_fast_optimistic_wrapper, + p7_input=config.p7_input, + eeprom_seed=config.eeprom_seed, + ) + eeprom_load = config.eeprom_load + if eeprom_load is not None and eeprom_load.is_file(): + emulator.memory.load_eeprom_image(eeprom_load.read_bytes()) + + boot_context = DivergenceContext() + boot_steps_used, boot_reason = _run_until(emulator, config.boot_steps, _rx_ready, boot_context) + boot_summary = ( + f"boot={boot_reason} steps={boot_steps_used} pc={h16(emulator.cpu.pc)} " + f"SCR={emulator.sci1.scr:02X} SSR={emulator.sci1.ssr:02X} " + f"rx_serviceable={int(_rx_ready(emulator))} " + f"sci1_priority={_sci1_priority(emulator)} interrupt_mask={_interrupt_mask(emulator)} " + f"clock_hz={emulator.clock_hz} p7_input={config.p7_input:#04x}" + ) + + heartbeat_summary = None + if config.wait_heartbeats: + heartbeat_summary = _wait_for_heartbeats(emulator, config.wait_heartbeats, config.wait_heartbeat_steps) + + traces = tuple(_trace_frame(emulator, frame, config) for frame in frames) + return RxDivergenceResult( + rom_path=discovered_rom_path, + eeprom_load=eeprom_load if eeprom_load is not None and eeprom_load.is_file() else None, + reset_vector=emulator.reset_vector(), + boot_summary=boot_summary, + heartbeat_summary=heartbeat_summary, + traces=traces, + all_tx_frames=tuple(emulator.sci1.tx_frames), + ) + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Focused H8/536 RX divergence trace around command dispatch and serial state.") + parser.add_argument("frames", nargs="*", type=parse_frame, help="host frame hex; 5-byte inputs get a 0x5A-XOR checksum appended") + parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN") + parser.add_argument("--default-frames", action="store_true", help="append the known bench-divergence frame trio") + parser.add_argument("--boot-steps", type=int, default=RxDivergenceConfig.boot_steps) + parser.add_argument("--eeprom-load", type=Path, default=DEFAULT_EEPROM_LOAD, help="logical EEPROM image loaded before boot") + parser.add_argument("--no-eeprom-load", action="store_true", help="boot without loading the default bench EEPROM image") + parser.add_argument("--p7-input", type=parse_int, default=RxDivergenceConfig.p7_input) + parser.add_argument("--wait-heartbeats", type=int, default=RxDivergenceConfig.wait_heartbeats) + parser.add_argument("--wait-heartbeat-steps", type=int, default=RxDivergenceConfig.wait_heartbeat_steps) + parser.add_argument("--uart-timing", action="store_true", help="inject bytes at UART character timing instead of waiting for RDRF clear") + parser.add_argument("--uart-baud", type=parse_int, default=RxDivergenceConfig.uart_baud) + parser.add_argument("--post-frame-steps", type=int, default=RxDivergenceConfig.post_frame_steps) + parser.add_argument("--per-byte-steps", type=int, default=RxDivergenceConfig.per_byte_steps) + parser.add_argument("--clock-hz", type=parse_int, default=RxDivergenceConfig.clock_hz) + parser.add_argument("--interval-steps", type=int, default=RxDivergenceConfig.interval_steps) + parser.add_argument("--frt1-ocia-steps", type=int, default=RxDivergenceConfig.frt1_ocia_steps) + parser.add_argument("--frt2-ocia-steps", type=int, default=RxDivergenceConfig.frt2_ocia_steps) + parser.add_argument("--no-p9-fast-path", action="store_true") + parser.add_argument("--p9-fast-input", type=parse_int, default=RxDivergenceConfig.p9_fast_input) + parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true") + parser.add_argument("--eeprom-seed", choices=("blank", "factory"), default=RxDivergenceConfig.eeprom_seed) + parser.add_argument("--stop-after-tx-frame", action="store_true", help="stop each post-frame run after the first new TX frame") + parser.add_argument("--summary-only", action="store_true", help="omit detailed state/write trace lines") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_arg_parser().parse_args(argv) + frames = list(args.frames) + if args.default_frames: + frames.extend(DEFAULT_FRAMES) + if not frames: + raise SystemExit("pass at least one frame, or use --default-frames") + + result = run_rx_divergence( + frames, + rom_path=args.rom, + config=RxDivergenceConfig( + boot_steps=args.boot_steps, + wait_heartbeats=args.wait_heartbeats, + wait_heartbeat_steps=args.wait_heartbeat_steps, + post_frame_steps=args.post_frame_steps, + per_byte_steps=args.per_byte_steps, + interval_steps=args.interval_steps, + frt1_ocia_steps=args.frt1_ocia_steps, + frt2_ocia_steps=args.frt2_ocia_steps, + clock_hz=args.clock_hz, + uart_timing=args.uart_timing, + uart_baud=args.uart_baud, + p7_input=args.p7_input, + p9_fast_path=not args.no_p9_fast_path, + p9_fast_input=args.p9_fast_input, + p9_fast_optimistic_wrapper=args.p9_fast_optimistic_wrapper, + eeprom_seed=args.eeprom_seed, + eeprom_load=None if args.no_eeprom_load else args.eeprom_load, + stop_after_tx_frame=args.stop_after_tx_frame, + ), + ) + for line in result.lines(summary_only=args.summary_only): + print(line) + return 0 + + +def _trace_frame(emulator: H8536Emulator, frame: bytes, config: RxDivergenceConfig) -> FrameTrace: + state_before = snapshot_state(emulator) + log_start = len(emulator.memory.access_log) + tx_frame_start = len(emulator.sci1.tx_frames) + context = DivergenceContext() + steps_total = 0 + + if config.uart_timing: + timing = UartTiming(baud=config.uart_baud) + steps_total, stopped_reason = _inject_frame_uart_timed( + emulator, + frame, + timing=timing, + max_steps_per_gap=config.per_byte_steps, + context=context, + ) + rx_injection = timing.summary(emulator.clock_hz) + injected_all_bytes = stopped_reason == "frame_injected_uart_timing" + else: + rx_injection = "polite_wait_for_rdrf_clear" + injected_all_bytes = True + stopped_reason = "post_frame_steps" + for offset, value in enumerate(frame): + emulator.inject_sci1_rx_byte(value) + steps, reason = _run_until(emulator, config.per_byte_steps, _rx_byte_consumed, context) + steps_total += steps + if reason != "predicate": + stopped_reason = f"rx_byte_{offset}_{reason}" + injected_all_bytes = False + break + + if injected_all_bytes: + target_frame_count = tx_frame_start + 1 + + def stop_predicate(inner: H8536Emulator) -> bool: + return config.stop_after_tx_frame and len(inner.sci1.tx_frames) >= target_frame_count + + steps, reason = _run_until(emulator, config.post_frame_steps, stop_predicate, context) + steps_total += steps + stopped_reason = "tx_frame" if reason == "predicate" and config.stop_after_tx_frame else reason + + log_end = len(emulator.memory.access_log) + state_after = snapshot_state(emulator) + return FrameTrace( + frame=frame, + checksum_ok=frame_checksum_ok(frame), + rx_injection=rx_injection, + steps=steps_total, + stopped_reason=stopped_reason, + tx_frames=tuple(emulator.sci1.tx_frames[tx_frame_start:]), + state_changes=tuple(format_state_changes(state_before, state_after)), + writes=tuple(format_interesting_writes(emulator.memory.access_log[log_start:log_end])), + context=context, + ) + + +def _wait_for_heartbeats(emulator: H8536Emulator, target_count: int, max_steps: int) -> str: + start_count = _heartbeat_count(emulator.sci1.tx_frames) + + def predicate(inner: H8536Emulator) -> bool: + return _heartbeat_count(inner.sci1.tx_frames) - start_count >= target_count + + context = DivergenceContext() + steps, reason = _run_until(emulator, max_steps, predicate, context) + seen = _heartbeat_count(emulator.sci1.tx_frames) - start_count + return f"wait_heartbeats target={target_count} seen={seen} steps={steps} stopped={reason}" + + +def snapshot_state(emulator: H8536Emulator) -> dict[str, int | bytes]: + state: dict[str, int | bytes] = {} + for address, name in STATE_BYTES.items(): + state[name] = emulator.memory.read8(address) + for name, (address, length) in STATE_BUFFERS.items(): + state[name] = bytes(emulator.memory.read8(address + offset) for offset in range(length)) + return state + + +def format_state_changes(before: dict[str, int | bytes], after: dict[str, int | bytes]) -> list[str]: + lines = [] + for key in sorted(after): + if before.get(key) == after[key]: + continue + lines.append(f"{key}:{_state_value(before.get(key))}->{_state_value(after[key])}") + return lines + + +def format_interesting_writes(accesses: Iterable[MemoryAccess], *, limit: int = 96) -> list[str]: + lines = [] + total = 0 + for access in accesses: + if access.kind != "write": + continue + label = write_label(access.address) + if label is None: + continue + total += 1 + if len(lines) < limit: + lines.append(f"{h16(access.address)}={access.value:02X} {label}") + if total > limit: + lines.append(f"... {total - limit} more watched writes") + return lines + + +def write_label(address: int) -> str | None: + for start, end, label in WATCH_WRITE_RANGES: + if start <= address <= end: + return label + return None + + +def classify_hits(hits: Counter[int]) -> dict[str, bool]: + return {name: any(hits.get(pc, 0) for pc in pcs) for name, pcs in WATCH_GROUPS.items()} + + +def format_pc_hits(context: DivergenceContext) -> list[str]: + lines = [f"{h16(pc)}:{WATCH_PCS[pc]}={context.pc_hits[pc]}" for pc in sorted(context.pc_hits)] + if context.first_pcs: + first = ",".join(h16(pc) for pc in context.first_pcs[:16]) + lines.append(f"first={first}") + return lines + + +def label_frame(frame: bytes) -> str: + signature = BENCH_SIGNATURES.get(frame) + if signature: + return f"{format_frame(frame)} ({signature})" + if frame == HEARTBEAT_FRAME: + return f"{format_frame(frame)} (heartbeat)" + return format_frame(frame) + + +def bench_signature_line() -> str: + return "bench_signatures=" + " | ".join(label_frame(frame) for frame in BENCH_SIGNATURES) + + +def _heartbeat_count(frames: Iterable[bytes]) -> int: + return sum(1 for frame in frames if frame == HEARTBEAT_FRAME) + + +def _state_value(value: int | bytes | None) -> str: + if isinstance(value, bytes): + return format_frame(value) + if isinstance(value, int): + return f"{value:02X}" + return "none" + + +def _format_frame_list(frames: Iterable[bytes]) -> str: + items = [label_frame(frame) for frame in frames] + return " | ".join(items) if items else "none" + + +__all__ = [ + "BENCH_SIGNATURES", + "DEFAULT_EEPROM_LOAD", + "DEFAULT_FRAMES", + "DivergenceContext", + "FrameTrace", + "RxDivergenceConfig", + "RxDivergenceResult", + "bench_signature_line", + "build_arg_parser", + "classify_hits", + "format_interesting_writes", + "format_pc_hits", + "format_state_changes", + "label_frame", + "main", + "parse_frame", + "run_rx_divergence", + "write_label", +] diff --git a/h8536_emulator_rx_divergence.py b/h8536_emulator_rx_divergence.py new file mode 100644 index 0000000..4f3c93a --- /dev/null +++ b/h8536_emulator_rx_divergence.py @@ -0,0 +1,5 @@ +from h8536.emulator.rx_divergence import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_emulator_rx_divergence.py b/tests/test_emulator_rx_divergence.py new file mode 100644 index 0000000..92be801 --- /dev/null +++ b/tests/test_emulator_rx_divergence.py @@ -0,0 +1,80 @@ +import unittest +from collections import Counter + +from h8536.emulator.memory import MemoryAccess +from h8536.emulator.rx_divergence import ( + DivergenceContext, + STATE_BUFFERS, + bench_signature_line, + classify_hits, + format_interesting_writes, + format_pc_hits, + format_state_changes, + label_frame, + parse_frame, + write_label, +) + + +class EmulatorRxDivergenceTest(unittest.TestCase): + def test_reuses_probe_frame_parser_and_labels_bench_signature(self): + frame = parse_frame("04 00 00 80 80") + + self.assertEqual(frame, bytes.fromhex("04000080805E")) + self.assertIn("bench_cmd0_echo_80", label_frame(frame)) + self.assertIn("bench_connect_ok", bench_signature_line()) + + def test_pc_hit_summary_makes_dispatch_outcome_visible(self): + hits = Counter({0xBC69: 1, 0xBA26: 2}) + + summary = classify_hits(hits) + + self.assertTrue(summary["cmd0_reached_BC69"]) + self.assertFalse(summary["cmd1_reached_BCD7"]) + self.assertFalse(summary["retry_echo"]) + self.assertFalse(summary["cmd7_replay"]) + + def test_formats_watched_pc_hits_in_trace_order(self): + context = DivergenceContext() + for pc in (0xBC0F, 0xBC69, 0xBC69): + context.record_pc(pc) + + lines = format_pc_hits(context) + + self.assertIn("H'BC0F:faa2_split=1", lines) + self.assertIn("H'BC69:cmd0_set_value=2", lines) + self.assertIn("first=H'BC0F,H'BC69,H'BC69", lines) + + def test_filters_required_write_addresses(self): + accesses = [ + MemoryAccess(0xF860, 1, 0x12, "write", "on_chip_ram"), + MemoryAccess(0xE001, 1, 0x34, "write", "external"), + MemoryAccess(0xE002, 1, 0x56, "write", "external"), + MemoryAccess(0xFEDD, 1, 0x78, "write", "register"), + ] + + lines = format_interesting_writes(accesses) + + self.assertEqual(len(lines), 2) + self.assertIn("rx_validation_F860_F865", lines[0]) + self.assertIn("primary_table_E000", lines[1]) + self.assertEqual(write_label(0xFAA6), "serial_latches_FAA2_FAA6") + self.assertIsNone(write_label(0xE002)) + + def test_state_changes_formats_bytes_and_ints_compactly(self): + lines = format_state_changes( + {"FAA2_session_flags": 0, "F860_F865_rx_validation": b"\x00" * 6}, + {"FAA2_session_flags": 0x80, "F860_F865_rx_validation": bytes.fromhex("010203040506")}, + ) + + self.assertIn("FAA2_session_flags:00->80", lines) + self.assertIn("F860_F865_rx_validation:00 00 00 00 00 00->01 02 03 04 05 06", lines) + + def test_snapshots_visible_40a0_candidate_table_slots(self): + self.assertEqual(STATE_BUFFERS["E880_E881_current_0040"], (0xE880, 2)) + self.assertEqual(STATE_BUFFERS["E900_E901_current_0080"], (0xE900, 2)) + self.assertEqual(STATE_BUFFERS["E980_E981_current_00C0"], (0xE980, 2)) + + +if __name__ == "__main__": + unittest.main()