diff --git a/README.md b/README.md index 6d5f903..d99a88e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ To start the current emulator harness: ```powershell .\.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 ``` ## What It Does @@ -76,7 +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, `SCB/F`, stack/call/return support, and scaffolded SCI1 TXI/interval interrupt scheduling. +- 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. Current serial observations: @@ -178,6 +181,7 @@ For the emulator harness: ```powershell python h8536_emulator.py --help +python h8536_emulator_probe.py --help ``` - `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`. @@ -185,7 +189,8 @@ python h8536_emulator.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. -- Current status: boots from `H'1000`, initializes SCI1, supports the first stack/call/interrupt pieces, but does not yet reach the heartbeat. The next emulator work is a better external P9 bit-banged device/handshake model around the `BFE0/BFFE/C08B/C0DB/C121` routines. +- `--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. ## Code Layout @@ -215,7 +220,7 @@ python h8536_emulator.py --help - `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction. - `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/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, runner, CLI, and peripheral scaffolding. +- `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/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. @@ -225,4 +230,4 @@ python h8536_emulator.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`: sidecar analysis CLI wrappers. -- `h8536_emulator.py`: emulator CLI wrapper. +- `h8536_emulator.py`, `h8536_emulator_probe.py`: emulator CLI wrappers. diff --git a/h8536/emulator/__init__.py b/h8536/emulator/__init__.py index d558f9f..8b3585e 100644 --- a/h8536/emulator/__init__.py +++ b/h8536/emulator/__init__.py @@ -3,7 +3,12 @@ from __future__ import annotations from .cli import build_arg_parser, discover_rom_path, load_rom, main from .constants import ( HEARTBEAT_FRAME, + FRT_TCR_OCIEA, + FRT_TCSR_OCFA, + FRT2_TCR, + FRT2_TCSR, IPRA, + IPRC, IPRE, ON_CHIP_RAM_END, ON_CHIP_RAM_START, @@ -28,11 +33,13 @@ from .constants import ( SCI1_SSR, SCI1_TDR, VECTOR_INTERVAL_TIMER, + VECTOR_FRT2_OCIA, VECTOR_SCI1_TXI, WDT_TCSR_R, ) from .cpu import CPUState from .errors import EmulatorError, UnsupportedInstruction +from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent from .memory import MemoryAccess, MemoryMap, describe_regions from .runner import H8536Emulator, RunReport from .sci import SCI1, SciTxEvent @@ -40,9 +47,14 @@ from .sci import SCI1, SciTxEvent __all__ = [ "CPUState", "EmulatorError", + "FRT2_TCR", + "FRT2_TCSR", + "FRT_TCR_OCIEA", + "FRT_TCSR_OCFA", "HEARTBEAT_FRAME", "H8536Emulator", "IPRA", + "IPRC", "IPRE", "MemoryAccess", "MemoryMap", @@ -50,6 +62,9 @@ __all__ = [ "ON_CHIP_RAM_START", "P9DDR", "P9DR", + "P9FastPath", + "P9FastPathConfig", + "P9FastPathEvent", "RAMCR", "REGISTER_FIELD_END", "REGISTER_FIELD_START", @@ -73,6 +88,7 @@ __all__ = [ "SciTxEvent", "UnsupportedInstruction", "VECTOR_INTERVAL_TIMER", + "VECTOR_FRT2_OCIA", "VECTOR_SCI1_TXI", "WDT_TCSR_R", "build_arg_parser", diff --git a/h8536/emulator/cli.py b/h8536/emulator/cli.py index cb956f9..358abc8 100644 --- a/h8536/emulator/cli.py +++ b/h8536/emulator/cli.py @@ -3,7 +3,7 @@ from __future__ import annotations import argparse from pathlib import Path -from ..formatting import h16 +from ..formatting import h16, parse_int from .memory import describe_regions from .runner import H8536Emulator @@ -39,6 +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("--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") return parser @@ -50,7 +53,13 @@ def main(argv: list[str] | None = None) -> int: print(str(exc)) return 2 - emulator = H8536Emulator(rom_bytes, interval_steps=args.interval_steps) + emulator = H8536Emulator( + rom_bytes, + interval_steps=args.interval_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, + ) print(f"rom={rom_path}") print(f"reset_vector={h16(emulator.reset_vector())}") if args.memory_map: diff --git a/h8536/emulator/constants.py b/h8536/emulator/constants.py index 0d81a01..0753596 100644 --- a/h8536/emulator/constants.py +++ b/h8536/emulator/constants.py @@ -12,9 +12,13 @@ P9DDR = 0xFEFE P9DR = 0xFEFF IPRA = 0xFF00 +IPRC = 0xFF02 IPRE = 0xFF04 WDT_TCSR_R = 0xFEEC +FRT2_TCR = 0xFEA0 +FRT2_TCSR = 0xFEA1 + SCI_SCR_TIE = 0x80 SCI_SCR_RIE = 0x40 SCI_SCR_TE = 0x20 @@ -24,6 +28,8 @@ SCI_SSR_RDRF = 0x40 SCI_SSR_ORER = 0x20 SCI_SSR_FER = 0x10 SCI_SSR_PER = 0x08 +FRT_TCR_OCIEA = 0x20 +FRT_TCSR_OCFA = 0x20 ON_CHIP_RAM_START = 0xF680 ON_CHIP_RAM_END = 0xFE7F @@ -33,4 +39,5 @@ RAMCR = 0xFF11 HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) VECTOR_INTERVAL_TIMER = 0x0042 +VECTOR_FRT2_OCIA = 0x006A VECTOR_SCI1_TXI = 0x0084 diff --git a/h8536/emulator/fast_paths.py b/h8536/emulator/fast_paths.py new file mode 100644 index 0000000..838912d --- /dev/null +++ b/h8536/emulator/fast_paths.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from .cpu import mask, sign_bit + + +LOC_BFE0_TRANSFER_WRAPPER = 0xBFE0 +LOC_BFFE_TRANSFER_WRAPPER = 0xBFFE +LOC_C08B_P9_WRITE_BYTE = 0xC08B +LOC_C0DB_P9_READ_BYTE = 0xC0DB +LOC_C10C_P9_MARKER = 0xC10C +LOC_C121_P9_MARKER = 0xC121 +LOC_C142_P9_MARKER = 0xC142 + + +@dataclass(frozen=True) +class P9FastPathConfig: + """Configuration for optional ROM P9 transfer shortcuts. + + The helper assumes the CPU PC is exactly at a known routine entry. It + models the routine as if it completed successfully and returned via RTS. + Integration should keep this disabled unless the runner intentionally opts + into skipping these ROM routines. + """ + + enabled: bool = False + write_byte_pc: int = LOC_C08B_P9_WRITE_BYTE + read_byte_pc: int = LOC_C0DB_P9_READ_BYTE + marker_pcs: frozenset[int] = frozenset( + { + LOC_C10C_P9_MARKER, + LOC_C121_P9_MARKER, + LOC_C142_P9_MARKER, + } + ) + wrapper_pcs: frozenset[int] = frozenset( + { + LOC_BFE0_TRANSFER_WRAPPER, + LOC_BFFE_TRANSFER_WRAPPER, + } + ) + default_input_byte: int = 0xFF + account_step: bool = True + cycles_per_hit: int = 0 + + +@dataclass(frozen=True) +class P9FastPathEvent: + kind: str + pc: int + value: int | None = None + + +@dataclass +class P9FastPath: + """Optional fast-path scaffold for ROM P9 bit-transfer routines.""" + + config: P9FastPathConfig = field(default_factory=P9FastPathConfig) + input_bytes: list[int] = field(default_factory=list) + output_bytes: list[int] = field(default_factory=list) + events: list[P9FastPathEvent] = field(default_factory=list) + + def queue_input(self, *values: int) -> None: + self.input_bytes.extend(value & 0xFF for value in values) + + def try_handle(self, emulator: Any) -> bool: + if not self.config.enabled: + return False + + pc = emulator.cpu.pc & 0xFFFF + if pc == (self.config.write_byte_pc & 0xFFFF): + self._handle_write_byte(emulator) + elif pc == (self.config.read_byte_pc & 0xFFFF): + self._handle_read_byte(emulator) + elif pc in self.config.marker_pcs: + self.events.append(P9FastPathEvent("marker", pc)) + self._return_from_subroutine(emulator) + elif pc in self.config.wrapper_pcs: + self.events.append(P9FastPathEvent("wrapper_success", pc)) + emulator.cpu.regs[0] = 1 + self._set_logic_flags(emulator.cpu, 1, 1) + self._return_from_subroutine(emulator) + else: + return False + + if self.config.account_step: + emulator.cpu.steps += 1 + emulator.cpu.cycles += self.config.cycles_per_hit + return True + + def _handle_write_byte(self, emulator: Any) -> None: + pc = emulator.cpu.pc & 0xFFFF + value = emulator.cpu.regs[0] & 0xFF + self.output_bytes.append(value) + self.events.append(P9FastPathEvent("write_byte", pc, value)) + + emulator.cpu.regs[0] = 1 + self._set_logic_flags(emulator.cpu, 1, 1) + self._return_from_subroutine(emulator) + + def _handle_read_byte(self, emulator: Any) -> None: + pc = emulator.cpu.pc & 0xFFFF + value = self.input_bytes.pop(0) if self.input_bytes else self.config.default_input_byte + value &= 0xFF + self.events.append(P9FastPathEvent("read_byte", pc, value)) + + # The ROM-side read routine yields a byte in R5. Model that as a byte + # register write so the existing high byte is not accidentally clobbered. + emulator.cpu.regs[5] = (emulator.cpu.regs[5] & 0xFF00) | value + self._set_logic_flags(emulator.cpu, value, 1) + self._return_from_subroutine(emulator) + + def _return_from_subroutine(self, emulator: Any) -> None: + sp = emulator.cpu.regs[7] & 0xFFFF + emulator.cpu.pc = emulator.memory.read16(sp) & 0xFFFF + emulator.cpu.regs[7] = (sp + 2) & 0xFFFF + + def _set_logic_flags(self, cpu: Any, value: int, size: int) -> None: + value &= mask(size) + cpu.z = value == 0 + cpu.n = bool(value & sign_bit(size)) + cpu.v = False diff --git a/h8536/emulator/memory.py b/h8536/emulator/memory.py index 19798f3..615fdd1 100644 --- a/h8536/emulator/memory.py +++ b/h8536/emulator/memory.py @@ -21,7 +21,7 @@ from .constants import ( SCI1_TDR, ) from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS -from .peripherals.p9_bus import P9_ACK_BIT +from .peripherals.p9_bus import P9Bus from .sci import SCI1 @@ -38,6 +38,7 @@ class MemoryMap: def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None: self.rom = Rom(rom_bytes, base=0) self.sci1 = sci1 if sci1 is not None else SCI1() + self.p9_bus = P9Bus() self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1) self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1) self.external: dict[int, int] = {} @@ -58,22 +59,23 @@ class MemoryMap: if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): value = self.sci1.read(address) self._set_register(address, value) - elif address in self.external: - value = self.external[address] - elif address in (LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS): + elif address == LCD_E_CLOCK_STATUS: # LCD E-clock/status space. Default to ready/zero so boot can pass # busy-flag polling until a fuller external bus model exists. value = 0x00 + elif address == LCD_E_CLOCK_DATA: + value = self.external.get(address, 0x00) + elif address in self.external: + value = self.external[address] elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: value = self.ram[address - ON_CHIP_RAM_START] + elif address == P9DDR: + value = self.p9_bus.read_ddr() + self._set_register(address, value) + elif address == P9DR: + value = self.p9_bus.read_dr() elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: value = self.registers[address - REGISTER_FIELD_START] - if address == P9DR and not (self.registers[P9DDR - REGISTER_FIELD_START] & 0x80): - # P97 is used as an input during the serial panel/camera-side - # bit-bang handshake. With no external device modeled, hold the - # input low so the firmware sees an idle/acknowledged bus rather - # than reading back its previous output latch forever. - value &= ~P9_ACK_BIT elif self.rom.contains(address): value = self.rom.u8(address) else: @@ -94,6 +96,10 @@ class MemoryMap: self._set_register(address, self.sci1.read(address)) elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: self.ram[address - ON_CHIP_RAM_START] = value + elif address == P9DDR: + self._set_register(address, self.p9_bus.write_ddr(value)) + elif address == P9DR: + self._set_register(address, self.p9_bus.write_dr(value)) elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: self._set_register(address, value) elif self.rom.contains(address): diff --git a/h8536/emulator/peripherals/__init__.py b/h8536/emulator/peripherals/__init__.py index e69a9dc..909e0d9 100644 --- a/h8536/emulator/peripherals/__init__.py +++ b/h8536/emulator/peripherals/__init__.py @@ -1,10 +1,13 @@ from __future__ import annotations from .lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS -from .p9_bus import P9_ACK_BIT +from .p9_bus import P9_ACK_BIT, P9_STROBE_BIT, P9Bus, P9StrobeEvent __all__ = [ "LCD_E_CLOCK_DATA", "LCD_E_CLOCK_STATUS", "P9_ACK_BIT", + "P9_STROBE_BIT", + "P9Bus", + "P9StrobeEvent", ] diff --git a/h8536/emulator/peripherals/lcd.py b/h8536/emulator/peripherals/lcd.py index 86e6239..150815d 100644 --- a/h8536/emulator/peripherals/lcd.py +++ b/h8536/emulator/peripherals/lcd.py @@ -1,5 +1,5 @@ from __future__ import annotations -LCD_E_CLOCK_DATA = 0xF200 -LCD_E_CLOCK_STATUS = 0xF201 +LCD_E_CLOCK_STATUS = 0xF200 +LCD_E_CLOCK_DATA = 0xF201 diff --git a/h8536/emulator/peripherals/p9_bus.py b/h8536/emulator/peripherals/p9_bus.py index 335bede..ff78b75 100644 --- a/h8536/emulator/peripherals/p9_bus.py +++ b/h8536/emulator/peripherals/p9_bus.py @@ -1,4 +1,80 @@ from __future__ import annotations +from dataclasses import dataclass +from typing import Iterable + P9_ACK_BIT = 0x80 +P9_STROBE_BIT = 0x02 + + +@dataclass(frozen=True) +class P9StrobeEvent: + edge: str + ddr: int + dr: int + data_bit: int + bit7_output: bool + + +class P9Bus: + """Small model for the ROM's P9 bit-banged serial handshake.""" + + def __init__(self, ddr: int = 0x00, dr: int = 0x00, input_bits: Iterable[int] = ()) -> None: + self.ddr = ddr & 0xFF + self.dr_latch = dr & 0xFF + self.input_bits: list[int] = [1 if bit else 0 for bit in input_bits] + self.default_input_bit = 0 + self.strobe_edges: list[P9StrobeEvent] = [] + self.transmitted_bits: list[int] = [] + self.byte_candidates: list[int] = [] + + def write_ddr(self, value: int) -> int: + self.ddr = value & 0xFF + return self.ddr + + def write_dr(self, value: int) -> int: + previous = self.dr_latch + self.dr_latch = value & 0xFF + + previous_strobe = bool(previous & P9_STROBE_BIT) + current_strobe = bool(self.dr_latch & P9_STROBE_BIT) + if previous_strobe != current_strobe: + edge = "rising" if current_strobe else "falling" + data_bit = 1 if self.dr_latch & P9_ACK_BIT else 0 + bit7_output = bool(self.ddr & P9_ACK_BIT) + self.strobe_edges.append(P9StrobeEvent(edge, self.ddr, self.dr_latch, data_bit, bit7_output)) + if edge == "rising" and bit7_output: + self._record_transmitted_bit(data_bit) + + return self.dr_latch + + def read_ddr(self) -> int: + return self.ddr + + def read_dr(self) -> int: + value = self.dr_latch + if not (self.ddr & P9_ACK_BIT): + if self.input_bits: + input_bit = self.input_bits.pop(0) + else: + input_bit = self.default_input_bit + if input_bit: + value |= P9_ACK_BIT + else: + value &= ~P9_ACK_BIT + return value & 0xFF + + def queue_input_bits(self, bits: Iterable[int]) -> None: + self.input_bits.extend(1 if bit else 0 for bit in bits) + + def set_default_input_bit(self, bit: int) -> None: + self.default_input_bit = 1 if bit else 0 + + def _record_transmitted_bit(self, bit: int) -> None: + self.transmitted_bits.append(bit) + if len(self.transmitted_bits) % 8 == 0: + byte = 0 + for data_bit in self.transmitted_bits[-8:]: + byte = (byte << 1) | data_bit + self.byte_candidates.append(byte) diff --git a/h8536/emulator/probe.py b/h8536/emulator/probe.py new file mode 100644 index 0000000..f38410f --- /dev/null +++ b/h8536/emulator/probe.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import argparse +from collections import Counter +from dataclasses import dataclass, field +from pathlib import Path + +from ..formatting import h16, parse_int +from .cli import load_rom +from .constants import P9DDR, P9DR, SCI1_TDR +from .errors import UnsupportedInstruction +from .runner import H8536Emulator + + +DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059) + + +@dataclass(frozen=True) +class WatchSnapshot: + pc: int + step: int + regs: tuple[int, ...] + sp: int + stack_words: tuple[tuple[int, int], ...] + callers: tuple[tuple[int, int | None], ...] + + def line(self) -> str: + regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs)) + stack = " ".join(f"{h16(address)}:{h16(value)}" for address, value in self.stack_words) + if self.callers: + callers = " ".join( + f"{h16(return_address)}<-{h16(call_site)}" if call_site is not None else h16(return_address) + for return_address, call_site in self.callers + ) + else: + callers = "-" + return f"step={self.step} pc={h16(self.pc)} sp={h16(self.sp)} {regs} stack=[{stack}] callers=[{callers}]" + + +@dataclass +class ProbeReport: + steps: int + pc: int + stopped_reason: str + hot_pcs: Counter[int] = field(default_factory=Counter) + tx_bytes: bytes = b"" + p9_bytes: list[int] = field(default_factory=list) + p9_fast_bytes: list[int] = field(default_factory=list) + p9_fast_events: int = 0 + p9_accesses: list[str] = field(default_factory=list) + sci_accesses: list[str] = field(default_factory=list) + watch_snapshots: list[WatchSnapshot] = field(default_factory=list) + unsupported: str | None = None + + def lines(self, hot_limit: int = 12) -> list[str]: + lines = [ + f"steps={self.steps}", + f"pc={h16(self.pc)}", + f"stopped={self.stopped_reason}", + "tx_bytes=" + self.tx_bytes.hex(" ").upper(), + "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.unsupported: + lines.append(f"unsupported={self.unsupported}") + if self.p9_accesses: + lines.append("recent_p9:") + lines.extend(" " + line for line in self.p9_accesses[-24:]) + if self.sci_accesses: + lines.append("recent_sci:") + lines.extend(" " + line for line in self.sci_accesses[-16:]) + if self.watch_snapshots: + lines.append("recent_watch_snapshots:") + lines.extend(" " + snapshot.line() for snapshot in self.watch_snapshots) + return lines + + +def parse_watch_pc(text: str) -> int: + try: + value = parse_int(text) + except ValueError: + value = int(text, 16) + return value & 0xFFFF + + +def _likely_call_site(emulator: H8536Emulator, return_address: int) -> int | None: + rom = emulator.memory.rom + for size in (2, 3): + candidate = (return_address - size) & 0xFFFF + if not rom.contains(candidate, size): + continue + opcode = rom.u8(candidate) + if size == 2 and opcode == 0x0E: + return candidate + if size == 3 and opcode in (0x1E, 0x18): + return candidate + return None + + +def _watch_snapshot(emulator: H8536Emulator, *, stack_words: int = 6) -> WatchSnapshot: + sp = emulator.cpu.regs[7] & 0xFFFF + words: list[tuple[int, int]] = [] + callers: list[tuple[int, int | None]] = [] + seen_callers: set[int] = set() + for offset in range(0, stack_words * 2, 2): + address = (sp + offset) & 0xFFFF + try: + value = emulator.memory.read16(address) + except Exception: + continue + words.append((address, value)) + if value in seen_callers or not emulator.memory.rom.contains(value): + continue + seen_callers.add(value) + callers.append((value, _likely_call_site(emulator, value))) + return WatchSnapshot( + pc=emulator.cpu.pc & 0xFFFF, + step=emulator.cpu.steps, + regs=tuple(register & 0xFFFF for register in emulator.cpu.regs), + sp=sp, + stack_words=tuple(words), + callers=tuple(callers), + ) + + +def run_probe( + rom_bytes: bytes, + *, + max_steps: int, + interval_steps: int, + stop_on_tx: bool, + p9_log_limit: int, + frt2_ocia_steps: int = 1024, + p9_fast_path: bool = False, + p9_fast_input: int = 0xFF, + watch_pcs: list[int] | tuple[int, ...] | None = None, + watch_snapshot_limit: int = 32, + watch_pc_limit: int = 8, + watch_min_interval: int = 1024, +) -> ProbeReport: + emulator = H8536Emulator( + rom_bytes, + interval_steps=interval_steps, + frt2_ocia_steps=frt2_ocia_steps, + p9_fast_path_enabled=p9_fast_path, + p9_fast_default_input_byte=p9_fast_input, + ) + hot_pcs: Counter[int] = Counter() + p9_accesses: list[str] = [] + sci_accesses: list[str] = [] + snapshots: list[WatchSnapshot] = [] + watch_set = set(DEFAULT_WATCH_PCS if watch_pcs is None else watch_pcs) + watch_counts: Counter[int] = Counter() + watch_last_step: dict[int, int] = {} + stopped_reason = "max_steps" + unsupported: str | None = None + last_access_index = 0 + + for _ in range(max_steps): + pc = emulator.cpu.pc + hot_pcs[pc] += 1 + if pc in watch_set and watch_counts[pc] < watch_pc_limit: + last_step = watch_last_step.get(pc) + if last_step is None or emulator.cpu.steps - last_step >= watch_min_interval: + snapshots.append(_watch_snapshot(emulator)) + if len(snapshots) > watch_snapshot_limit: + del snapshots[: len(snapshots) - watch_snapshot_limit] + watch_counts[pc] += 1 + watch_last_step[pc] = emulator.cpu.steps + last_access_index = len(emulator.memory.access_log) + try: + emulator.step() + except UnsupportedInstruction as exc: + stopped_reason = "unsupported_instruction" + unsupported = str(exc) + break + + for access in emulator.memory.access_log[last_access_index:]: + if access.address in (P9DDR, P9DR): + p9_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}") + 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}") + last_access_index = len(emulator.memory.access_log) + + if stop_on_tx and emulator.sci1.tx_bytes: + stopped_reason = "tx" + break + if emulator.sci1.saw_heartbeat(): + stopped_reason = "heartbeat" + break + + return ProbeReport( + steps=emulator.cpu.steps, + pc=emulator.cpu.pc, + stopped_reason=stopped_reason, + hot_pcs=hot_pcs, + tx_bytes=bytes(emulator.sci1.tx_bytes), + p9_bytes=list(emulator.memory.p9_bus.byte_candidates), + p9_fast_bytes=list(emulator.p9_fast_path.output_bytes), + p9_fast_events=len(emulator.p9_fast_path.events), + p9_accesses=p9_accesses, + sci_accesses=sci_accesses, + watch_snapshots=snapshots, + unsupported=unsupported, + ) + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Probe H8/536 emulator progress and likely hold-ups") + 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("--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("--hot-limit", type=int, default=12) + parser.add_argument( + "--watch-pc", + action="append", + type=parse_watch_pc, + default=[], + help="additional PC to snapshot when hit, e.g. C08B, 0xC08B, or H'C08B", + ) + parser.add_argument("--watch-snapshot-limit", type=int, default=32) + parser.add_argument("--watch-pc-limit", type=int, default=8) + parser.add_argument("--watch-min-interval", type=int, default=1024) + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_arg_parser().parse_args(argv) + try: + rom_bytes, rom_path = load_rom(args.rom) + except FileNotFoundError as exc: + print(str(exc)) + return 2 + print(f"rom={rom_path}") + report = run_probe( + rom_bytes, + max_steps=args.max_steps, + interval_steps=args.interval_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, + 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, + watch_min_interval=args.watch_min_interval, + ) + for line in report.lines(hot_limit=args.hot_limit): + print(line) + return 0 diff --git a/h8536/emulator/runner.py b/h8536/emulator/runner.py index de15442..598b9b8 100644 --- a/h8536/emulator/runner.py +++ b/h8536/emulator/runner.py @@ -7,15 +7,22 @@ from ..formatting import h16 from ..rom import DecodeError from ..vectors import read_vectors_min from .constants import ( + FRT_TCR_OCIEA, + FRT_TCSR_OCFA, + FRT2_TCR, + FRT2_TCSR, IPRA, + IPRC, IPRE, SCI_SCR_TIE, SCI_SSR_TDRE, + VECTOR_FRT2_OCIA, VECTOR_INTERVAL_TIMER, VECTOR_SCI1_TXI, ) from .cpu import CPUState, mask, s8, s16, sign_bit from .errors import EmulatorError, UnsupportedInstruction +from .fast_paths import P9FastPath, P9FastPathConfig from .memory import MemoryMap from .sci import SCI1 @@ -51,15 +58,29 @@ class RunReport: class H8536Emulator: - def __init__(self, rom_bytes: bytes, *, interval_steps: int = 2048) -> None: + def __init__( + self, + rom_bytes: bytes, + *, + interval_steps: int = 2048, + frt2_ocia_steps: int = 1024, + p9_fast_path: P9FastPath | None = None, + p9_fast_path_enabled: bool = False, + p9_fast_default_input_byte: int = 0xFF, + ) -> None: if not rom_bytes: raise ValueError("ROM image is empty") self.sci1 = SCI1() self.memory = MemoryMap(rom_bytes, self.sci1) + self.p9_fast_path = p9_fast_path or P9FastPath( + P9FastPathConfig(enabled=p9_fast_path_enabled, default_input_byte=p9_fast_default_input_byte) + ) self.cpu = CPUState() self.vectors = read_vectors_min(self.memory.rom) self.interval_steps = max(1, interval_steps) + self.frt2_ocia_steps = max(1, frt2_ocia_steps) self._interval_counter = 0 + self._frt2_ocia_counter = 0 self.reset() def reset(self) -> None: @@ -72,6 +93,10 @@ class H8536Emulator: def step(self) -> str: pc = self.cpu.pc + if self.p9_fast_path.try_handle(self): + self._tick_peripherals() + return f"{h16(pc)}: {'':<17} P9 fast-path" + decoder = H8536Decoder(self.memory.rom, br=self.cpu.br) ins = decoder.decode(pc) if not ins.valid: @@ -99,6 +124,7 @@ class H8536Emulator: self._cmp(self.cpu.regs[reg], int.from_bytes(raw[1:3], "big"), 2) elif 0x50 <= raw[0] <= 0x57 and len(raw) == 2: self._reg_write(raw[0] & 0x07, raw[1], 1) + self._set_logic_flags(raw[1], 1) elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3: self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big") self._set_logic_flags(self.cpu.regs[raw[0] & 0x07], 2) @@ -221,6 +247,13 @@ class H8536Emulator: self._set_logic_flags(result, size) elif base == 0x70: self._cmp(self._reg_read(rd, size), self._read_ea(ea, size), size) + elif base == 0xA8: + if size != 1: + raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text) + result = (self._reg_read(rd, 1) * self._read_ea(ea, 1)) & 0xFFFF + self.cpu.regs[rd] = result + self._set_logic_flags(result, 2) + self.cpu.c = False elif op in (0x08, 0x09, 0x0C, 0x0D): delta = {0x08: 1, 0x09: 2, 0x0C: -1, 0x0D: -2}[op] old = self._read_ea(ea, size) @@ -233,6 +266,10 @@ class H8536Emulator: elif op == 0x13: self._write_ea(ea, 0, size) self._set_logic_flags(0, size) + elif op == 0x15: + result = (~self._read_ea(ea, size)) & mask(size) + self._write_ea(ea, result, size) + self._set_logic_flags(result, size) elif op == 0x16: self._set_logic_flags(self._read_ea(ea, size), size) elif op == 0x10: @@ -335,6 +372,7 @@ class H8536Emulator: def _tick_peripherals(self) -> None: self.sci1.tick() self._interval_counter += 1 + self._frt2_ocia_counter += 1 self._service_pending_interrupt() def _service_pending_interrupt(self) -> None: @@ -349,6 +387,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._frt2_ocia_pending(): + target = self._vector_target(VECTOR_FRT2_OCIA) + if target is not None: + candidates.append((self._frt2_priority(), target, "frt2_ocia")) if not candidates: return @@ -357,6 +399,9 @@ class H8536Emulator: return if source == "interval_timer": self._interval_counter = 0 + elif source == "frt2_ocia": + 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: @@ -385,6 +430,16 @@ class H8536Emulator: def _interval_priority(self) -> int: return (self.memory.read8(IPRA) >> 4) & 0x07 + def _frt2_ocia_pending(self) -> bool: + if self._frt2_ocia_counter < self.frt2_ocia_steps: + return False + return bool(self.memory.read8(FRT2_TCR) & FRT_TCR_OCIEA) + + def _frt2_priority(self) -> int: + # H8/536 IPRC assigns bits 6..4 to FRT1 and bits 2..0 to FRT2; + # the ROM's IPRC=H'66 therefore gives both timers priority 6. + return self.memory.read8(IPRC) & 0x07 + def _push16(self, value: int) -> None: sp = (self.cpu.regs[7] - 2) & 0xFFFF self.cpu.regs[7] = sp diff --git a/h8536_emulator_probe.py b/h8536_emulator_probe.py new file mode 100644 index 0000000..531a369 --- /dev/null +++ b/h8536_emulator_probe.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""Compatibility wrapper for the H8/536 emulator progress probe.""" + +from h8536.emulator.probe import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_emulator_fast_paths.py b/tests/test_emulator_fast_paths.py new file mode 100644 index 0000000..521a289 --- /dev/null +++ b/tests/test_emulator_fast_paths.py @@ -0,0 +1,87 @@ +import unittest + +from h8536.emulator.fast_paths import ( + LOC_C08B_P9_WRITE_BYTE, + LOC_C0DB_P9_READ_BYTE, + P9FastPath, + P9FastPathConfig, +) +from h8536.emulator.runner import H8536Emulator + + +def rom_with_reset(*, reset: int = 0x1000, size: int = 0xD000) -> bytearray: + rom = bytearray([0xFF] * size) + rom[0:2] = reset.to_bytes(2, "big") + return rom + + +class P9FastPathTest(unittest.TestCase): + def test_disabled_fast_path_does_not_handle_known_pc(self): + emulator = H8536Emulator(bytes(rom_with_reset())) + emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE + + fast_path = P9FastPath() + + self.assertFalse(fast_path.try_handle(emulator)) + self.assertEqual(emulator.cpu.pc, LOC_C08B_P9_WRITE_BYTE) + + def test_c08b_write_byte_logs_r0_sets_success_and_returns_to_caller(self): + emulator = H8536Emulator(bytes(rom_with_reset())) + emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE + emulator.cpu.regs[0] = 0x12A5 + emulator.cpu.regs[7] = 0xFE7E + emulator.cpu.c = True + emulator.cpu.v = True + emulator.memory.write16(0xFE7E, 0x3456) + + fast_path = P9FastPath(P9FastPathConfig(enabled=True)) + + self.assertTrue(fast_path.try_handle(emulator)) + self.assertEqual(fast_path.output_bytes, [0xA5]) + self.assertEqual(fast_path.events[-1].kind, "write_byte") + self.assertEqual(fast_path.events[-1].value, 0xA5) + self.assertEqual(emulator.cpu.regs[0], 1) + self.assertEqual(emulator.cpu.pc, 0x3456) + self.assertEqual(emulator.cpu.regs[7], 0xFE80) + self.assertFalse(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertTrue(emulator.cpu.c) + self.assertEqual(emulator.cpu.steps, 1) + + def test_c0db_read_byte_puts_queued_byte_in_r5_low_and_returns(self): + emulator = H8536Emulator(bytes(rom_with_reset())) + emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE + emulator.cpu.regs[5] = 0xBE00 + emulator.cpu.regs[7] = 0xFE7C + emulator.memory.write16(0xFE7C, 0x4567) + + fast_path = P9FastPath(P9FastPathConfig(enabled=True), input_bytes=[0x3C]) + + self.assertTrue(fast_path.try_handle(emulator)) + self.assertEqual(emulator.cpu.regs[5], 0xBE3C) + self.assertEqual(emulator.cpu.pc, 0x4567) + self.assertEqual(emulator.cpu.regs[7], 0xFE7E) + self.assertEqual(fast_path.events[-1].kind, "read_byte") + self.assertEqual(fast_path.events[-1].value, 0x3C) + self.assertFalse(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + + def test_c0db_read_byte_uses_default_when_queue_is_empty(self): + emulator = H8536Emulator(bytes(rom_with_reset())) + emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE + emulator.cpu.regs[7] = 0xFE80 + emulator.memory.write16(0xFE80, 0x5678) + + fast_path = P9FastPath(P9FastPathConfig(enabled=True, default_input_byte=0x81)) + + self.assertTrue(fast_path.try_handle(emulator)) + self.assertEqual(emulator.cpu.regs[5], 0x0081) + self.assertEqual(emulator.cpu.pc, 0x5678) + self.assertFalse(emulator.cpu.z) + self.assertTrue(emulator.cpu.n) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_flags.py b/tests/test_emulator_flags.py new file mode 100644 index 0000000..6283970 --- /dev/null +++ b/tests/test_emulator_flags.py @@ -0,0 +1,116 @@ +import unittest + +from h8536.emulator import H8536Emulator + + +def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray: + rom = bytearray([0xFF] * size) + rom[0:2] = reset.to_bytes(2, "big") + return rom + + +class EmulatorMovFlagTest(unittest.TestCase): + def test_mov_e_byte_zero_sets_z_for_beq_and_preserves_c(self): + rom = rom_with_reset() + rom[0x1000:0x1002] = b"\x50\x00" # MOV:E.B #H'00, R0 + rom[0x1002:0x1004] = b"\x27\x02" # BEQ H'1006 + rom[0x1004:0x1006] = b"\x51\x01" # MOV:E.B #H'01, R1 + rom[0x1006:0x1008] = b"\x52\x02" # MOV:E.B #H'02, R2 + + emulator = H8536Emulator(bytes(rom)) + emulator.cpu.c = True + emulator.cpu.v = True + emulator.step() + + self.assertTrue(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertTrue(emulator.cpu.c) + + emulator.run(max_steps=2) + + self.assertEqual(emulator.cpu.regs[1], 0) + self.assertEqual(emulator.cpu.regs[2], 2) + self.assertEqual(emulator.cpu.pc, 0x1008) + + def test_mov_e_byte_one_clears_z_for_beq_and_preserves_c(self): + rom = rom_with_reset() + rom[0x1000:0x1002] = b"\x50\x01" # MOV:E.B #H'01, R0 + rom[0x1002:0x1004] = b"\x27\x02" # BEQ H'1006 + rom[0x1004:0x1006] = b"\x51\x01" # MOV:E.B #H'01, R1 + rom[0x1006:0x1008] = b"\x52\x02" # MOV:E.B #H'02, R2 + + emulator = H8536Emulator(bytes(rom)) + emulator.cpu.c = True + emulator.cpu.v = True + emulator.step() + + self.assertFalse(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertTrue(emulator.cpu.c) + + emulator.run(max_steps=2) + + self.assertEqual(emulator.cpu.regs[1], 1) + self.assertEqual(emulator.cpu.regs[2], 0) + self.assertEqual(emulator.cpu.pc, 0x1006) + + def test_mov_i_word_immediate_sets_word_flags_and_preserves_c(self): + rom = rom_with_reset() + rom[0x1000:0x1003] = b"\x58\x80\x00" # MOV:I.W #H'8000, R0 + rom[0x1003:0x1006] = b"\x59\x00\x00" # MOV:I.W #H'0000, R1 + + emulator = H8536Emulator(bytes(rom)) + emulator.cpu.c = True + emulator.cpu.v = True + emulator.step() + + self.assertEqual(emulator.cpu.regs[0], 0x8000) + self.assertFalse(emulator.cpu.z) + self.assertTrue(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertTrue(emulator.cpu.c) + + emulator.step() + + self.assertEqual(emulator.cpu.regs[1], 0) + self.assertTrue(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertTrue(emulator.cpu.c) + + def test_mulxu_byte_immediate_writes_word_result_and_clears_carry(self): + rom = rom_with_reset() + rom[0x1000:0x1002] = b"\x53\x12" # MOV:E.B #H'12, R3 + rom[0x1002:0x1005] = b"\x04\x10\xAB" # MULXU.B #H'10, R3 + + emulator = H8536Emulator(bytes(rom)) + emulator.cpu.c = True + emulator.run(max_steps=2) + + self.assertEqual(emulator.cpu.regs[3], 0x0120) + self.assertFalse(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertFalse(emulator.cpu.c) + + def test_not_byte_memory_updates_logic_flags_and_preserves_carry(self): + rom = rom_with_reset() + rom[0x1000:0x1005] = b"\x15\xF6\x80\x15\x00" # NOT.B @H'F680, then NOP + + emulator = H8536Emulator(bytes(rom)) + emulator.memory.write8(0xF680, 0xFF) + emulator.cpu.c = True + emulator.cpu.v = True + emulator.step() + + self.assertEqual(emulator.memory.read8(0xF680), 0x00) + self.assertTrue(emulator.cpu.z) + self.assertFalse(emulator.cpu.n) + self.assertFalse(emulator.cpu.v) + self.assertTrue(emulator.cpu.c) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_lcd.py b/tests/test_emulator_lcd.py new file mode 100644 index 0000000..36a37e9 --- /dev/null +++ b/tests/test_emulator_lcd.py @@ -0,0 +1,25 @@ +import unittest + +from h8536.emulator import MemoryMap +from h8536.emulator.peripherals import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS + + +class EmulatorLcdBusTest(unittest.TestCase): + def test_status_read_reports_ready_even_after_command_write(self): + memory = MemoryMap(b"\x00" * 4) + memory.write8(LCD_E_CLOCK_STATUS, 0x80) + + self.assertEqual(memory.read8(LCD_E_CLOCK_STATUS), 0x00) + + def test_data_read_returns_data_latch_defaulting_to_zero(self): + memory = MemoryMap(b"\x00" * 4) + + self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x00) + + memory.write8(LCD_E_CLOCK_DATA, 0x41) + + self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x41) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_probe.py b/tests/test_emulator_probe.py new file mode 100644 index 0000000..ad7567a --- /dev/null +++ b/tests/test_emulator_probe.py @@ -0,0 +1,56 @@ +import unittest + +from h8536.emulator.probe import DEFAULT_WATCH_PCS, parse_watch_pc, run_probe + + +def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray: + rom = bytearray([0xFF] * size) + rom[0:2] = reset.to_bytes(2, "big") + return rom + + +class EmulatorProbeTest(unittest.TestCase): + def test_parse_watch_pc_accepts_h8_hex_forms(self): + self.assertEqual(parse_watch_pc("C08B"), 0xC08B) + self.assertEqual(parse_watch_pc("0xC08B"), 0xC08B) + self.assertEqual(parse_watch_pc("H'C08B"), 0xC08B) + + def test_default_watch_pcs_include_bit_bang_transfer_path(self): + self.assertIn(0xC08B, DEFAULT_WATCH_PCS) + self.assertIn(0xC0DB, DEFAULT_WATCH_PCS) + self.assertIn(0xC121, DEFAULT_WATCH_PCS) + self.assertIn(0xBFE0, DEFAULT_WATCH_PCS) + self.assertIn(0xBFFE, DEFAULT_WATCH_PCS) + self.assertIn(0xC059, DEFAULT_WATCH_PCS) + + def test_watch_snapshot_includes_bsr_return_address_on_stack(self): + rom = rom_with_reset() + rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7 + rom[0x1003:0x1005] = b"\x0E\x03" # BSR H'1008, return H'1005 + rom[0x1005:0x1008] = b"\x59\x12\x34" # MOV:I.W #H'1234, R1 + rom[0x1008] = 0x19 # RTS + + report = run_probe( + bytes(rom), + max_steps=4, + interval_steps=512, + stop_on_tx=False, + p9_log_limit=8, + watch_pcs=(0x1008,), + watch_snapshot_limit=4, + watch_pc_limit=2, + watch_min_interval=0, + ) + + self.assertEqual(len(report.watch_snapshots), 1) + snapshot = report.watch_snapshots[0] + self.assertEqual(snapshot.pc, 0x1008) + self.assertEqual(snapshot.sp, 0xFE7E) + self.assertIn((0xFE7E, 0x1005), snapshot.stack_words) + self.assertIn((0x1005, 0x1003), snapshot.callers) + self.assertIn("H'1005<-H'1003", snapshot.line()) + self.assertTrue(any("recent_watch_snapshots:" == line for line in report.lines())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_timers.py b/tests/test_emulator_timers.py new file mode 100644 index 0000000..544aaba --- /dev/null +++ b/tests/test_emulator_timers.py @@ -0,0 +1,78 @@ +import unittest + +from h8536.emulator import H8536Emulator, ON_CHIP_RAM_START +from h8536.emulator.constants import ( + FRT_TCR_OCIEA, + FRT_TCSR_OCFA, + FRT2_TCR, + FRT2_TCSR, + IPRC, + VECTOR_FRT2_OCIA, +) + + +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 + + +def write_mov_b_abs_imm(rom: bytearray, address: int, target: int, value: int) -> int: + rom[address : address + 5] = bytes([0x15, (target >> 8) & 0xFF, target & 0xFF, 0x06, value & 0xFF]) + return address + 5 + + +class Frt2OciaTimerTest(unittest.TestCase): + 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") + + 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'06 makes + # only the FRT2 priority field high enough to pass interrupt mask 0. + 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), frt2_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(FRT2_TCSR) & FRT_TCSR_OCFA) + self.assertEqual(emulator.cpu.pc, isr + 4) + + 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") + + 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) + 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), frt2_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(FRT2_TCSR) & FRT_TCSR_OCFA, 0) + self.assertEqual(emulator.cpu.pc, pc) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_p9_bus.py b/tests/test_p9_bus.py new file mode 100644 index 0000000..ca47913 --- /dev/null +++ b/tests/test_p9_bus.py @@ -0,0 +1,42 @@ +import unittest + +from h8536.emulator import MemoryMap, P9DDR, P9DR +from h8536.emulator.peripherals import P9Bus + + +class P9BusTest(unittest.TestCase): + def test_bit7_input_uses_queued_then_default_low_response(self): + memory = MemoryMap(b"\x00" * 4) + memory.write8(P9DDR, 0x13) + memory.write8(P9DR, 0x80) + memory.p9_bus.queue_input_bits([1]) + + self.assertEqual(memory.read8(P9DDR), 0x13) + self.assertEqual(memory.read8(P9DR) & 0x80, 0x80) + self.assertEqual(memory.read8(P9DR) & 0x80, 0x00) + self.assertEqual(memory.registers[P9DR - 0xFE80], 0x80) + + def test_bit7_output_reads_latch(self): + memory = MemoryMap(b"\x00" * 4) + memory.write8(P9DDR, 0x93) + memory.write8(P9DR, 0x80) + + self.assertEqual(memory.read8(P9DDR), 0x93) + self.assertEqual(memory.read8(P9DR) & 0x80, 0x80) + self.assertEqual(memory.registers[P9DR - 0xFE80], 0x80) + + def test_strobe_rising_edges_capture_output_bits_and_byte_candidates(self): + bus = P9Bus() + bus.write_ddr(0x93) + for bit in (1, 0, 1, 0, 0, 1, 0, 1): + bus.write_dr(0x80 if bit else 0x00) + bus.write_dr((0x80 if bit else 0x00) | 0x02) + bus.write_dr(0x80 if bit else 0x00) + + self.assertEqual(bus.transmitted_bits, [1, 0, 1, 0, 0, 1, 0, 1]) + self.assertEqual(bus.byte_candidates, [0xA5]) + self.assertEqual([event.edge for event in bus.strobe_edges[:2]], ["rising", "falling"]) + + +if __name__ == "__main__": + unittest.main()