From e141f3b30dc77a147eb97f38660d25ee6df5d341 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 21:25:10 +1000 Subject: [PATCH] Emualtor RX side --- README.md | 11 +- h8536/emulator/__init__.py | 4 + h8536/emulator/constants.py | 2 + h8536/emulator/memory.py | 5 + h8536/emulator/runner.py | 36 +++ h8536/emulator/rx_probe.py | 470 ++++++++++++++++++++++++++++++++ h8536_emulator_rx_probe.py | 5 + tests/test_emulator.py | 39 +++ tests/test_emulator_rx_probe.py | 27 ++ 9 files changed, 597 insertions(+), 2 deletions(-) create mode 100644 h8536/emulator/rx_probe.py create mode 100644 h8536_emulator_rx_probe.py create mode 100644 tests/test_emulator_rx_probe.py diff --git a/README.md b/README.md index 703c3f9..bf73c0a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ To start the current emulator harness: .\.venv\Scripts\python.exe h8536_emulator.py --max-steps 1000000 --stop-on-heartbeat --interval-steps 512 .\.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 ``` ## What It Does @@ -81,8 +82,9 @@ To start the current emulator harness: - Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`. - Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes. - Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns. -- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, scaffolded SCI1 TXI/interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path. +- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path. - Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM. +- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, listens for device TX frames, and reports serial latch/table/LCD-buffer effects. Current serial observations: @@ -92,6 +94,7 @@ Current serial observations: - Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms. - Static/runtime finding: `F9C4` is a candidate idle heartbeat/report countdown. Init loads `H'14`, `loc_BA26` reloads `H'07` after a send, FRT2 OCIA decrements it, and `loc_4046` can enqueue report `H'0000` when it reaches zero and the queue is empty. - Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`. +- RX probe finding: the `--preset connect-lcd` sequence reaches the command-`0x04` handler; `04 00 00 80 00 DE` writes table index zero, fills the LCD line buffer with `CONNECT: OK`, and emits `02 00 02 00 00 5A` in the current emulator model. - Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM. The generated listing is written to: @@ -190,6 +193,7 @@ For the emulator harness: ```powershell python h8536_emulator.py --help python h8536_emulator_probe.py --help +python h8536_emulator_rx_probe.py --help ``` - `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`. @@ -201,6 +205,8 @@ python h8536_emulator_probe.py --help - `--p9-fast-path`: shortcut known P9 transfer routines for exploration. - `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history. - `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame. +- `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 --preset connect-lcd`: replay the current CONNECT LCD activation candidates. - Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT1/FRT2 OCIA. With the P9 fast path and current timer cadence, the emulator reaches the SCI1 transmit path and emits the observed heartbeat frame `00 00 00 00 80 DA`. ## Code Layout @@ -233,6 +239,7 @@ python h8536_emulator_probe.py --help - `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation. - `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks. - `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, P9 bus model, runner, probe, CLI, and peripheral scaffolding. +- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments. - `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path. - `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis. - `h8536/pseudocode.py`: JSON-to-C-like pseudocode generation. @@ -242,4 +249,4 @@ python h8536_emulator_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_consistency.py`: sidecar analysis CLI wrappers. -- `h8536_emulator.py`, `h8536_emulator_probe.py`: emulator CLI wrappers. +- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`: emulator CLI wrappers. diff --git a/h8536/emulator/__init__.py b/h8536/emulator/__init__.py index 9ea2d3c..a6106f5 100644 --- a/h8536/emulator/__init__.py +++ b/h8536/emulator/__init__.py @@ -37,6 +37,8 @@ from .constants import ( VECTOR_FRT1_OCIA, VECTOR_INTERVAL_TIMER, VECTOR_FRT2_OCIA, + VECTOR_SCI1_ERI, + VECTOR_SCI1_RXI, VECTOR_SCI1_TXI, WDT_TCSR_R, ) @@ -95,6 +97,8 @@ __all__ = [ "VECTOR_FRT1_OCIA", "VECTOR_INTERVAL_TIMER", "VECTOR_FRT2_OCIA", + "VECTOR_SCI1_ERI", + "VECTOR_SCI1_RXI", "VECTOR_SCI1_TXI", "WDT_TCSR_R", "build_arg_parser", diff --git a/h8536/emulator/constants.py b/h8536/emulator/constants.py index a06d32e..8411ae5 100644 --- a/h8536/emulator/constants.py +++ b/h8536/emulator/constants.py @@ -43,4 +43,6 @@ HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) VECTOR_INTERVAL_TIMER = 0x0042 VECTOR_FRT1_OCIA = 0x0062 VECTOR_FRT2_OCIA = 0x006A +VECTOR_SCI1_ERI = 0x0080 +VECTOR_SCI1_RXI = 0x0082 VECTOR_SCI1_TXI = 0x0084 diff --git a/h8536/emulator/memory.py b/h8536/emulator/memory.py index 615fdd1..681e383 100644 --- a/h8536/emulator/memory.py +++ b/h8536/emulator/memory.py @@ -115,6 +115,11 @@ class MemoryMap: self.write8(address, (value >> 8) & 0xFF) self.write8((address + 1) & 0xFFFF, value & 0xFF) + def inject_sci1_rx_byte(self, value: int) -> None: + self.sci1.inject_rx(value) + self._set_register(SCI1_RDR, self.sci1.read(SCI1_RDR)) + self._set_register(SCI1_SSR, self.sci1.read(SCI1_SSR)) + def _set_register(self, address: int, value: int) -> None: self.registers[address - REGISTER_FIELD_START] = value & 0xFF diff --git a/h8536/emulator/runner.py b/h8536/emulator/runner.py index 8e4ca46..2d4e7ce 100644 --- a/h8536/emulator/runner.py +++ b/h8536/emulator/runner.py @@ -16,11 +16,19 @@ from .constants import ( IPRA, IPRC, IPRE, + SCI_SCR_RE, + SCI_SCR_RIE, SCI_SCR_TIE, + SCI_SSR_FER, + SCI_SSR_ORER, + SCI_SSR_PER, + SCI_SSR_RDRF, SCI_SSR_TDRE, VECTOR_FRT1_OCIA, VECTOR_FRT2_OCIA, VECTOR_INTERVAL_TIMER, + VECTOR_SCI1_ERI, + VECTOR_SCI1_RXI, VECTOR_SCI1_TXI, ) from .cpu import CPUState, mask, s8, s16, sign_bit @@ -97,6 +105,9 @@ class H8536Emulator: return self.memory.rom.u16(0) raise DecodeError("ROM does not contain a reset vector at H'0000") + def inject_sci1_rx_byte(self, value: int) -> None: + self.memory.inject_sci1_rx_byte(value) + def step(self) -> str: pc = self.cpu.pc if self.p9_fast_path.try_handle(self): @@ -116,6 +127,8 @@ class H8536Emulator: pass elif raw[0] == 0x02 and len(raw) == 2: self._pop_register_mask(raw[1]) + elif raw[0] == 0x11 and len(raw) >= 2: + next_pc = self._indirect_jump_call(raw, pc, next_pc) elif raw[0] == 0x12 and len(raw) == 2: self._push_register_mask(raw[1]) elif raw[0] in (0x01, 0x06, 0x07) and len(raw) == 3 and 0xB8 <= raw[1] <= 0xBF: @@ -366,6 +379,20 @@ class H8536Emulator: return (next_pc + s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF return int.from_bytes(raw[1:3], "big") + def _indirect_jump_call(self, raw: bytes, pc: int, next_pc: int) -> int: + op = raw[1] + if 0xC0 <= op <= 0xDF: + target = self.cpu.regs[op & 0x07] & 0xFFFF + elif 0xE0 <= op <= 0xEF and len(raw) >= 3: + target = (self.cpu.regs[op & 0x07] + s8(raw[2])) & 0xFFFF + elif 0xF0 <= op <= 0xFF and len(raw) >= 4: + target = (self.cpu.regs[op & 0x07] + s16(int.from_bytes(raw[2:4], "big"))) & 0xFFFF + else: + raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text) + if 0xC8 <= op <= 0xCF or 0xD8 <= op <= 0xDF or 0xE8 <= op <= 0xEF or 0xF8 <= op <= 0xFF: + self._push16(next_pc) + return target + def _scb(self, raw: bytes, pc: int, next_pc: int) -> int: reg = raw[1] & 0x07 condition = {0x01: False, 0x06: not self.cpu.z, 0x07: self.cpu.z}[raw[0]] @@ -389,6 +416,15 @@ class H8536Emulator: if self.cpu.interrupt_depth: return candidates: list[tuple[int, int, str]] = [] + sci1_rx_interrupts_enabled = bool(self.sci1.scr & SCI_SCR_RIE and self.sci1.scr & SCI_SCR_RE) + if sci1_rx_interrupts_enabled and self.sci1.ssr & (SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER): + target = self._vector_target(VECTOR_SCI1_ERI) + if target is not None: + candidates.append((self._sci1_priority(), target, "sci1_eri")) + if sci1_rx_interrupts_enabled and self.sci1.ssr & SCI_SSR_RDRF: + target = self._vector_target(VECTOR_SCI1_RXI) + if target is not None: + candidates.append((self._sci1_priority(), target, "sci1_rxi")) if self.sci1.scr & SCI_SCR_TIE and self.sci1.ssr & SCI_SSR_TDRE: target = self._vector_target(VECTOR_SCI1_TXI) if target is not None: diff --git a/h8536/emulator/rx_probe.py b/h8536/emulator/rx_probe.py new file mode 100644 index 0000000..669f4d0 --- /dev/null +++ b/h8536/emulator/rx_probe.py @@ -0,0 +1,470 @@ +from __future__ import annotations + +import argparse +from collections import Counter +from dataclasses import dataclass, field +from pathlib import Path +from typing import Callable, Iterable + +from ..formatting import h16, parse_int +from .cli import load_rom +from .constants import ( + IPRE, + SCI_SCR_RE, + SCI_SCR_RIE, + SCI_SSR_RDRF, + VECTOR_SCI1_RXI, +) +from .errors import UnsupportedInstruction +from .memory import MemoryAccess +from .runner import H8536Emulator + + +CHECKSUM_SEED = 0x5A +FRAME_LENGTH = 6 + +CONNECT_LCD_FRAMES = ( + bytes.fromhex("04000040001E"), + bytes.fromhex("0400008000DE"), + bytes.fromhex("040000C0009E"), +) + +WATCH_PCS = { + 0xBB57: "sci1_eri_entry", + 0xBB67: "sci1_rxi_entry", + 0xBBD6: "rx_checksum_seed", + 0xBBF0: "rx_checksum_compare", + 0xBC08: "command_dispatch", + 0xBD0E: "command_04_handler", + 0xBCCD: "command_04_send", + 0xBE05: "command_07_handler", + 0xBE29: "rx_error_or_retry", + 0xBA26: "tx_builder", + 0xBA72: "tx_first_byte", + 0xBA84: "txi_entry", + 0x3ECC: "lcd_line_buffer_entry", + 0x3F28: "lcd_driver_stage", + 0x3F40: "lcd_port_writer", +} + +WATCH_RANGES = ( + (0x2CA6, 0x2D20, "connect_display_window"), + (0xBB57, 0xBE6F, "sci1_rx_command_window"), +) + +ACCESS_RANGES = ( + (0xF850, 0xF85D, "tx_staging_or_frame"), + (0xF860, 0xF86D, "rx_validation_or_capture"), + (0xF870, 0xF96F, "report_queue"), + (0xF970, 0xF9AF, "secondary_dispatch_or_table"), + (0xF9B0, 0xF9C8, "serial_gate_state"), + (0xFAA2, 0xFAA6, "serial_latches"), + (0xFAF0, 0xFAFF, "lcd_line_buffer"), + (0xE000, 0xE001, "primary_table_index_0000"), + (0xE400, 0xE401, "secondary_table_index_0000"), + (0xE800, 0xE801, "current_table_index_0000"), + (0xEC00, 0xEC01, "flag_table_index_0000"), + (0xF200, 0xF201, "lcd_ports"), +) + +STATE_BYTES = { + 0xF9B0: "queue_head", + 0xF9B5: "queue_tail", + 0xF9C0: "tx_gate", + 0xF9C1: "rx_interbyte_timeout", + 0xF9C3: "rx_index", + 0xF9C4: "idle_heartbeat_gate", + 0xF9C5: "rx_session_timeout", + 0xF9C6: "resend_period_hi", + 0xF9C7: "resend_period_lo", + 0xF9C8: "resend_countdown", + 0xFAA2: "session_flags", + 0xFAA3: "pending_mask", + 0xFAA4: "rx_error_latch", + 0xFAA5: "retry_or_gate_flags", + 0xFAA6: "retry_counter", +} + +STATE_WORDS = { + 0xE000: "E000_index_0000_primary", + 0xE400: "E400_index_0000_secondary", + 0xE800: "E800_index_0000_current", + 0xF860: "rx_frame_01", + 0xF862: "rx_frame_23", + 0xF864: "rx_frame_45", + 0xF970: "F970_selector_zero_dispatch", +} + + +@dataclass +class RunContext: + pc_hits: Counter[str] = field(default_factory=Counter) + first_pcs: list[tuple[int, str]] = field(default_factory=list) + unsupported: str | None = None + + def record_pc(self, pc: int) -> None: + label = WATCH_PCS.get(pc) + if label is None: + for start, end, range_label in WATCH_RANGES: + if start <= pc <= end: + label = range_label + break + if label is None: + return + self.pc_hits[label] += 1 + if len(self.first_pcs) < 32: + self.first_pcs.append((pc, label)) + + +@dataclass(frozen=True) +class FrameResult: + input_frame: bytes + checksum_ok: bool + steps: int + stopped_reason: str + new_tx_bytes: bytes + new_tx_frames: list[bytes] + state_before: dict[str, int | str] + state_after: dict[str, int | str] + accesses: list[MemoryAccess] + context: RunContext + + def lines(self, index: int) -> list[str]: + lines = [ + f"host_frame[{index}]={format_frame(self.input_frame)} checksum_ok={int(self.checksum_ok)}", + f" stopped={self.stopped_reason} steps={self.steps}", + f" new_tx_bytes={format_frame(self.new_tx_bytes) if self.new_tx_bytes else 'none'}", + ] + if self.new_tx_frames: + lines.append(" new_tx_frames=" + " | ".join(format_frame(frame) for frame in self.new_tx_frames)) + else: + lines.append(" new_tx_frames=none") + state_changes = _state_change_lines(self.state_before, self.state_after) + if state_changes: + lines.append(" state_changes:") + lines.extend(f" {line}" for line in state_changes) + pc_lines = _pc_hit_lines(self.context) + if pc_lines: + lines.append(" pc_hits:") + lines.extend(f" {line}" for line in pc_lines) + access_lines = _access_lines(self.accesses) + if access_lines: + lines.append(" interesting_accesses:") + lines.extend(f" {line}" for line in access_lines) + if self.context.unsupported: + lines.append(f" unsupported={self.context.unsupported}") + return lines + + +def parse_frame(text: str) -> bytes: + normalized = text.strip().replace(",", " ").replace(":", " ").replace("-", " ").replace("_", " ") + parts = normalized.split() + if len(parts) == 1: + compact = parts[0] + if compact.lower().startswith("0x"): + compact = compact[2:] + if compact.upper().startswith("H'"): + compact = compact[2:] + if len(compact) % 2: + raise argparse.ArgumentTypeError("frame compact hex must have an even number of digits") + parts = [compact[index : index + 2] for index in range(0, len(compact), 2)] + values = [_parse_byte(part) for part in parts] + if len(values) == FRAME_LENGTH - 1: + values.append(frame_checksum(bytes(values))) + if len(values) != FRAME_LENGTH: + raise argparse.ArgumentTypeError("frame must contain 5 bytes plus computed checksum or exactly 6 bytes") + return bytes(values) + + +def frame_checksum(data: bytes) -> int: + checksum = CHECKSUM_SEED + for value in data[: FRAME_LENGTH - 1]: + checksum ^= value + return checksum & 0xFF + + +def frame_checksum_ok(frame: bytes) -> bool: + return len(frame) == FRAME_LENGTH and frame_checksum(frame) == frame[-1] + + +def format_frame(data: bytes) -> str: + return data.hex(" ").upper() + + +def run_rx_probe( + frames: Iterable[bytes], + *, + rom_path: Path | None = None, + boot_steps: int = 250_000, + per_byte_steps: int = 5_000, + post_frame_steps: int = 80_000, + interval_steps: int = 512, + frt1_ocia_steps: int = 512, + frt2_ocia_steps: int = 512, + p9_fast_path: bool = True, + p9_fast_input: int = 0xFF, + stop_after_tx_frame: bool = True, +) -> tuple[Path, H8536Emulator, str, list[FrameResult]]: + rom_bytes, discovered_rom_path = load_rom(rom_path) + emulator = H8536Emulator( + rom_bytes, + interval_steps=interval_steps, + frt1_ocia_steps=frt1_ocia_steps, + frt2_ocia_steps=frt2_ocia_steps, + p9_fast_path_enabled=p9_fast_path, + p9_fast_default_input_byte=p9_fast_input, + ) + + boot_context = RunContext() + boot_steps_used, boot_reason = _run_until(emulator, 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))}" + ) + + results = [ + _run_frame( + emulator, + frame, + per_byte_steps=per_byte_steps, + post_frame_steps=post_frame_steps, + stop_after_tx_frame=stop_after_tx_frame, + ) + for frame in frames + ] + return discovered_rom_path, emulator, boot_summary, results + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Inject host SCI1 frames into the H8/536 emulator and listen for ROM TX responses.") + 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 when present") + parser.add_argument("--preset", choices=("connect-lcd",), help="append a built-in host-frame set") + parser.add_argument("--boot-steps", type=int, default=250_000, help="maximum steps to boot until SCI1 RXI is serviceable") + parser.add_argument("--per-byte-steps", type=int, default=5_000, help="maximum steps after each injected RX byte") + parser.add_argument("--post-frame-steps", type=int, default=80_000, help="maximum steps after a full injected frame") + parser.add_argument("--keep-listening", action="store_true", help="use all post-frame steps instead of stopping at the first new TX frame") + parser.add_argument("--interval-steps", type=int, default=512, help="rough step period for the scaffolded interval timer interrupt") + parser.add_argument("--frt1-ocia-steps", type=int, default=512, help="rough step period for the scaffolded FRT1 OCIA interrupt") + parser.add_argument("--frt2-ocia-steps", type=int, default=512, help="rough step period for the scaffolded FRT2 OCIA interrupt") + parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines") + parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_arg_parser().parse_args(argv) + frames = list(args.frames) + if args.preset == "connect-lcd": + frames.extend(CONNECT_LCD_FRAMES) + if not frames: + raise SystemExit("pass at least one frame or use --preset connect-lcd") + + rom_path, emulator, boot_summary, results = run_rx_probe( + frames, + rom_path=args.rom, + boot_steps=args.boot_steps, + per_byte_steps=args.per_byte_steps, + post_frame_steps=args.post_frame_steps, + interval_steps=args.interval_steps, + frt1_ocia_steps=args.frt1_ocia_steps, + frt2_ocia_steps=args.frt2_ocia_steps, + p9_fast_path=not args.no_p9_fast_path, + p9_fast_input=args.p9_fast_input, + stop_after_tx_frame=not args.keep_listening, + ) + + print(f"rom={rom_path}") + print(f"reset_vector={h16(emulator.reset_vector())}") + print(boot_summary) + for index, result in enumerate(results): + for line in result.lines(index): + print(line) + print("total_tx_frames=" + " | ".join(format_frame(frame) for frame in emulator.sci1.tx_frames)) + return 0 + + +def _run_frame( + emulator: H8536Emulator, + frame: bytes, + *, + per_byte_steps: int, + post_frame_steps: int, + stop_after_tx_frame: bool, +) -> FrameResult: + state_before = _state_snapshot(emulator) + log_start = len(emulator.memory.access_log) + tx_byte_start = len(emulator.sci1.tx_bytes) + tx_frame_start = len(emulator.sci1.tx_frames) + context = RunContext() + stopped_reason = "post_frame_steps" + steps_total = 0 + + for offset, value in enumerate(frame): + emulator.inject_sci1_rx_byte(value) + steps, reason = _run_until(emulator, per_byte_steps, _rx_byte_consumed, context) + steps_total += steps + if reason != "predicate": + stopped_reason = f"rx_byte_{offset}_{reason}" + break + else: + target_frame_count = tx_frame_start + 1 + + def post_predicate(inner: H8536Emulator) -> bool: + return stop_after_tx_frame and len(inner.sci1.tx_frames) >= target_frame_count + + steps, reason = _run_until(emulator, post_frame_steps, post_predicate, context) + steps_total += steps + stopped_reason = "tx_frame" if reason == "predicate" and stop_after_tx_frame else reason + + log_end = len(emulator.memory.access_log) + state_after = _state_snapshot(emulator) + return FrameResult( + input_frame=frame, + checksum_ok=frame_checksum_ok(frame), + steps=steps_total, + stopped_reason=stopped_reason, + new_tx_bytes=bytes(emulator.sci1.tx_bytes[tx_byte_start:]), + new_tx_frames=list(emulator.sci1.tx_frames[tx_frame_start:]), + state_before=state_before, + state_after=state_after, + accesses=emulator.memory.access_log[log_start:log_end], + context=context, + ) + + +def _run_until( + emulator: H8536Emulator, + max_steps: int, + predicate: Callable[[H8536Emulator], bool], + context: RunContext, +) -> tuple[int, str]: + for index in range(max_steps): + if predicate(emulator): + return index, "predicate" + pc = emulator.cpu.pc + context.record_pc(pc) + try: + emulator.step() + except UnsupportedInstruction as exc: + context.unsupported = str(exc) + return index, "unsupported_instruction" + return max_steps, "max_steps" + + +def _rx_ready(emulator: H8536Emulator) -> bool: + if not (emulator.sci1.scr & SCI_SCR_RIE and emulator.sci1.scr & SCI_SCR_RE): + return False + if emulator.vectors.get(VECTOR_SCI1_RXI) is None: + return False + return _sci1_priority(emulator) > _interrupt_mask(emulator) + + +def _rx_byte_consumed(emulator: H8536Emulator) -> bool: + return not (emulator.sci1.ssr & SCI_SSR_RDRF) and emulator.cpu.interrupt_depth == 0 + + +def _sci1_priority(emulator: H8536Emulator) -> int: + return (emulator.memory.read8(IPRE) >> 4) & 0x07 + + +def _interrupt_mask(emulator: H8536Emulator) -> int: + return (emulator.cpu.sr >> 8) & 0x07 + + +def _state_snapshot(emulator: H8536Emulator) -> dict[str, int | str]: + snapshot: dict[str, int | str] = {} + for address, name in STATE_BYTES.items(): + snapshot[name] = emulator.memory.read8(address) + for address, name in STATE_WORDS.items(): + snapshot[name] = emulator.memory.read16(address) + snapshot["lcd_line_buffer_ascii"] = _ascii_window(emulator, 0xFAF0, 16) + snapshot["tx_frame_staging"] = format_frame(bytes(emulator.memory.read8(0xF850 + offset) for offset in range(6))) + snapshot["rx_frame_validation"] = format_frame(bytes(emulator.memory.read8(0xF860 + offset) for offset in range(6))) + return snapshot + + +def _ascii_window(emulator: H8536Emulator, start: int, length: int) -> str: + chars = [] + for offset in range(length): + value = emulator.memory.read8(start + offset) + chars.append(chr(value) if 0x20 <= value <= 0x7E else ".") + return "".join(chars) + + +def _state_change_lines(before: dict[str, int | str], after: dict[str, int | str]) -> list[str]: + lines = [] + for key in sorted(after): + if before.get(key) == after[key]: + continue + old = _state_value(before.get(key)) + new = _state_value(after[key]) + lines.append(f"{key}: {old}->{new}") + return lines + + +def _state_value(value: int | str | None) -> str: + if isinstance(value, int): + return f"H'{value:04X}" if value > 0xFF else f"H'{value:02X}" + return repr(value) + + +def _pc_hit_lines(context: RunContext) -> list[str]: + lines = [f"{name}={count}" for name, count in sorted(context.pc_hits.items())] + if context.first_pcs: + first = ", ".join(f"{h16(pc)}:{label}" for pc, label in context.first_pcs[:16]) + lines.append(f"first={first}") + return lines + + +def _access_lines(accesses: list[MemoryAccess]) -> list[str]: + interesting = [access for access in accesses if _interesting_access(access)] + lines = [] + for access in interesting[:80]: + label = _access_label(access.address) + lines.append(f"{access.kind:<5} {h16(access.address)} {access.value:02X} {label}") + if len(interesting) > 80: + lines.append(f"... {len(interesting) - 80} more interesting accesses") + return lines + + +def _interesting_access(access: MemoryAccess) -> bool: + if access.kind == "write": + return _access_label(access.address) != "" + return _access_label(access.address) in {"secondary_dispatch_or_table", "lcd_ports"} + + +def _access_label(address: int) -> str: + for start, end, label in ACCESS_RANGES: + if start <= address <= end: + return label + return "" + + +def _parse_byte(text: str) -> int: + token = text.strip() + if token.lower().startswith("0x"): + token = token[2:] + if token.upper().startswith("H'"): + token = token[2:] + if not token or len(token) > 2: + raise argparse.ArgumentTypeError(f"invalid byte token {text!r}") + try: + value = int(token, 16) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"invalid byte token {text!r}") from exc + if not 0 <= value <= 0xFF: + raise argparse.ArgumentTypeError(f"byte out of range {text!r}") + return value + + +__all__ = [ + "CONNECT_LCD_FRAMES", + "format_frame", + "frame_checksum", + "frame_checksum_ok", + "main", + "parse_frame", + "run_rx_probe", +] diff --git a/h8536_emulator_rx_probe.py b/h8536_emulator_rx_probe.py new file mode 100644 index 0000000..9c04fc0 --- /dev/null +++ b/h8536_emulator_rx_probe.py @@ -0,0 +1,5 @@ +from h8536.emulator.rx_probe import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_emulator.py b/tests/test_emulator.py index 68ecad2..30fc3f6 100644 --- a/tests/test_emulator.py +++ b/tests/test_emulator.py @@ -7,11 +7,16 @@ from h8536.emulator import ( IPRE, ON_CHIP_RAM_START, REGISTER_FIELD_START, + SCI1_RDR, SCI1_SCR, SCI1_SSR, SCI1_TDR, + SCI_SCR_RE, + SCI_SCR_RIE, SCI_SCR_TE, + SCI_SSR_RDRF, SCI_SSR_TDRE, + VECTOR_SCI1_RXI, H8536Emulator, MemoryMap, SCI1, @@ -106,6 +111,18 @@ class EmulatorHarnessTest(unittest.TestCase): self.assertEqual(emulator.cpu.regs[1], 0x1234) self.assertEqual(emulator.cpu.regs[7], 0xFE80) + def test_jmp_register_uses_register_target(self): + rom = rom_with_reset(size=0x1020) + rom[0x1000:0x1003] = b"\x59\x10\x10" # MOV:I.W #H'1010, R1 + rom[0x1003:0x1005] = b"\x11\xD1" # JMP @R1 + rom[0x1010:0x1013] = b"\x58\x12\x34" # MOV:I.W #H'1234, R0 + + emulator = H8536Emulator(bytes(rom)) + emulator.run(max_steps=3) + + self.assertEqual(emulator.cpu.regs[0], 0x1234) + self.assertEqual(emulator.cpu.pc, 0x1013) + def test_sci1_txi_interrupt_can_emit_through_tdr(self): rom = rom_with_reset(size=0x1040) rom[0x0084:0x0086] = (0x1010).to_bytes(2, "big") @@ -123,6 +140,28 @@ class EmulatorHarnessTest(unittest.TestCase): self.assertEqual(bytes(emulator.sci1.tx_bytes), b"\x42") self.assertFalse(report.heartbeat_seen) + def test_sci1_rxi_interrupt_consumes_injected_rdr_byte(self): + rom = rom_with_reset(size=0x1040) + rom[VECTOR_SCI1_RXI : VECTOR_SCI1_RXI + 2] = (0x1010).to_bytes(2, "big") + rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7 + rom[0x1003:0x1008] = bytes([0x15, (IPRE >> 8) & 0xFF, IPRE & 0xFF, 0x06, 0x70]) + rom[0x1008:0x100D] = bytes( + [0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, SCI_SCR_RIE | SCI_SCR_RE], + ) + rom[0x100D] = 0x00 + rom[0x1010:0x1014] = bytes([0x15, (SCI1_SSR >> 8) & 0xFF, SCI1_SSR & 0xFF, 0xD6]) + rom[0x1014:0x1018] = bytes([0x15, (SCI1_RDR >> 8) & 0xFF, SCI1_RDR & 0xFF, 0x80]) + rom[0x1018:0x101C] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x90]) + rom[0x101C] = 0x0A + + emulator = H8536Emulator(bytes(rom)) + emulator.run(max_steps=3) + emulator.inject_sci1_rx_byte(0xA5) + emulator.run(max_steps=5) + + self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 0xA5) + self.assertFalse(emulator.sci1.read(SCI1_SSR) & SCI_SSR_RDRF) + def test_interval_interrupt_vector_can_fire(self): rom = rom_with_reset(size=0x1040) rom[0x0042:0x0044] = (0x1020).to_bytes(2, "big") diff --git a/tests/test_emulator_rx_probe.py b/tests/test_emulator_rx_probe.py new file mode 100644 index 0000000..5a45326 --- /dev/null +++ b/tests/test_emulator_rx_probe.py @@ -0,0 +1,27 @@ +import argparse +import unittest + +from h8536.emulator.rx_probe import frame_checksum, frame_checksum_ok, parse_frame + + +class EmulatorRxProbeTest(unittest.TestCase): + def test_parse_frame_accepts_five_bytes_and_appends_checksum(self): + frame = parse_frame("04 00 00 40 00") + + self.assertEqual(frame, bytes.fromhex("04000040001E")) + self.assertTrue(frame_checksum_ok(frame)) + + def test_parse_frame_accepts_compact_checked_frame(self): + frame = parse_frame("0780684030C5") + + self.assertEqual(frame, bytes.fromhex("0780684030C5")) + self.assertEqual(frame_checksum(frame), 0xC5) + self.assertTrue(frame_checksum_ok(frame)) + + def test_parse_frame_rejects_wrong_length(self): + with self.assertRaises(argparse.ArgumentTypeError): + parse_frame("04 00 00 40") + + +if __name__ == "__main__": + unittest.main()