diff --git a/README.md b/README.md index 3cc2e66..899872d 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,9 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\ - 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, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, 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 and emulated-LCD effects. +- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, can optionally schedule 38400 8N1 byte arrivals at real UART spacing, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects. - Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs. -- Includes a bench-log replay harness that feeds recorded host TX frames back into the ROM emulator and asserts parity against the real device's observed response/LCD state. +- Includes a bench-log replay harness that feeds recorded host TX frames back into the ROM emulator with bench-style UART byte timing by default and asserts parity against the real device's observed response/LCD state. Current serial observations: @@ -216,9 +216,10 @@ python h8536_emulator_rx_probe.py --help - `--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 --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. - `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text. -- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. +- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode. - 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, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`. ## Code Layout @@ -250,7 +251,7 @@ python h8536_emulator_rx_probe.py --help - `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer. - `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, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding. +- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, 38400 8N1 UART injection timing, P9 bus model, LCD model, manual-derived FRT timer scheduling, 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. diff --git a/h8536/emulator/__init__.py b/h8536/emulator/__init__.py index 051b9e0..39f335f 100644 --- a/h8536/emulator/__init__.py +++ b/h8536/emulator/__init__.py @@ -58,6 +58,7 @@ from .memory import MemoryAccess, MemoryMap, describe_regions from .peripherals import LCD from .runner import H8536Emulator, RunReport from .sci import SCI1, SciTxEvent +from .uart import UartTiming __all__ = [ "CPUState", @@ -114,6 +115,7 @@ __all__ = [ "SCI_SSR_TDRE", "SciTxEvent", "UnsupportedInstruction", + "UartTiming", "VECTOR_FRT1_OCIA", "VECTOR_INTERVAL_TIMER", "VECTOR_FRT2_OCIA", diff --git a/h8536/emulator/bench_replay.py b/h8536/emulator/bench_replay.py index 66ad19d..1a57d17 100644 --- a/h8536/emulator/bench_replay.py +++ b/h8536/emulator/bench_replay.py @@ -14,6 +14,8 @@ from .errors import UnsupportedInstruction from .runner import H8536Emulator from .rx_probe import ( RunContext, + UartTiming, + _inject_frame_uart_timed, _interrupt_mask, _rx_byte_consumed, _rx_ready, @@ -88,6 +90,7 @@ class ReplayFrameResult: host_frame: bytes host_timestamp: str host_delta_ms: int + rx_injection: str steps_before: int steps_during_rx: int emulator_gap_frames_before: tuple[bytes, ...] @@ -120,6 +123,7 @@ class BenchReplayResult: "host_timestamp": item.host_timestamp, "host_delta_ms": item.host_delta_ms, "host_frame": format_frame(item.host_frame), + "rx_injection": item.rx_injection, "steps_before": item.steps_before, "steps_during_rx": item.steps_during_rx, "emulator_gap_frames_before": [format_frame(frame) for frame in item.emulator_gap_frames_before], @@ -150,6 +154,7 @@ class BenchReplayResult: lines.append( ( f" [{index}] {item.host_timestamp} delta={item.host_delta_ms}ms " + f"rx_injection={item.rx_injection} " f"steps_before={item.steps_before} steps_rx={item.steps_during_rx} " f"host={format_frame(item.host_frame)} " f"gap_emu={_format_frame_list(item.emulator_gap_frames_before)} " @@ -171,6 +176,8 @@ class ReplayConfig: frt1_ocia_steps: int | None = None frt2_ocia_steps: int | None = None clock_hz: int = 10_000_000 + uart_timing: bool = True + uart_baud: int = 38_400 p9_fast_path: bool = True p9_fast_input: int = 0xFF @@ -256,12 +263,25 @@ def run_bench_replay(log_path: Path, *, rom_path: Path | None = None, config: Re steps_before = _run_cycles_for_ms(emulator, delta_ms, config.clock_hz, context) gap_frames = tuple(emulator.sci1.tx_frames[tx_frame_start_before_delay:]) tx_frame_start = len(emulator.sci1.tx_frames) - steps_during_rx = _inject_host_frame(emulator, host.frame, config.per_byte_steps, context) + if config.uart_timing: + timing = UartTiming(baud=config.uart_baud) + steps_during_rx, inject_reason = _inject_frame_uart_timed( + emulator, + host.frame, + timing=timing, + max_steps_per_gap=config.per_byte_steps, + context=context, + ) + rx_injection = f"{timing.summary(emulator.clock_hz)} reason={inject_reason}" + else: + steps_during_rx = _inject_host_frame(emulator, host.frame, config.per_byte_steps, context) + rx_injection = "polite_wait_for_rdrf_clear" replay_results.append( ReplayFrameResult( host_frame=host.frame, host_timestamp=host.timestamp, host_delta_ms=delta_ms, + rx_injection=rx_injection, steps_before=steps_before, steps_during_rx=steps_during_rx, emulator_gap_frames_before=gap_frames, @@ -336,10 +356,12 @@ def build_arg_parser() -> argparse.ArgumentParser: parser.add_argument("log", type=Path, help="bench log produced by scripts/bench_connect_lcd_sequence.py") parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN") parser.add_argument("--boot-steps", type=int, default=ReplayConfig.boot_steps) - parser.add_argument("--per-byte-steps", type=int, default=ReplayConfig.per_byte_steps) + parser.add_argument("--per-byte-steps", type=int, default=ReplayConfig.per_byte_steps, help="UART mode step limit between byte arrivals, or polite mode byte-consume limit") parser.add_argument("--post-log-steps", type=int, default=ReplayConfig.post_log_steps) parser.add_argument("--interval-steps", type=int, default=ReplayConfig.interval_steps) parser.add_argument("--clock-hz", type=lambda text: int(text, 0), default=ReplayConfig.clock_hz) + parser.add_argument("--uart-baud", type=lambda text: int(text, 0), default=ReplayConfig.uart_baud, help="baud rate for bench-style UART injection") + parser.add_argument("--polite-rx", action="store_true", help="wait for each RX byte to be consumed before injecting the next byte") parser.add_argument("--frt1-ocia-steps", type=int, default=ReplayConfig.frt1_ocia_steps) parser.add_argument("--frt2-ocia-steps", type=int, default=ReplayConfig.frt2_ocia_steps) parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines") @@ -362,6 +384,8 @@ def main(argv: list[str] | None = None) -> int: frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, clock_hz=args.clock_hz, + uart_timing=not args.polite_rx, + uart_baud=args.uart_baud, p9_fast_path=not args.no_p9_fast_path, p9_fast_input=args.p9_fast_input, ), diff --git a/h8536/emulator/rx_probe.py b/h8536/emulator/rx_probe.py index fdf5f6c..e95672d 100644 --- a/h8536/emulator/rx_probe.py +++ b/h8536/emulator/rx_probe.py @@ -18,6 +18,7 @@ from .constants import ( from .errors import UnsupportedInstruction from .memory import MemoryAccess from .runner import H8536Emulator +from .uart import UartTiming CHECKSUM_SEED = 0x5A @@ -120,6 +121,8 @@ class RunContext: class FrameResult: input_frame: bytes checksum_ok: bool + rx_injection: str + uart_byte_cycles: int | None steps: int stopped_reason: str new_tx_bytes: bytes @@ -132,6 +135,7 @@ class FrameResult: def lines(self, index: int) -> list[str]: lines = [ f"host_frame[{index}]={format_frame(self.input_frame)} checksum_ok={int(self.checksum_ok)}", + f" rx_injection={self.rx_injection}", 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'}", ] @@ -201,6 +205,8 @@ def run_rx_probe( boot_steps: int = 250_000, per_byte_steps: int = 5_000, post_frame_steps: int = 80_000, + uart_timing: bool = False, + uart_baud: int = 38_400, interval_steps: int = 512, frt1_ocia_steps: int | None = None, frt2_ocia_steps: int | None = None, @@ -236,6 +242,8 @@ def run_rx_probe( frame, per_byte_steps=per_byte_steps, post_frame_steps=post_frame_steps, + uart_timing=uart_timing, + uart_baud=uart_baud, stop_after_tx_frame=stop_after_tx_frame, ) for frame in frames @@ -249,8 +257,10 @@ def build_arg_parser() -> argparse.ArgumentParser: 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("--per-byte-steps", type=int, default=5_000, help="polite mode byte-consume limit, or UART mode step limit between byte arrivals") parser.add_argument("--post-frame-steps", type=int, default=80_000, help="maximum steps after a full injected frame") + parser.add_argument("--uart-timing", action="store_true", help="inject frame bytes at real 8N1 UART inter-byte timing instead of waiting for RDRF consumption") + parser.add_argument("--uart-baud", type=parse_int, default=38_400, help="baud rate for --uart-timing; 38400 gives about 260 us per 8N1 byte") 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("--clock-hz", type=parse_int, default=10_000_000, help="CPU/phi clock in Hz for calibrated FRT timing") @@ -275,6 +285,8 @@ def main(argv: list[str] | None = None) -> int: boot_steps=args.boot_steps, per_byte_steps=args.per_byte_steps, post_frame_steps=args.post_frame_steps, + uart_timing=args.uart_timing, + uart_baud=args.uart_baud, interval_steps=args.interval_steps, frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, @@ -300,6 +312,8 @@ def _run_frame( *, per_byte_steps: int, post_frame_steps: int, + uart_timing: bool, + uart_baud: int, stop_after_tx_frame: bool, ) -> FrameResult: state_before = _state_snapshot(emulator) @@ -309,15 +323,29 @@ def _run_frame( context = RunContext() stopped_reason = "post_frame_steps" steps_total = 0 + timing = UartTiming(baud=uart_baud) - 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 + if uart_timing: + steps_total, stopped_reason = _inject_frame_uart_timed( + emulator, + frame, + timing=timing, + max_steps_per_gap=per_byte_steps, + context=context, + ) + injected_all_bytes = stopped_reason == "frame_injected_uart_timing" else: + injected_all_bytes = True + 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}" + injected_all_bytes = False + break + + if injected_all_bytes: target_frame_count = tx_frame_start + 1 def post_predicate(inner: H8536Emulator) -> bool: @@ -332,6 +360,8 @@ def _run_frame( return FrameResult( input_frame=frame, checksum_ok=frame_checksum_ok(frame), + rx_injection=timing.summary(emulator.clock_hz) if uart_timing else "polite_wait_for_rdrf_clear", + uart_byte_cycles=timing.cycles_per_character(emulator.clock_hz) if uart_timing else None, steps=steps_total, stopped_reason=stopped_reason, new_tx_bytes=bytes(emulator.sci1.tx_bytes[tx_byte_start:]), @@ -343,6 +373,28 @@ def _run_frame( ) +def _inject_frame_uart_timed( + emulator: H8536Emulator, + frame: bytes, + *, + timing: UartTiming, + max_steps_per_gap: int, + context: RunContext, +) -> tuple[int, str]: + steps_total = 0 + start_cycles = emulator.cpu.cycles + byte_cycles = timing.cycles_per_character(emulator.clock_hz) + for offset, value in enumerate(frame): + if offset: + target_cycles = start_cycles + (offset * byte_cycles) + steps, reason = _run_until_cycle(emulator, target_cycles, max_steps_per_gap, context) + steps_total += steps + if reason != "target_cycle": + return steps_total, f"rx_byte_{offset}_{reason}" + emulator.inject_sci1_rx_byte(value) + return steps_total, "frame_injected_uart_timing" + + def _run_until( emulator: H8536Emulator, max_steps: int, @@ -362,6 +414,27 @@ def _run_until( return max_steps, "max_steps" +def _run_until_cycle( + emulator: H8536Emulator, + target_cycles: int, + max_steps: int, + context: RunContext, +) -> tuple[int, str]: + for index in range(max(0, max_steps)): + if emulator.cpu.cycles >= target_cycles: + return index, "target_cycle" + pc = emulator.cpu.pc + context.record_pc(pc) + try: + emulator.step() + except UnsupportedInstruction as exc: + context.unsupported = str(exc) + return index, "unsupported_instruction" + if emulator.cpu.cycles >= target_cycles: + return max(0, max_steps), "target_cycle" + return max(0, 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 @@ -477,4 +550,5 @@ __all__ = [ "main", "parse_frame", "run_rx_probe", + "UartTiming", ] diff --git a/h8536/emulator/sci.py b/h8536/emulator/sci.py index e828feb..1a6378e 100644 --- a/h8536/emulator/sci.py +++ b/h8536/emulator/sci.py @@ -105,6 +105,11 @@ class SCI1: self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags) def inject_rx(self, value: int) -> None: + if self.ssr & SCI_SSR_ORER: + return + if self.ssr & SCI_SSR_RDRF: + self.ssr |= SCI_SSR_ORER + return self.rdr = value & 0xFF self.ssr |= SCI_SSR_RDRF diff --git a/h8536/emulator/uart.py b/h8536/emulator/uart.py new file mode 100644 index 0000000..f87b2bd --- /dev/null +++ b/h8536/emulator/uart.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UartTiming: + baud: int = 38_400 + data_bits: int = 8 + parity_bits: int = 0 + stop_bits: int = 1 + start_bits: int = 1 + + @property + def bits_per_character(self) -> int: + return self.start_bits + self.data_bits + self.parity_bits + self.stop_bits + + def seconds_per_character(self) -> float: + return self.bits_per_character / max(1, self.baud) + + def micros_per_character(self) -> float: + return 1_000_000.0 * self.seconds_per_character() + + def cycles_per_character(self, clock_hz: int) -> int: + return max(1, round(max(1, clock_hz) * self.seconds_per_character())) + + def summary(self, clock_hz: int) -> str: + return ( + f"uart_{self.data_bits}{'N' if self.parity_bits == 0 else 'P'}{self.stop_bits} " + f"baud={self.baud} byte_us={self.micros_per_character():.3f} " + f"byte_cycles={self.cycles_per_character(clock_hz)}" + ) + + +__all__ = ["UartTiming"] diff --git a/tests/test_emulator_rx_probe.py b/tests/test_emulator_rx_probe.py index 5a45326..7546cae 100644 --- a/tests/test_emulator_rx_probe.py +++ b/tests/test_emulator_rx_probe.py @@ -1,7 +1,15 @@ import argparse import unittest -from h8536.emulator.rx_probe import frame_checksum, frame_checksum_ok, parse_frame +from h8536.emulator import H8536Emulator, SCI1_RDR, SCI1_SSR, SCI_SSR_ORER, SCI_SSR_RDRF +from h8536.emulator.rx_probe import RunContext, UartTiming, _inject_frame_uart_timed, frame_checksum, frame_checksum_ok, parse_frame + + +def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1020) -> bytearray: + rom = bytearray([0xFF] * size) + rom[0:2] = reset.to_bytes(2, "big") + rom[reset : reset + 2] = b"\x20\xFE" # BRA self + return rom class EmulatorRxProbeTest(unittest.TestCase): @@ -22,6 +30,23 @@ class EmulatorRxProbeTest(unittest.TestCase): with self.assertRaises(argparse.ArgumentTypeError): parse_frame("04 00 00 40") + def test_uart_timed_injection_does_not_wait_for_rdrf_consumption(self): + emulator = H8536Emulator(bytes(rom_with_reset()), clock_hz=10_000_000) + context = RunContext() + + steps, reason = _inject_frame_uart_timed( + emulator, + b"\x11\x22", + timing=UartTiming(baud=38_400), + max_steps_per_gap=1000, + context=context, + ) + + self.assertEqual(reason, "frame_injected_uart_timing") + self.assertGreater(steps, 0) + self.assertEqual(emulator.memory.read8(SCI1_RDR), 0x11) + self.assertEqual(emulator.memory.read8(SCI1_SSR) & (SCI_SSR_RDRF | SCI_SSR_ORER), SCI_SSR_RDRF | SCI_SSR_ORER) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_emulator_sci_timing.py b/tests/test_emulator_sci_timing.py index 0f56e05..702d351 100644 --- a/tests/test_emulator_sci_timing.py +++ b/tests/test_emulator_sci_timing.py @@ -12,6 +12,7 @@ from h8536.emulator import ( SCI_SSR_RDRF, SCI_SSR_TDRE, SCI1, + UartTiming, ) @@ -66,6 +67,22 @@ class SciTimingTest(unittest.TestCase): self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME]) self.assertTrue(sci.saw_heartbeat()) + def test_inject_rx_sets_overrun_if_rdrf_is_still_full(self): + sci = SCI1() + + sci.inject_rx(0x11) + sci.inject_rx(0x22) + + self.assertEqual(sci.read(SCI1_SSR) & (SCI_SSR_RDRF | SCI_SSR_ORER), SCI_SSR_RDRF | SCI_SSR_ORER) + self.assertEqual(sci.rdr, 0x11) + + def test_uart_8n1_38400_byte_timing(self): + timing = UartTiming(baud=38_400) + + self.assertEqual(timing.bits_per_character, 10) + self.assertAlmostEqual(timing.micros_per_character(), 260.416666, places=3) + self.assertEqual(timing.cycles_per_character(10_000_000), 2604) + if __name__ == "__main__": unittest.main()