From 1fabf6587d2c9102264866d9b2d9072fa02b69cc Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 18:55:50 +1000 Subject: [PATCH] Emualtor improements --- README.md | 7 +- h8536/emulator/__init__.py | 6 ++ h8536/emulator/cli.py | 2 + h8536/emulator/constants.py | 3 + h8536/emulator/probe.py | 133 +++++++++++++++++++++++++++++- h8536/emulator/runner.py | 22 +++++ h8536/emulator/sci.py | 27 ++++-- tests/test_emulator_probe_sci.py | 104 +++++++++++++++++++++++ tests/test_emulator_sci_timing.py | 71 ++++++++++++++++ tests/test_emulator_timers.py | 53 ++++++++++++ 10 files changed, 413 insertions(+), 15 deletions(-) create mode 100644 tests/test_emulator_probe_sci.py create mode 100644 tests/test_emulator_sci_timing.py diff --git a/README.md b/README.md index d99a88e..49b2967 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ 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/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, and captured P9 byte candidates while running the real ROM. +- 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. +- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM. Current serial observations: @@ -189,8 +189,9 @@ python h8536_emulator_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. - `--p9-fast-path`: shortcut known P9 transfer routines for exploration. -- 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 FRT2 OCIA, but does not yet reach the SCI1 heartbeat. The next emulator work is validating the P9 fast-path semantics against real captures/board behavior and tightening SCI RX/TX interrupt cadence. +- 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. The probe now reaches the SCI1 transmit path, but still needs more timing/peripheral fidelity before it emits the full observed heartbeat frame. ## Code Layout diff --git a/h8536/emulator/__init__.py b/h8536/emulator/__init__.py index 8b3585e..9ea2d3c 100644 --- a/h8536/emulator/__init__.py +++ b/h8536/emulator/__init__.py @@ -5,6 +5,8 @@ from .constants import ( HEARTBEAT_FRAME, FRT_TCR_OCIEA, FRT_TCSR_OCFA, + FRT1_TCR, + FRT1_TCSR, FRT2_TCR, FRT2_TCSR, IPRA, @@ -32,6 +34,7 @@ from .constants import ( SCI1_SMR, SCI1_SSR, SCI1_TDR, + VECTOR_FRT1_OCIA, VECTOR_INTERVAL_TIMER, VECTOR_FRT2_OCIA, VECTOR_SCI1_TXI, @@ -47,6 +50,8 @@ from .sci import SCI1, SciTxEvent __all__ = [ "CPUState", "EmulatorError", + "FRT1_TCR", + "FRT1_TCSR", "FRT2_TCR", "FRT2_TCSR", "FRT_TCR_OCIEA", @@ -87,6 +92,7 @@ __all__ = [ "SCI_SSR_TDRE", "SciTxEvent", "UnsupportedInstruction", + "VECTOR_FRT1_OCIA", "VECTOR_INTERVAL_TIMER", "VECTOR_FRT2_OCIA", "VECTOR_SCI1_TXI", diff --git a/h8536/emulator/cli.py b/h8536/emulator/cli.py index 358abc8..e52479a 100644 --- a/h8536/emulator/cli.py +++ b/h8536/emulator/cli.py @@ -39,6 +39,7 @@ 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("--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") @@ -56,6 +57,7 @@ def main(argv: list[str] | None = None) -> int: emulator = H8536Emulator( rom_bytes, interval_steps=args.interval_steps, + frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, 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 0753596..a06d32e 100644 --- a/h8536/emulator/constants.py +++ b/h8536/emulator/constants.py @@ -16,6 +16,8 @@ IPRC = 0xFF02 IPRE = 0xFF04 WDT_TCSR_R = 0xFEEC +FRT1_TCR = 0xFE90 +FRT1_TCSR = 0xFE91 FRT2_TCR = 0xFEA0 FRT2_TCSR = 0xFEA1 @@ -39,5 +41,6 @@ RAMCR = 0xFF11 HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) VECTOR_INTERVAL_TIMER = 0x0042 +VECTOR_FRT1_OCIA = 0x0062 VECTOR_FRT2_OCIA = 0x006A VECTOR_SCI1_TXI = 0x0084 diff --git a/h8536/emulator/probe.py b/h8536/emulator/probe.py index f38410f..20cf76f 100644 --- a/h8536/emulator/probe.py +++ b/h8536/emulator/probe.py @@ -7,12 +7,93 @@ from pathlib import Path from ..formatting import h16, parse_int from .cli import load_rom -from .constants import P9DDR, P9DR, SCI1_TDR +from .constants import ( + P9DDR, + P9DR, + SCI1_BRR, + SCI1_RDR, + SCI1_SCR, + SCI1_SMR, + SCI1_SSR, + SCI1_TDR, + SCI_SCR_TE, + SCI_SCR_TIE, + SCI_SSR_TDRE, + VECTOR_SCI1_TXI, +) from .errors import UnsupportedInstruction from .runner import H8536Emulator DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059) +SCI1_PROBE_REGISTERS = { + SCI1_SCR: "SCR", + SCI1_TDR: "TDR", + SCI1_SSR: "SSR", + SCI1_RDR: "RDR", +} + + +def _format_bytes(data: bytes, *, limit: int = 96) -> str: + if len(data) <= limit: + return data.hex(" ").upper() + head = data[: limit // 2].hex(" ").upper() + tail = data[-(limit // 2) :].hex(" ").upper() + return f"{head} ... {tail} ({len(data)} bytes)" + + +def _format_six_byte_frames(data: bytes, *, limit: int = 8) -> str: + frames = [data[index : index + 6] for index in range(0, len(data) - 5, 6)] + if not frames: + return "" + recent = frames[-limit:] + return " | ".join(frame.hex(" ").upper() for frame in recent) + + +@dataclass(frozen=True) +class SCI1Snapshot: + smr: int + brr: int + scr: int + ssr: int + tdr: int + rdr: int + tx_ready_delay: int | None = None + + def line(self) -> str: + fields = [ + f"SMR={self.smr:02X}", + f"BRR={self.brr:02X}", + f"SCR={self.scr:02X}", + f"SSR={self.ssr:02X}", + f"TDR={self.tdr:02X}", + f"RDR={self.rdr:02X}", + ] + if self.tx_ready_delay is not None: + fields.append(f"tx_ready_delay={self.tx_ready_delay}") + return "sci1=" + " ".join(fields) + + +@dataclass(frozen=True) +class SCI1TXISummary: + tie: bool + te: bool + tdre: bool + vector_target: int | None + priority: int + interrupt_mask: int + interrupt_depth: int + + def line(self) -> str: + pending = self.tie and self.tdre and self.vector_target is not None + serviceable = pending and self.interrupt_depth == 0 and self.priority > self.interrupt_mask + vector = h16(self.vector_target) if self.vector_target is not None else "none" + return ( + "sci1_txi=" + f"TIE={int(self.tie)} TE={int(self.te)} TDRE={int(self.tdre)} " + f"vector={vector} priority={self.priority} mask={self.interrupt_mask} " + f"depth={self.interrupt_depth} pending={int(pending)} serviceable={int(serviceable)}" + ) @dataclass(frozen=True) @@ -49,6 +130,8 @@ class ProbeReport: p9_fast_events: int = 0 p9_accesses: list[str] = field(default_factory=list) sci_accesses: list[str] = field(default_factory=list) + sci1: SCI1Snapshot | None = None + sci1_txi: SCI1TXISummary | None = None watch_snapshots: list[WatchSnapshot] = field(default_factory=list) unsupported: str | None = None @@ -57,12 +140,17 @@ class ProbeReport: f"steps={self.steps}", f"pc={h16(self.pc)}", f"stopped={self.stopped_reason}", - "tx_bytes=" + self.tx_bytes.hex(" ").upper(), + "tx_bytes=" + _format_bytes(self.tx_bytes), + "tx_6byte_recent=" + _format_six_byte_frames(self.tx_bytes), "p9_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_bytes[-32:]), "p9_fast_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_fast_bytes[-32:]), f"p9_fast_events={self.p9_fast_events}", "hot_pcs=" + ", ".join(f"{h16(pc)}:{count}" for pc, count in self.hot_pcs.most_common(hot_limit)), ] + if self.sci1: + lines.append(self.sci1.line()) + if self.sci1_txi: + lines.append(self.sci1_txi.line()) if self.unsupported: lines.append(f"unsupported={self.unsupported}") if self.p9_accesses: @@ -77,6 +165,31 @@ class ProbeReport: return lines +def _sci1_snapshot(emulator: H8536Emulator) -> SCI1Snapshot: + sci1 = emulator.sci1 + return SCI1Snapshot( + smr=sci1.smr, + brr=sci1.brr, + scr=sci1.scr, + ssr=sci1.ssr, + tdr=sci1.tdr, + rdr=sci1.rdr, + tx_ready_delay=getattr(sci1, "tx_ready_delay", None), + ) + + +def _sci1_txi_summary(emulator: H8536Emulator) -> SCI1TXISummary: + return SCI1TXISummary( + tie=bool(emulator.sci1.scr & SCI_SCR_TIE), + te=bool(emulator.sci1.scr & SCI_SCR_TE), + tdre=bool(emulator.sci1.ssr & SCI_SSR_TDRE), + vector_target=emulator._vector_target(VECTOR_SCI1_TXI), + priority=emulator._sci1_priority(), + interrupt_mask=emulator._interrupt_mask(), + interrupt_depth=emulator.cpu.interrupt_depth, + ) + + def parse_watch_pc(text: str) -> int: try: value = parse_int(text) @@ -132,9 +245,11 @@ def run_probe( interval_steps: int, stop_on_tx: bool, p9_log_limit: int, + frt1_ocia_steps: int = 1024, frt2_ocia_steps: int = 1024, p9_fast_path: bool = False, p9_fast_input: int = 0xFF, + sci_log_limit: int = 32, watch_pcs: list[int] | tuple[int, ...] | None = None, watch_snapshot_limit: int = 32, watch_pc_limit: int = 8, @@ -143,6 +258,7 @@ def run_probe( 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, @@ -183,7 +299,12 @@ def run_probe( if len(p9_accesses) > p9_log_limit: del p9_accesses[: len(p9_accesses) - p9_log_limit] elif access.address == SCI1_TDR: - sci_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}") + sci_accesses.append(f"{h16(pc)} {access.kind} TDR={access.value:02X}") + elif access.address in SCI1_PROBE_REGISTERS: + name = SCI1_PROBE_REGISTERS[access.address] + sci_accesses.append(f"{h16(pc)} {access.kind} {name}={access.value:02X}") + if len(sci_accesses) > sci_log_limit: + del sci_accesses[: len(sci_accesses) - sci_log_limit] last_access_index = len(emulator.memory.access_log) if stop_on_tx and emulator.sci1.tx_bytes: @@ -204,6 +325,8 @@ def run_probe( p9_fast_events=len(emulator.p9_fast_path.events), p9_accesses=p9_accesses, sci_accesses=sci_accesses, + sci1=_sci1_snapshot(emulator), + sci1_txi=_sci1_txi_summary(emulator), watch_snapshots=snapshots, unsupported=unsupported, ) @@ -214,11 +337,13 @@ 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("--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) parser.add_argument("--p9-log-limit", type=int, default=80) + parser.add_argument("--sci-log-limit", type=int, default=32) parser.add_argument("--hot-limit", type=int, default=12) parser.add_argument( "--watch-pc", @@ -245,11 +370,13 @@ def main(argv: list[str] | None = None) -> int: rom_bytes, max_steps=args.max_steps, interval_steps=args.interval_steps, + frt1_ocia_steps=args.frt1_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps, stop_on_tx=args.stop_on_tx, p9_log_limit=args.p9_log_limit, p9_fast_path=args.p9_fast_path, p9_fast_input=args.p9_fast_input, + sci_log_limit=args.sci_log_limit, watch_pcs=tuple(dict.fromkeys((*DEFAULT_WATCH_PCS, *args.watch_pc))), watch_snapshot_limit=args.watch_snapshot_limit, watch_pc_limit=args.watch_pc_limit, diff --git a/h8536/emulator/runner.py b/h8536/emulator/runner.py index 598b9b8..61eca40 100644 --- a/h8536/emulator/runner.py +++ b/h8536/emulator/runner.py @@ -9,6 +9,8 @@ from ..vectors import read_vectors_min from .constants import ( FRT_TCR_OCIEA, FRT_TCSR_OCFA, + FRT1_TCR, + FRT1_TCSR, FRT2_TCR, FRT2_TCSR, IPRA, @@ -16,6 +18,7 @@ from .constants import ( IPRE, SCI_SCR_TIE, SCI_SSR_TDRE, + VECTOR_FRT1_OCIA, VECTOR_FRT2_OCIA, VECTOR_INTERVAL_TIMER, VECTOR_SCI1_TXI, @@ -63,6 +66,7 @@ class H8536Emulator: rom_bytes: bytes, *, interval_steps: int = 2048, + frt1_ocia_steps: int = 1024, frt2_ocia_steps: int = 1024, p9_fast_path: P9FastPath | None = None, p9_fast_path_enabled: bool = False, @@ -78,8 +82,10 @@ 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._interval_counter = 0 + self._frt1_ocia_counter = 0 self._frt2_ocia_counter = 0 self.reset() @@ -372,6 +378,7 @@ class H8536Emulator: def _tick_peripherals(self) -> None: self.sci1.tick() self._interval_counter += 1 + self._frt1_ocia_counter += 1 self._frt2_ocia_counter += 1 self._service_pending_interrupt() @@ -387,6 +394,10 @@ class H8536Emulator: target = self._vector_target(VECTOR_INTERVAL_TIMER) if target is not None: candidates.append((self._interval_priority(), target, "interval_timer")) + if self._frt1_ocia_pending(): + target = self._vector_target(VECTOR_FRT1_OCIA) + if target is not None: + candidates.append((self._frt1_priority(), target, "frt1_ocia")) if self._frt2_ocia_pending(): target = self._vector_target(VECTOR_FRT2_OCIA) if target is not None: @@ -399,6 +410,9 @@ class H8536Emulator: return 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) elif source == "frt2_ocia": self._frt2_ocia_counter = 0 self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA) @@ -430,6 +444,14 @@ class H8536Emulator: def _interval_priority(self) -> int: return (self.memory.read8(IPRA) >> 4) & 0x07 + def _frt1_ocia_pending(self) -> bool: + if self._frt1_ocia_counter < self.frt1_ocia_steps: + return False + return bool(self.memory.read8(FRT1_TCR) & FRT_TCR_OCIEA) + + def _frt1_priority(self) -> int: + return (self.memory.read8(IPRC) >> 4) & 0x07 + def _frt2_ocia_pending(self) -> bool: if self._frt2_ocia_counter < self.frt2_ocia_steps: return False diff --git a/h8536/emulator/sci.py b/h8536/emulator/sci.py index 4bb1504..e828feb 100644 --- a/h8536/emulator/sci.py +++ b/h8536/emulator/sci.py @@ -11,6 +11,9 @@ from .constants import ( SCI1_SSR, SCI1_TDR, SCI_SCR_TE, + SCI_SSR_FER, + SCI_SSR_ORER, + SCI_SSR_PER, SCI_SSR_RDRF, SCI_SSR_TDRE, ) @@ -34,6 +37,8 @@ class SCI1: - SCR bit 7 TIE, bit 6 RIE, bit 5 TE, bit 4 RE. - SSR bit 7 TDRE, bit 6 RDRF, bits 5..3 ORER/FER/PER. - Software normally writes TDR after TDRE=1, then clears SSR.TDRE. + - SSR status flags are R/(W)*: writing 0 clears a latched flag, while + writing 1 leaves the hardware-owned flag unchanged. """ smr: int = 0x00 @@ -47,6 +52,8 @@ class SCI1: tx_frames: list[bytes] = field(default_factory=list) _frame_buffer: bytearray = field(default_factory=bytearray) tx_ready_delay: int = 0 + tx_ready_ticks: int = 2 + _tx_ready_pending: bool = False def read(self, address: int) -> int: if address == SCI1_SMR: @@ -75,10 +82,7 @@ class SCI1: self.tdr = value self._write_tdr(value) elif address == SCI1_SSR: - # The real SSR is R/(W)*: writable zeroes clear latched flags after - # the required read sequence. This scaffold applies zero bits - # directly so ROM BCLR/BSET style accesses can be modeled. - self.ssr = value + self._write_ssr(value) elif address == SCI1_RDR: self.rdr = value else: @@ -92,10 +96,14 @@ class SCI1: if len(self._frame_buffer) == len(HEARTBEAT_FRAME): self.tx_frames.append(bytes(self._frame_buffer)) self._frame_buffer.clear() - self.ssr |= SCI_SSR_TDRE - self.tx_ready_delay = 2 + self.tx_ready_delay = max(0, self.tx_ready_ticks) + self._tx_ready_pending = True self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) + def _write_ssr(self, value: int) -> None: + writable_zero_flags = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER + self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags) + def inject_rx(self, value: int) -> None: self.rdr = value & 0xFF self.ssr |= SCI_SSR_RDRF @@ -104,7 +112,8 @@ class SCI1: return HEARTBEAT_FRAME in self.tx_frames def tick(self) -> None: - if self.tx_ready_delay: + if self._tx_ready_pending and self.tx_ready_delay: self.tx_ready_delay -= 1 - if self.tx_ready_delay == 0: - self.ssr |= SCI_SSR_TDRE + if self._tx_ready_pending and self.tx_ready_delay == 0 and not (self.ssr & SCI_SSR_TDRE): + self.ssr |= SCI_SSR_TDRE + self._tx_ready_pending = False diff --git a/tests/test_emulator_probe_sci.py b/tests/test_emulator_probe_sci.py new file mode 100644 index 0000000..da7fd96 --- /dev/null +++ b/tests/test_emulator_probe_sci.py @@ -0,0 +1,104 @@ +import unittest +from collections import Counter + +from h8536.emulator.constants import IPRE, SCI1_SCR, SCI1_TDR, SCI_SCR_TE +from h8536.emulator.probe import ProbeReport, SCI1Snapshot, SCI1TXISummary, run_probe + + +def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1040) -> bytearray: + rom = bytearray([0xFF] * size) + rom[0:2] = reset.to_bytes(2, "big") + return rom + + +class EmulatorProbeSciTest(unittest.TestCase): + def test_report_lines_include_compact_sci_state_and_txi_summary(self): + report = ProbeReport( + steps=12, + pc=0xBA68, + stopped_reason="max_steps", + hot_pcs=Counter({0xBA68: 7}), + tx_bytes=b"\x00\x03", + sci_accesses=[ + "old write SCR=00", + "H'BA60 read SSR=00", + "H'BA64 write SCR=A0", + "H'BA68 write TDR=03", + ], + sci1=SCI1Snapshot( + smr=0x00, + brr=0x07, + scr=0xA0, + ssr=0x80, + tdr=0x03, + rdr=0x00, + tx_ready_delay=1, + ), + sci1_txi=SCI1TXISummary( + tie=True, + te=True, + tdre=True, + vector_target=0xBA68, + priority=6, + interrupt_mask=2, + interrupt_depth=0, + ), + ) + + lines = report.lines() + + self.assertIn("sci1=SMR=00 BRR=07 SCR=A0 SSR=80 TDR=03 RDR=00 tx_ready_delay=1", lines) + self.assertIn( + "sci1_txi=TIE=1 TE=1 TDRE=1 vector=H'BA68 priority=6 mask=2 depth=0 pending=1 serviceable=1", + lines, + ) + self.assertIn("recent_sci:", lines) + self.assertIn(" H'BA60 read SSR=00", lines) + self.assertIn(" H'BA68 write TDR=03", lines) + + def test_report_lines_bound_recent_sci_accesses(self): + report = ProbeReport( + steps=1, + pc=0x1000, + stopped_reason="max_steps", + sci_accesses=[f"H'{idx:04X} read SSR={idx:02X}" for idx in range(20)], + ) + + lines = report.lines() + + self.assertNotIn(" H'0000 read SSR=00", lines) + self.assertIn(" H'0004 read SSR=04", lines) + self.assertIn(" H'0013 read SSR=13", lines) + + def test_run_probe_tracks_sci_register_accesses_and_final_txi_state(self): + rom = rom_with_reset() + rom[0x0084:0x0086] = (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, 0x50]) + rom[0x1008:0x100D] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, 0xA0]) + rom[0x100D] = 0x00 + rom[0x1010:0x1015] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, SCI_SCR_TE]) + rom[0x1015:0x101A] = bytes([0x15, (SCI1_TDR >> 8) & 0xFF, SCI1_TDR & 0xFF, 0x06, 0x42]) + rom[0x101A] = 0x0A + + report = run_probe( + bytes(rom), + max_steps=8, + interval_steps=512, + stop_on_tx=False, + p9_log_limit=8, + sci_log_limit=8, + watch_pcs=(), + ) + + self.assertEqual(report.tx_bytes, b"\x42") + self.assertIsNotNone(report.sci1) + self.assertIsNotNone(report.sci1_txi) + self.assertTrue(any("write SCR=A0" in line for line in report.sci_accesses)) + self.assertTrue(any("write SCR=20" in line for line in report.sci_accesses)) + self.assertTrue(any("write TDR=42" in line for line in report.sci_accesses)) + self.assertTrue(any(line.startswith("sci1=SMR=00") for line in report.lines())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_sci_timing.py b/tests/test_emulator_sci_timing.py new file mode 100644 index 0000000..0f56e05 --- /dev/null +++ b/tests/test_emulator_sci_timing.py @@ -0,0 +1,71 @@ +import unittest + +from h8536.emulator import ( + HEARTBEAT_FRAME, + SCI1_SCR, + SCI1_SSR, + SCI1_TDR, + SCI_SCR_TE, + SCI_SSR_FER, + SCI_SSR_ORER, + SCI_SSR_PER, + SCI_SSR_RDRF, + SCI_SSR_TDRE, + SCI1, +) + + +class SciTimingTest(unittest.TestCase): + def test_ssr_write_one_does_not_set_hardware_flags(self): + sci = SCI1() + sci.ssr = 0x00 + + sci.write(SCI1_SSR, SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER) + + self.assertEqual(sci.read(SCI1_SSR) & 0xF8, 0x00) + + def test_ssr_write_zero_clears_selected_writable_flags(self): + sci = SCI1() + sci.ssr = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER | 0x07 + + sci.write(SCI1_SSR, sci.ssr & ~SCI_SSR_RDRF) + + self.assertEqual(sci.read(SCI1_SSR) & SCI_SSR_RDRF, 0x00) + self.assertEqual( + sci.read(SCI1_SSR) & (SCI_SSR_TDRE | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER), + SCI_SSR_TDRE | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER, + ) + + def test_tdr_write_then_ssr_clear_delays_tdre_until_ticks(self): + sci = SCI1(tx_ready_ticks=2) + sci.write(SCI1_SCR, sci.read(SCI1_SCR) | SCI_SCR_TE) + self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE) + + sci.write(SCI1_TDR, 0x42) + sci.write(SCI1_SSR, sci.read(SCI1_SSR) & ~SCI_SSR_TDRE) + self.assertFalse(sci.read(SCI1_SSR) & SCI_SSR_TDRE) + + sci.tick() + self.assertFalse(sci.read(SCI1_SSR) & SCI_SSR_TDRE) + + sci.tick() + self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE) + + def test_transmit_capture_requires_te_enabled(self): + sci = SCI1() + + sci.write(SCI1_TDR, 0x33) + self.assertEqual(sci.tx_bytes, []) + self.assertFalse(sci.tx_events[-1].emitted) + + sci.write(SCI1_SCR, sci.read(SCI1_SCR) | SCI_SCR_TE) + for byte in HEARTBEAT_FRAME: + sci.write(SCI1_TDR, byte) + + self.assertEqual(bytes(sci.tx_bytes), HEARTBEAT_FRAME) + self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME]) + self.assertTrue(sci.saw_heartbeat()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_timers.py b/tests/test_emulator_timers.py index 544aaba..4de71e4 100644 --- a/tests/test_emulator_timers.py +++ b/tests/test_emulator_timers.py @@ -4,9 +4,12 @@ from h8536.emulator import H8536Emulator, ON_CHIP_RAM_START from h8536.emulator.constants import ( FRT_TCR_OCIEA, FRT_TCSR_OCFA, + FRT1_TCR, + FRT1_TCSR, FRT2_TCR, FRT2_TCSR, IPRC, + VECTOR_FRT1_OCIA, VECTOR_FRT2_OCIA, ) @@ -23,6 +26,56 @@ def write_mov_b_abs_imm(rom: bytearray, address: int, target: int, value: int) - class Frt2OciaTimerTest(unittest.TestCase): + 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") + + pc = 0x1000 + rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7 + pc += 3 + # IPRC bits 6..4 are FRT1 and bits 2..0 are FRT2, so H'60 makes + # only the FRT1 priority field high enough to pass interrupt mask 0. + pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x60) + pc = write_mov_b_abs_imm(rom, pc, FRT1_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, (FRT1_TCSR >> 8) & 0xFF, FRT1_TCSR & 0xFF, 0xD5]) + rom[isr + 4] = 0x0A # RTE + + emulator = H8536Emulator(bytes(rom), frt1_ocia_steps=2) + emulator.memory.write8(ON_CHIP_RAM_START, 3) + emulator.run(max_steps=5) + + self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 2) + self.assertFalse(emulator.memory.read8(FRT1_TCSR) & FRT_TCSR_OCFA) + self.assertEqual(emulator.cpu.pc, isr + 4) + + def test_frt1_ocia_does_not_fire_when_ociea_disabled(self): + rom = rom_with_reset() + rom[VECTOR_FRT1_OCIA : VECTOR_FRT1_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, 0x60) + rom[pc : pc + 2] = b"\x20\xFE" # BRA self + + rom[0x1020 : 0x1024] = bytes( + [0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C] + ) + rom[0x1024] = 0x0A # RTE + + emulator = H8536Emulator(bytes(rom), frt1_ocia_steps=1) + emulator.memory.write8(ON_CHIP_RAM_START, 3) + emulator.run(max_steps=8) + + self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 3) + self.assertEqual(emulator.memory.read8(FRT1_TCSR) & FRT_TCSR_OCFA, 0) + self.assertEqual(emulator.cpu.pc, pc) + def test_frt2_ocia_vector_can_fire_and_decrement_ram(self): rom = rom_with_reset() rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")