From 4b50d0e98f83a1684489d5d3f8b7c621238619d2 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 22:17:36 +1000 Subject: [PATCH] timing adjustments --- README.md | 16 ++--- h8536/emulator/__init__.py | 18 ++++++ h8536/emulator/bench_replay.py | 30 ++++++--- h8536/emulator/cli.py | 6 +- h8536/emulator/constants.py | 9 +++ h8536/emulator/memory.py | 15 +++++ h8536/emulator/probe.py | 12 ++-- h8536/emulator/runner.py | 64 ++++++++++++++----- h8536/emulator/rx_probe.py | 13 ++-- h8536/emulator/timers.py | 109 +++++++++++++++++++++++++++++++++ tests/test_emulator_timers.py | 61 ++++++++++++++++++ 11 files changed, 313 insertions(+), 40 deletions(-) create mode 100644 h8536/emulator/timers.py diff --git a/README.md b/README.md index 28b2739..3cc2e66 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\ - 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, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval/FRT1-OCIA/FRT2-OCIA interrupt 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. +- 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 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. @@ -99,10 +99,11 @@ Current serial observations: - Capture-side label: `heartbeat_alive_candidate`. - 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. +- Emulator timing finding: the ROM initializes FRT2 with `TCR=H'02` and `OCRA=H'7A12`; using the manual's `phi/32` prescaler gives a 1,000,000-cycle OCIA period, so the default `--clock-hz 10000000` models that tick as 100 ms and the post-send `F9C4=H'07` heartbeat delay as about 700 ms. - 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`. - Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue. -- 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. -- Bench follow-up: replaying the emulator CONNECT sequence on the real device did not switch the LCD to OK. The real device answered the `04 00 00 80 00 DE` step with `07 80 C0 60 20 5D` in the captured run and remained at `CONNECT NOT ACT`, which points to a missing gate/session precondition in the emulator. +- RX probe finding: with calibrated FRT timing, the `--preset connect-lcd` sequence reaches the command-`0x04` handler but leaves the emulated LCD at ` CONNECT:NOT ACT` and falls back to heartbeat output; the earlier `CONNECT: OK`/`02 00 02 00 00 5A` result is now treated as a legacy step-timer artifact. +- Bench follow-up: replaying the emulator CONNECT sequence on the real device did not switch the LCD to OK. The real device answered the `04 00 00 80 00 DE` step with `07 80 C0 60 20 5D` in the captured run and remained at `CONNECT NOT ACT`, so the next mismatch to chase is the missing visible `07 80 C0 60 20 5D` response/session context rather than the LCD OK branch. - 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: @@ -209,15 +210,16 @@ python h8536_emulator_rx_probe.py --help - `--trace`: print executed instructions. - `--stop-on-heartbeat`: stop only if `00 00 00 00 80 DA` is emitted through SCI1 TDR. - `--interval-steps N`: tune the scaffolded interval timer cadence. -- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: tune rough FRT compare-interrupt cadence. +- `--clock-hz N`: set the CPU/phi clock used for calibrated FRT1/FRT2 compare timing; the default is 10 MHz. +- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: optional legacy overrides for forcing rough FRT compare cadence in targeted tests. - `--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. - `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 the emulator still emits `02 00 02 00 00 5A` instead of the bench-observed `07 80 C0 60 20 5D`. -- 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, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`. +- `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. +- 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 @@ -248,7 +250,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, runner, probe, CLI, and peripheral scaffolding. +- `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/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 89ff598..051b9e0 100644 --- a/h8536/emulator/__init__.py +++ b/h8536/emulator/__init__.py @@ -4,9 +4,18 @@ from .cli import build_arg_parser, discover_rom_path, load_rom, main from .constants import ( HEARTBEAT_FRAME, FRT_TCR_OCIEA, + FRT_TCSR_CCLRA, FRT_TCSR_OCFA, + FRT1_FRC_H, + FRT1_FRC_L, + FRT1_OCRA_H, + FRT1_OCRA_L, FRT1_TCR, FRT1_TCSR, + FRT2_FRC_H, + FRT2_FRC_L, + FRT2_OCRA_H, + FRT2_OCRA_L, FRT2_TCR, FRT2_TCSR, IPRA, @@ -55,9 +64,18 @@ __all__ = [ "EmulatorError", "FRT1_TCR", "FRT1_TCSR", + "FRT1_FRC_H", + "FRT1_FRC_L", + "FRT1_OCRA_H", + "FRT1_OCRA_L", "FRT2_TCR", "FRT2_TCSR", + "FRT2_FRC_H", + "FRT2_FRC_L", + "FRT2_OCRA_H", + "FRT2_OCRA_L", "FRT_TCR_OCIEA", + "FRT_TCSR_CCLRA", "FRT_TCSR_OCFA", "HEARTBEAT_FRAME", "H8536Emulator", diff --git a/h8536/emulator/bench_replay.py b/h8536/emulator/bench_replay.py index 2a3818d..66ad19d 100644 --- a/h8536/emulator/bench_replay.py +++ b/h8536/emulator/bench_replay.py @@ -166,11 +166,11 @@ class BenchReplayResult: class ReplayConfig: boot_steps: int = 250_000 per_byte_steps: int = 5_000 - steps_per_second: int = 65_000 post_log_steps: int = 50_000 interval_steps: int = 512 - frt1_ocia_steps: int = 512 - frt2_ocia_steps: int = 512 + frt1_ocia_steps: int | None = None + frt2_ocia_steps: int | None = None + clock_hz: int = 10_000_000 p9_fast_path: bool = True p9_fast_input: int = 0xFF @@ -232,6 +232,7 @@ def run_bench_replay(log_path: Path, *, rom_path: Path | None = None, config: Re 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, ) @@ -243,6 +244,7 @@ def run_bench_replay(log_path: Path, *, rom_path: Path | None = None, config: Re 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} " f"lcd_display={emulator.memory.lcd.display_text(lines=4)!r}" ) @@ -251,7 +253,7 @@ def run_bench_replay(log_path: Path, *, rom_path: Path | None = None, config: Re for host in bench_log.tx_frames: delta_ms = 0 if previous_tx_ms is None else max(0, host.timestamp_ms - previous_tx_ms) tx_frame_start_before_delay = len(emulator.sci1.tx_frames) - steps_before = _run_steps_for_ms(emulator, delta_ms, config.steps_per_second, context) + 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) @@ -335,9 +337,9 @@ def build_arg_parser() -> argparse.ArgumentParser: 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("--steps-per-second", type=int, default=ReplayConfig.steps_per_second) 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("--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") @@ -355,11 +357,11 @@ def main(argv: list[str] | None = None) -> int: config=ReplayConfig( boot_steps=args.boot_steps, per_byte_steps=args.per_byte_steps, - steps_per_second=args.steps_per_second, post_log_steps=args.post_log_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, p9_fast_path=not args.no_p9_fast_path, p9_fast_input=args.p9_fast_input, ), @@ -383,9 +385,19 @@ def _inject_host_frame(emulator: H8536Emulator, frame: bytes, per_byte_steps: in return steps_total -def _run_steps_for_ms(emulator: H8536Emulator, delta_ms: int, steps_per_second: int, context: RunContext) -> int: - steps = int((max(0, delta_ms) * max(1, steps_per_second)) / 1000) - return _run_steps(emulator, steps, context) +def _run_cycles_for_ms(emulator: H8536Emulator, delta_ms: int, clock_hz: int, context: RunContext) -> int: + target_delta_cycles = int((max(0, delta_ms) * max(1, clock_hz)) / 1000) + target_cycles = emulator.cpu.cycles + target_delta_cycles + completed = 0 + while emulator.cpu.cycles < target_cycles: + context.record_pc(emulator.cpu.pc) + try: + emulator.step() + except UnsupportedInstruction as exc: + context.unsupported = str(exc) + break + completed += 1 + return completed def _run_steps(emulator: H8536Emulator, steps: int, context: RunContext) -> int: diff --git a/h8536/emulator/cli.py b/h8536/emulator/cli.py index e52479a..60a3353 100644 --- a/h8536/emulator/cli.py +++ b/h8536/emulator/cli.py @@ -39,8 +39,9 @@ def build_arg_parser() -> argparse.ArgumentParser: 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") parser.add_argument("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt") - parser.add_argument("--frt1-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT1 OCIA interrupt") - parser.add_argument("--frt2-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT2 OCIA interrupt") + parser.add_argument("--clock-hz", type=parse_int, default=10_000_000, help="CPU/phi clock in Hz for calibrated FRT timing") + parser.add_argument("--frt1-ocia-steps", type=int, default=None, help="legacy step-period override for FRT1 OCIA") + parser.add_argument("--frt2-ocia-steps", type=int, default=None, help="legacy step-period override for FRT2 OCIA") parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration") parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine") return parser @@ -59,6 +60,7 @@ def main(argv: list[str] | None = None) -> int: interval_steps=args.interval_steps, frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, + clock_hz=args.clock_hz, p9_fast_path_enabled=args.p9_fast_path, p9_fast_default_input_byte=args.p9_fast_input, ) diff --git a/h8536/emulator/constants.py b/h8536/emulator/constants.py index 8411ae5..3bb0c70 100644 --- a/h8536/emulator/constants.py +++ b/h8536/emulator/constants.py @@ -18,8 +18,16 @@ WDT_TCSR_R = 0xFEEC FRT1_TCR = 0xFE90 FRT1_TCSR = 0xFE91 +FRT1_FRC_H = 0xFE92 +FRT1_FRC_L = 0xFE93 +FRT1_OCRA_H = 0xFE94 +FRT1_OCRA_L = 0xFE95 FRT2_TCR = 0xFEA0 FRT2_TCSR = 0xFEA1 +FRT2_FRC_H = 0xFEA2 +FRT2_FRC_L = 0xFEA3 +FRT2_OCRA_H = 0xFEA4 +FRT2_OCRA_L = 0xFEA5 SCI_SCR_TIE = 0x80 SCI_SCR_RIE = 0x40 @@ -32,6 +40,7 @@ SCI_SSR_FER = 0x10 SCI_SSR_PER = 0x08 FRT_TCR_OCIEA = 0x20 FRT_TCSR_OCFA = 0x20 +FRT_TCSR_CCLRA = 0x01 ON_CHIP_RAM_START = 0xF680 ON_CHIP_RAM_END = 0xFE7F diff --git a/h8536/emulator/memory.py b/h8536/emulator/memory.py index 5b6e5d2..72802ee 100644 --- a/h8536/emulator/memory.py +++ b/h8536/emulator/memory.py @@ -125,6 +125,21 @@ class MemoryMap: self._set_register(SCI1_RDR, self.sci1.read(SCI1_RDR)) self._set_register(SCI1_SSR, self.sci1.read(SCI1_SSR)) + def register8(self, address: int) -> int: + return self.registers[(address & 0xFFFF) - REGISTER_FIELD_START] + + def register16(self, address: int) -> int: + address &= 0xFFFF + return (self.register8(address) << 8) | self.register8((address + 1) & 0xFFFF) + + def set_register8(self, address: int, value: int) -> None: + self._set_register(address & 0xFFFF, value) + + def set_register16(self, address: int, value: int) -> None: + address &= 0xFFFF + self._set_register(address, (value >> 8) & 0xFF) + self._set_register((address + 1) & 0xFFFF, value & 0xFF) + def _set_register(self, address: int, value: int) -> None: self.registers[address - REGISTER_FIELD_START] = value & 0xFF diff --git a/h8536/emulator/probe.py b/h8536/emulator/probe.py index 796cf2e..7c74637 100644 --- a/h8536/emulator/probe.py +++ b/h8536/emulator/probe.py @@ -900,8 +900,9 @@ def run_probe( interval_steps: int, stop_on_tx: bool, p9_log_limit: int, - frt1_ocia_steps: int = 1024, - frt2_ocia_steps: int = 1024, + frt1_ocia_steps: int | None = None, + frt2_ocia_steps: int | None = None, + clock_hz: int = 10_000_000, p9_fast_path: bool = False, p9_fast_input: int = 0xFF, sci_log_limit: int = 32, @@ -929,6 +930,7 @@ def run_probe( interval_steps=interval_steps, frt1_ocia_steps=frt1_ocia_steps, frt2_ocia_steps=frt2_ocia_steps, + clock_hz=clock_hz, p9_fast_path_enabled=p9_fast_path, p9_fast_default_input_byte=p9_fast_input, ) @@ -1150,8 +1152,9 @@ def build_arg_parser() -> argparse.ArgumentParser: parser.add_argument("--rom", type=Path, help="ROM image path; defaults to the repo ROM image") parser.add_argument("--max-steps", type=int, default=250_000) parser.add_argument("--interval-steps", type=int, default=512) - parser.add_argument("--frt1-ocia-steps", type=int, default=1024) - parser.add_argument("--frt2-ocia-steps", type=int, default=1024) + parser.add_argument("--clock-hz", type=parse_int, default=10_000_000) + parser.add_argument("--frt1-ocia-steps", type=int, default=None, help="legacy step-period override for FRT1 OCIA") + parser.add_argument("--frt2-ocia-steps", type=int, default=None, help="legacy step-period override for FRT2 OCIA") parser.add_argument("--stop-on-tx", action="store_true", help="stop when SCI1 TDR emits the first byte") parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration") parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF) @@ -1236,6 +1239,7 @@ def main(argv: list[str] | None = None) -> int: interval_steps=args.interval_steps, frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, + clock_hz=args.clock_hz, stop_on_tx=args.stop_on_tx, p9_log_limit=args.p9_log_limit, p9_fast_path=args.p9_fast_path, diff --git a/h8536/emulator/runner.py b/h8536/emulator/runner.py index 2d4e7ce..76feb57 100644 --- a/h8536/emulator/runner.py +++ b/h8536/emulator/runner.py @@ -9,8 +9,12 @@ from ..vectors import read_vectors_min from .constants import ( FRT_TCR_OCIEA, FRT_TCSR_OCFA, + FRT1_FRC_H, + FRT1_OCRA_H, FRT1_TCR, FRT1_TCSR, + FRT2_FRC_H, + FRT2_OCRA_H, FRT2_TCR, FRT2_TCSR, IPRA, @@ -36,6 +40,7 @@ from .errors import EmulatorError, UnsupportedInstruction from .fast_paths import P9FastPath, P9FastPathConfig from .memory import MemoryMap from .sci import SCI1 +from .timers import FrtOciaScheduler, FrtRegisters @dataclass @@ -47,6 +52,9 @@ class RunReport: tx_bytes: bytes tx_frames: list[bytes] heartbeat_seen: bool + clock_hz: int + frt1_ocia_period_ms: float | None = None + frt2_ocia_period_ms: float | None = None unsupported: str | None = None trace: list[str] = field(default_factory=list) @@ -56,6 +64,9 @@ class RunReport: f"cycles={self.cycles}", f"pc={h16(self.pc)}", f"stopped={self.stopped_reason}", + f"clock_hz={self.clock_hz}", + "frt1_ocia_period_ms=" + _format_optional_ms(self.frt1_ocia_period_ms), + "frt2_ocia_period_ms=" + _format_optional_ms(self.frt2_ocia_period_ms), "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}", @@ -74,8 +85,9 @@ class H8536Emulator: rom_bytes: bytes, *, interval_steps: int = 2048, - frt1_ocia_steps: int = 1024, - frt2_ocia_steps: int = 1024, + frt1_ocia_steps: int | None = None, + frt2_ocia_steps: int | None = None, + clock_hz: int = 10_000_000, p9_fast_path: P9FastPath | None = None, p9_fast_path_enabled: bool = False, p9_fast_default_input_byte: int = 0xFF, @@ -90,8 +102,11 @@ class H8536Emulator: self.cpu = CPUState() self.vectors = read_vectors_min(self.memory.rom) self.interval_steps = max(1, interval_steps) - self.frt1_ocia_steps = max(1, frt1_ocia_steps) - self.frt2_ocia_steps = max(1, frt2_ocia_steps) + self.frt1_ocia_steps = max(1, frt1_ocia_steps) if frt1_ocia_steps is not None else None + self.frt2_ocia_steps = max(1, frt2_ocia_steps) if frt2_ocia_steps is not None else None + self.clock_hz = max(1, clock_hz) + self.frt1_ocia = FrtOciaScheduler(FrtRegisters("FRT1", FRT1_TCR, FRT1_TCSR, FRT1_FRC_H, FRT1_OCRA_H)) + self.frt2_ocia = FrtOciaScheduler(FrtRegisters("FRT2", FRT2_TCR, FRT2_TCSR, FRT2_FRC_H, FRT2_OCRA_H)) self._interval_counter = 0 self._frt1_ocia_counter = 0 self._frt2_ocia_counter = 0 @@ -110,8 +125,9 @@ class H8536Emulator: def step(self) -> str: pc = self.cpu.pc + cycles_before = self.cpu.cycles if self.p9_fast_path.try_handle(self): - self._tick_peripherals() + self._tick_peripherals(self.cpu.cycles - cycles_before) return f"{h16(pc)}: {'':<17} P9 fast-path" decoder = H8536Decoder(self.memory.rom, br=self.cpu.br) @@ -166,8 +182,9 @@ class H8536Emulator: self.cpu.pc = next_pc self.cpu.steps += 1 - self.cpu.cycles += self._rough_cycles(raw) - self._tick_peripherals() + cycle_delta = self._rough_cycles(raw) + self.cpu.cycles += cycle_delta + self._tick_peripherals(cycle_delta) 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: @@ -194,6 +211,9 @@ class H8536Emulator: tx_bytes=bytes(self.sci1.tx_bytes), tx_frames=list(self.sci1.tx_frames), heartbeat_seen=self.sci1.saw_heartbeat(), + clock_hz=self.clock_hz, + frt1_ocia_period_ms=self.frt1_ocia.period_ms(self.memory, self.clock_hz), + frt2_ocia_period_ms=self.frt2_ocia.period_ms(self.memory, self.clock_hz), unsupported=unsupported, trace=trace_lines, ) @@ -405,11 +425,17 @@ class H8536Emulator: return (pc + 3 + s8(raw[2])) & 0xFFFF return next_pc - def _tick_peripherals(self) -> None: + def _tick_peripherals(self, cycle_delta: int) -> None: self.sci1.tick() self._interval_counter += 1 - self._frt1_ocia_counter += 1 - self._frt2_ocia_counter += 1 + if self.frt1_ocia_steps is None: + self.frt1_ocia.tick(self.memory, cycle_delta) + else: + self._frt1_ocia_counter += 1 + if self.frt2_ocia_steps is None: + self.frt2_ocia.tick(self.memory, cycle_delta) + else: + self._frt2_ocia_counter += 1 self._service_pending_interrupt() def _service_pending_interrupt(self) -> None: @@ -450,11 +476,13 @@ class H8536Emulator: if source == "interval_timer": self._interval_counter = 0 elif source == "frt1_ocia": - self._frt1_ocia_counter = 0 - self.memory.write8(FRT1_TCSR, self.memory.read8(FRT1_TCSR) | FRT_TCSR_OCFA) + if self.frt1_ocia_steps is not None: + self._frt1_ocia_counter = 0 + self.memory.write8(FRT1_TCSR, self.memory.read8(FRT1_TCSR) | FRT_TCSR_OCFA) elif source == "frt2_ocia": - self._frt2_ocia_counter = 0 - self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA) + if self.frt2_ocia_steps is not None: + self._frt2_ocia_counter = 0 + self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA) self._enter_interrupt(target) def _enter_interrupt(self, target: int) -> None: @@ -484,6 +512,8 @@ class H8536Emulator: return (self.memory.read8(IPRA) >> 4) & 0x07 def _frt1_ocia_pending(self) -> bool: + if self.frt1_ocia_steps is None: + return self.frt1_ocia.pending(self.memory) if self._frt1_ocia_counter < self.frt1_ocia_steps: return False return bool(self.memory.read8(FRT1_TCR) & FRT_TCR_OCIEA) @@ -492,6 +522,8 @@ class H8536Emulator: return (self.memory.read8(IPRC) >> 4) & 0x07 def _frt2_ocia_pending(self) -> bool: + if self.frt2_ocia_steps is None: + return self.frt2_ocia.pending(self.memory) if self._frt2_ocia_counter < self.frt2_ocia_steps: return False return bool(self.memory.read8(FRT2_TCR) & FRT_TCR_OCIEA) @@ -679,3 +711,7 @@ class H8536Emulator: if cond == 0xE: return not self.cpu.z and self.cpu.n == self.cpu.v return self.cpu.z or self.cpu.n != self.cpu.v + + +def _format_optional_ms(value: float | None) -> str: + return "unavailable" if value is None else f"{value:.3f}" diff --git a/h8536/emulator/rx_probe.py b/h8536/emulator/rx_probe.py index 124dd67..fdf5f6c 100644 --- a/h8536/emulator/rx_probe.py +++ b/h8536/emulator/rx_probe.py @@ -202,8 +202,9 @@ def run_rx_probe( 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, + frt1_ocia_steps: int | None = None, + frt2_ocia_steps: int | None = None, + clock_hz: int = 10_000_000, p9_fast_path: bool = True, p9_fast_input: int = 0xFF, stop_after_tx_frame: bool = True, @@ -214,6 +215,7 @@ def run_rx_probe( interval_steps=interval_steps, frt1_ocia_steps=frt1_ocia_steps, frt2_ocia_steps=frt2_ocia_steps, + clock_hz=clock_hz, p9_fast_path_enabled=p9_fast_path, p9_fast_default_input_byte=p9_fast_input, ) @@ -224,6 +226,7 @@ def run_rx_probe( 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"clock_hz={emulator.clock_hz} " f"lcd_display={emulator.memory.lcd.display_text(lines=4)!r}" ) @@ -250,8 +253,9 @@ def build_arg_parser() -> argparse.ArgumentParser: 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("--clock-hz", type=parse_int, default=10_000_000, help="CPU/phi clock in Hz for calibrated FRT timing") + parser.add_argument("--frt1-ocia-steps", type=int, default=None, help="legacy step-period override for FRT1 OCIA") + parser.add_argument("--frt2-ocia-steps", type=int, default=None, help="legacy step-period override for FRT2 OCIA") 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 @@ -274,6 +278,7 @@ def main(argv: list[str] | None = None) -> int: interval_steps=args.interval_steps, frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, + clock_hz=args.clock_hz, p9_fast_path=not args.no_p9_fast_path, p9_fast_input=args.p9_fast_input, stop_after_tx_frame=not args.keep_listening, diff --git a/h8536/emulator/timers.py b/h8536/emulator/timers.py new file mode 100644 index 0000000..e6b3763 --- /dev/null +++ b/h8536/emulator/timers.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .constants import FRT_TCR_OCIEA, FRT_TCSR_CCLRA, FRT_TCSR_OCFA +from .memory import MemoryMap + + +FRT_INTERNAL_PRESCALERS = { + 0b00: 4, + 0b01: 8, + 0b10: 32, +} + + +@dataclass(frozen=True) +class FrtRegisters: + name: str + tcr: int + tcsr: int + frc_h: int + ocra_h: int + + +@dataclass +class FrtOciaScheduler: + registers: FrtRegisters + cycle_accumulator: int = 0 + last_config: tuple[int, int] | None = None + compare_matches: int = 0 + + def tick(self, memory: MemoryMap, cycle_delta: int) -> None: + cycle_delta = max(0, cycle_delta) + tcr = memory.register8(self.registers.tcr) + prescaler = frt_internal_prescaler(tcr) + if prescaler is None: + self.cycle_accumulator = 0 + self.last_config = None + return + + config = self._config(memory) + if config != self.last_config: + self.cycle_accumulator = 0 + self.last_config = config + + self.cycle_accumulator += cycle_delta + ticks = self.cycle_accumulator // prescaler + self.cycle_accumulator %= prescaler + if ticks <= 0: + return + + frc = memory.register16(self.registers.frc_h) + ocra = memory.register16(self.registers.ocra_h) + tcsr = memory.register8(self.registers.tcsr) + clear_on_a = bool(tcsr & FRT_TCSR_CCLRA) + matched = False + + for _ in range(ticks): + frc = (frc + 1) & 0xFFFF + if frc == ocra: + matched = True + self.compare_matches += 1 + if clear_on_a: + frc = 0 + + memory.set_register16(self.registers.frc_h, frc) + if matched: + memory.set_register8(self.registers.tcsr, tcsr | FRT_TCSR_OCFA) + + def period_cycles(self, memory: MemoryMap) -> int | None: + tcr = memory.register8(self.registers.tcr) + prescaler = frt_internal_prescaler(tcr) + if prescaler is None: + return None + ocra = memory.register16(self.registers.ocra_h) + return frt_ocia_period_cycles(tcr, ocra) + + def pending(self, memory: MemoryMap) -> bool: + return bool(memory.register8(self.registers.tcr) & FRT_TCR_OCIEA and memory.register8(self.registers.tcsr) & FRT_TCSR_OCFA) + + def period_ms(self, memory: MemoryMap, clock_hz: int) -> float | None: + period = self.period_cycles(memory) + if period is None or clock_hz <= 0: + return None + return 1000.0 * period / clock_hz + + def _config(self, memory: MemoryMap) -> tuple[int, int]: + return memory.register8(self.registers.tcr) & 0x03, memory.register16(self.registers.ocra_h) + + +def frt_internal_prescaler(tcr: int) -> int | None: + return FRT_INTERNAL_PRESCALERS.get(tcr & 0x03) + + +def frt_ocia_period_cycles(tcr: int, ocra: int) -> int | None: + prescaler = frt_internal_prescaler(tcr) + if prescaler is None: + return None + compare_ticks = (ocra & 0xFFFF) or 0x10000 + return compare_ticks * prescaler + + +__all__ = [ + "FRT_INTERNAL_PRESCALERS", + "FrtOciaScheduler", + "FrtRegisters", + "frt_internal_prescaler", + "frt_ocia_period_cycles", +] diff --git a/tests/test_emulator_timers.py b/tests/test_emulator_timers.py index 4de71e4..5c5db17 100644 --- a/tests/test_emulator_timers.py +++ b/tests/test_emulator_timers.py @@ -3,7 +3,10 @@ import unittest from h8536.emulator import H8536Emulator, ON_CHIP_RAM_START from h8536.emulator.constants import ( FRT_TCR_OCIEA, + FRT_TCSR_CCLRA, FRT_TCSR_OCFA, + FRT2_FRC_H, + FRT2_OCRA_H, FRT1_TCR, FRT1_TCSR, FRT2_TCR, @@ -12,6 +15,7 @@ from h8536.emulator.constants import ( VECTOR_FRT1_OCIA, VECTOR_FRT2_OCIA, ) +from h8536.emulator.timers import frt_internal_prescaler, frt_ocia_period_cycles def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1040) -> bytearray: @@ -26,6 +30,36 @@ def write_mov_b_abs_imm(rom: bytearray, address: int, target: int, value: int) - class Frt2OciaTimerTest(unittest.TestCase): + def test_manual_prescaler_bits_are_used_for_frt_timing(self): + self.assertEqual(frt_internal_prescaler(0x00), 4) + self.assertEqual(frt_internal_prescaler(0x01), 8) + self.assertEqual(frt_internal_prescaler(0x02), 32) + self.assertIsNone(frt_internal_prescaler(0x03)) + + def test_rom_frt2_compare_period_is_100_ms_at_10_mhz(self): + emulator = H8536Emulator(bytes(rom_with_reset())) + emulator.memory.set_register8(FRT2_TCR, 0x02) + emulator.memory.set_register16(FRT2_OCRA_H, 0x7A12) + + self.assertEqual(frt_ocia_period_cycles(0x02, 0x7A12), 1_000_000) + self.assertEqual(emulator.frt2_ocia.period_ms(emulator.memory, 10_000_000), 100.0) + + def test_calibrated_scheduler_advances_frc_and_sets_ocfa(self): + emulator = H8536Emulator(bytes(rom_with_reset())) + emulator.memory.set_register8(FRT2_TCR, 0x02) + emulator.memory.set_register8(FRT2_TCSR, FRT_TCSR_CCLRA) + emulator.memory.set_register16(FRT2_FRC_H, 0x0000) + emulator.memory.set_register16(FRT2_OCRA_H, 0x0003) + + emulator.frt2_ocia.tick(emulator.memory, 64) + self.assertEqual(emulator.memory.register16(FRT2_FRC_H), 0x0002) + self.assertEqual(emulator.memory.register8(FRT2_TCSR) & FRT_TCSR_OCFA, 0) + + emulator.frt2_ocia.tick(emulator.memory, 32) + self.assertEqual(emulator.memory.register16(FRT2_FRC_H), 0x0000) + self.assertEqual(emulator.memory.register8(FRT2_TCSR) & FRT_TCSR_OCFA, FRT_TCSR_OCFA) + self.assertEqual(emulator.frt2_ocia.compare_matches, 1) + def test_frt1_ocia_vector_can_fire_and_decrement_ram(self): rom = rom_with_reset() rom[VECTOR_FRT1_OCIA : VECTOR_FRT1_OCIA + 2] = (0x1020).to_bytes(2, "big") @@ -103,6 +137,33 @@ class Frt2OciaTimerTest(unittest.TestCase): self.assertFalse(emulator.memory.read8(FRT2_TCSR) & FRT_TCSR_OCFA) self.assertEqual(emulator.cpu.pc, isr + 4) + def test_calibrated_frt2_ocia_vector_can_fire_without_step_override(self): + rom = rom_with_reset() + rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big") + + pc = 0x1000 + rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7 + pc += 3 + pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x06) + pc = write_mov_b_abs_imm(rom, pc, FRT2_TCR, FRT_TCR_OCIEA) + rom[pc : pc + 2] = b"\x20\xFE" # BRA self + + isr = 0x1020 + rom[isr : isr + 4] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C]) + isr += 4 + rom[isr : isr + 4] = bytes([0x15, (FRT2_TCSR >> 8) & 0xFF, FRT2_TCSR & 0xFF, 0xD5]) + rom[isr + 4] = 0x0A # RTE + + emulator = H8536Emulator(bytes(rom)) + emulator.memory.write8(ON_CHIP_RAM_START, 3) + emulator.memory.set_register8(FRT2_TCSR, FRT_TCSR_CCLRA) + emulator.memory.set_register16(FRT2_FRC_H, 0x0000) + emulator.memory.set_register16(FRT2_OCRA_H, 0x000A) + emulator.run(max_steps=9) + + self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 2) + self.assertFalse(emulator.memory.read8(FRT2_TCSR) & FRT_TCSR_OCFA) + def test_frt2_ocia_does_not_fire_when_ociea_disabled(self): rom = rom_with_reset() rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")