From 81f5d7a150967ebf6b75d740da347da937c24efb Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 18:07:55 +1000 Subject: [PATCH] emulator improvements --- README.md | 85 +++++ h8536/emulator/__init__.py | 83 +++++ h8536/emulator/__main__.py | 7 + h8536/emulator/cli.py | 66 ++++ h8536/emulator/constants.py | 36 ++ h8536/emulator/cpu.py | 34 ++ h8536/emulator/errors.py | 16 + h8536/emulator/memory.py | 120 +++++++ h8536/emulator/peripherals/__init__.py | 10 + h8536/emulator/peripherals/lcd.py | 5 + h8536/emulator/peripherals/p9_bus.py | 4 + h8536/{emulator.py => emulator/runner.py} | 419 +++------------------- h8536/emulator/sci.py | 110 ++++++ 13 files changed, 629 insertions(+), 366 deletions(-) create mode 100644 h8536/emulator/__init__.py create mode 100644 h8536/emulator/__main__.py create mode 100644 h8536/emulator/cli.py create mode 100644 h8536/emulator/constants.py create mode 100644 h8536/emulator/cpu.py create mode 100644 h8536/emulator/errors.py create mode 100644 h8536/emulator/memory.py create mode 100644 h8536/emulator/peripherals/__init__.py create mode 100644 h8536/emulator/peripherals/lcd.py create mode 100644 h8536/emulator/peripherals/p9_bus.py rename h8536/{emulator.py => emulator/runner.py} (60%) create mode 100644 h8536/emulator/sci.py diff --git a/README.md b/README.md index 293bad7..6d5f903 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,21 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction .\.venv\Scripts\python.exe h8536_serial_pseudocode.py build\rom_decompiled.json --out build\rom_serial_pseudocode.c ``` +To run the newer sidecar protocol and gate/queue analysis tools: + +```powershell +.\.venv\Scripts\python.exe h8536_serial_gate.py build\rom_decompiled.json --out build\rom_serial_gate.txt +.\.venv\Scripts\python.exe h8536_report_source_trace.py build\rom_decompiled.json --out build\rom_report_sources.txt +.\.venv\Scripts\python.exe h8536_table_xrefs.py --out build\rom_table_xrefs.txt +.\.venv\Scripts\python.exe h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt +``` + +To start the current emulator harness: + +```powershell +.\.venv\Scripts\python.exe h8536_emulator.py --max-steps 1000000 --stop-on-heartbeat --interval-steps 512 +``` + ## What It Does - Decodes the H8/500 instruction set used by the H8/536. @@ -43,6 +58,11 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction - Reconstructs evidence-supported SCI1 serial frame candidates, including the apparent six-byte TX/RX units and XOR checksum seeded by `0x5A`. - Infers candidate serial protocol semantics from validated frames, including `RX[0] & 0x07` command dispatch, likely index/value byte roles, and response staging through `F850-F854`. - Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates. +- Decodes observed serial byte captures into six-byte frames, validates checksums, labels capture-observed heartbeat/call/camera-power candidates, and summarizes heartbeat cadence. +- Accepts both analyzer-style lines such as `RX 006 bytes ...` and the idle reference `frame 006 ...` format in `ROM/rcp-txd-idle-only.txt`. +- Reconstructs the autonomous serial gate/queue state-machine around `loc_3FD3`, `loc_BAF2`, `F9B0/F9B5`, `FAA2/FAA3/FAA5`, and the resend path through `BE9E/BED5`. +- Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations. +- Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations. - Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver. - Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers. - Scans unreached ROM ranges for ASCII strings and pointer-table candidates. @@ -56,6 +76,15 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction - 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. + +Current serial observations: + +- Idle capture reference: `ROM/rcp-txd-idle-only.txt`. +- Idle frame: `00 00 00 00 80 DA`. +- 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. +- 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: @@ -69,6 +98,17 @@ The optional JSON output is useful for scripts or later analysis: build/rom_decompiled.json ``` +Common derived outputs: + +```text +build/rom_pseudocode.c +build/rom_serial_pseudocode.c +build/rom_serial_gate.txt +build/rom_report_sources.txt +build/rom_table_xrefs.txt +build/callgraph.dot +``` + ## Useful Options ```powershell @@ -111,6 +151,42 @@ python h8536_serial_pseudocode.py --help - `--no-board`: omit board/MAX202 comments. - `--no-semantics`: omit candidate command/field semantics. +For protocol trace and capture logs: + +```powershell +python h8536_protocol_trace.py --help +python h8536_protocol_capture.py --help +``` + +- `h8536_protocol_trace.py --direction tx 00 00 15 80 00 CF`: decode raw bytes as protocol frames. +- `h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt`: parse timestamped captures, recombine split chunks, validate checksums, and summarize cadence/gate hints. +- `--json` on the capture tool emits machine-readable frame and cadence data. + +For gate/queue and table reports: + +```powershell +python h8536_serial_gate.py --help +python h8536_report_source_trace.py --help +python h8536_table_xrefs.py --help +``` + +- `h8536_serial_gate.py`: reports the autonomous TX gate and report queue evidence. +- `h8536_report_source_trace.py`: traces direct `loc_3E54` report enqueue sources. Current finding: no direct static `R3 = 0x0007` enqueue in the JSON, so CAM power `0x0007` remains runtime/capture-observed unless a later indirect/table path proves it. +- `h8536_table_xrefs.py`: emits candidate table/index xrefs and LCD text correlation hints. + +For the emulator harness: + +```powershell +python h8536_emulator.py --help +``` + +- `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`. +- `--max-steps N`: bound execution. +- `--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. + ## Code Layout - `h8536_decompiler.py`: compatibility wrapper for the CLI. @@ -134,6 +210,12 @@ python h8536_serial_pseudocode.py --help - `h8536/serial_reconstruction.py`: cautious higher-level SCI frame reconstruction from decompiled evidence. - `h8536/serial_semantics.py`: candidate command/field semantics inferred from serial frame use. - `h8536/serial_pseudocode.py`: focused RX/TX protocol pseudocode generation from reconstruction metadata. +- `h8536/protocol_trace.py`: raw six-byte protocol frame decoder/checksum validator. +- `h8536/protocol_capture.py`: timestamped serial capture parser, frame recombiner, and cadence/gate-session analyzer. +- `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/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. @@ -141,3 +223,6 @@ python h8536_serial_pseudocode.py --help - `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers. - `h8536_pseudocode.py`: pseudocode CLI wrapper. - `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. diff --git a/h8536/emulator/__init__.py b/h8536/emulator/__init__.py new file mode 100644 index 0000000..d558f9f --- /dev/null +++ b/h8536/emulator/__init__.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from .cli import build_arg_parser, discover_rom_path, load_rom, main +from .constants import ( + HEARTBEAT_FRAME, + IPRA, + IPRE, + ON_CHIP_RAM_END, + ON_CHIP_RAM_START, + P9DDR, + P9DR, + RAMCR, + REGISTER_FIELD_END, + REGISTER_FIELD_START, + SCI_SCR_RE, + SCI_SCR_RIE, + SCI_SCR_TE, + SCI_SCR_TIE, + SCI_SSR_FER, + SCI_SSR_ORER, + SCI_SSR_PER, + SCI_SSR_RDRF, + SCI_SSR_TDRE, + SCI1_BRR, + SCI1_RDR, + SCI1_SCR, + SCI1_SMR, + SCI1_SSR, + SCI1_TDR, + VECTOR_INTERVAL_TIMER, + VECTOR_SCI1_TXI, + WDT_TCSR_R, +) +from .cpu import CPUState +from .errors import EmulatorError, UnsupportedInstruction +from .memory import MemoryAccess, MemoryMap, describe_regions +from .runner import H8536Emulator, RunReport +from .sci import SCI1, SciTxEvent + +__all__ = [ + "CPUState", + "EmulatorError", + "HEARTBEAT_FRAME", + "H8536Emulator", + "IPRA", + "IPRE", + "MemoryAccess", + "MemoryMap", + "ON_CHIP_RAM_END", + "ON_CHIP_RAM_START", + "P9DDR", + "P9DR", + "RAMCR", + "REGISTER_FIELD_END", + "REGISTER_FIELD_START", + "RunReport", + "SCI1", + "SCI1_BRR", + "SCI1_RDR", + "SCI1_SCR", + "SCI1_SMR", + "SCI1_SSR", + "SCI1_TDR", + "SCI_SCR_RE", + "SCI_SCR_RIE", + "SCI_SCR_TE", + "SCI_SCR_TIE", + "SCI_SSR_FER", + "SCI_SSR_ORER", + "SCI_SSR_PER", + "SCI_SSR_RDRF", + "SCI_SSR_TDRE", + "SciTxEvent", + "UnsupportedInstruction", + "VECTOR_INTERVAL_TIMER", + "VECTOR_SCI1_TXI", + "WDT_TCSR_R", + "build_arg_parser", + "describe_regions", + "discover_rom_path", + "load_rom", + "main", +] diff --git a/h8536/emulator/__main__.py b/h8536/emulator/__main__.py new file mode 100644 index 0000000..5417fda --- /dev/null +++ b/h8536/emulator/__main__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/h8536/emulator/cli.py b/h8536/emulator/cli.py new file mode 100644 index 0000000..cb956f9 --- /dev/null +++ b/h8536/emulator/cli.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from ..formatting import h16 +from .memory import describe_regions +from .runner import H8536Emulator + + +def discover_rom_path(root: Path) -> Path | None: + candidates = [ + root / "ROM" / "M27C512@DIP28_1.BIN", + root / "rom.bin", + ] + candidates.extend(sorted((root / "ROM").glob("*.BIN")) if (root / "ROM").exists() else []) + candidates.extend(sorted((root / "ROM").glob("*.bin")) if (root / "ROM").exists() else []) + for candidate in candidates: + if candidate.is_file(): + return candidate + return None + + +def load_rom(path: Path | None = None, root: Path | None = None) -> tuple[bytes, Path]: + root = root if root is not None else Path.cwd() + rom_path = path if path is not None else discover_rom_path(root) + if rom_path is None: + raise FileNotFoundError( + "could not discover ROM bytes; pass --rom PATH, expected ROM/M27C512@DIP28_1.BIN or another ROM/*.BIN" + ) + return rom_path.read_bytes(), rom_path + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Minimal H8/536 emulation harness scaffold") + parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present") + parser.add_argument("--max-steps", type=int, default=64, help="maximum CPU steps to execute") + parser.add_argument("--trace", action="store_true", help="print decoded/executed instruction trace") + 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") + 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 + + emulator = H8536Emulator(rom_bytes, interval_steps=args.interval_steps) + print(f"rom={rom_path}") + print(f"reset_vector={h16(emulator.reset_vector())}") + if args.memory_map: + print(describe_regions()) + report = emulator.run(args.max_steps, trace=args.trace, stop_on_heartbeat=args.stop_on_heartbeat) + if args.trace: + for line in report.trace: + print(line) + for line in report.summary_lines(): + print(line) + if not report.heartbeat_seen: + print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR") + return 0 diff --git a/h8536/emulator/constants.py b/h8536/emulator/constants.py new file mode 100644 index 0000000..0d81a01 --- /dev/null +++ b/h8536/emulator/constants.py @@ -0,0 +1,36 @@ +from __future__ import annotations + + +SCI1_SMR = 0xFED8 +SCI1_BRR = 0xFED9 +SCI1_SCR = 0xFEDA +SCI1_TDR = 0xFEDB +SCI1_SSR = 0xFEDC +SCI1_RDR = 0xFEDD + +P9DDR = 0xFEFE +P9DR = 0xFEFF + +IPRA = 0xFF00 +IPRE = 0xFF04 +WDT_TCSR_R = 0xFEEC + +SCI_SCR_TIE = 0x80 +SCI_SCR_RIE = 0x40 +SCI_SCR_TE = 0x20 +SCI_SCR_RE = 0x10 +SCI_SSR_TDRE = 0x80 +SCI_SSR_RDRF = 0x40 +SCI_SSR_ORER = 0x20 +SCI_SSR_FER = 0x10 +SCI_SSR_PER = 0x08 + +ON_CHIP_RAM_START = 0xF680 +ON_CHIP_RAM_END = 0xFE7F +REGISTER_FIELD_START = 0xFE80 +REGISTER_FIELD_END = 0xFFFF +RAMCR = 0xFF11 + +HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) +VECTOR_INTERVAL_TIMER = 0x0042 +VECTOR_SCI1_TXI = 0x0084 diff --git a/h8536/emulator/cpu.py b/h8536/emulator/cpu.py new file mode 100644 index 0000000..f960ae6 --- /dev/null +++ b/h8536/emulator/cpu.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class CPUState: + pc: int = 0 + sr: int = 0 + br: int = 0 + regs: list[int] = field(default_factory=lambda: [0] * 8) + cycles: int = 0 + steps: int = 0 + z: bool = False + c: bool = False + n: bool = False + v: bool = False + interrupt_depth: int = 0 + + +def s8(value: int) -> int: + return value - 0x100 if value & 0x80 else value + + +def s16(value: int) -> int: + return value - 0x10000 if value & 0x8000 else value + + +def mask(size: int) -> int: + return 0xFFFF if size == 2 else 0xFF + + +def sign_bit(size: int) -> int: + return 0x8000 if size == 2 else 0x80 diff --git a/h8536/emulator/errors.py b/h8536/emulator/errors.py new file mode 100644 index 0000000..320510a --- /dev/null +++ b/h8536/emulator/errors.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from ..formatting import h16 + + +class EmulatorError(Exception): + pass + + +class UnsupportedInstruction(EmulatorError): + def __init__(self, pc: int, raw: bytes, text: str) -> None: + raw_text = " ".join(f"{byte:02X}" for byte in raw) + super().__init__(f"unsupported instruction at {h16(pc)}: {raw_text} {text}".rstrip()) + self.pc = pc + self.raw = raw + self.text = text diff --git a/h8536/emulator/memory.py b/h8536/emulator/memory.py new file mode 100644 index 0000000..19798f3 --- /dev/null +++ b/h8536/emulator/memory.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable + +from ..formatting import h16 +from ..memory import MEMORY_REGIONS, MemoryRegion, region_for +from ..rom import Rom +from .constants import ( + ON_CHIP_RAM_END, + ON_CHIP_RAM_START, + P9DDR, + P9DR, + REGISTER_FIELD_END, + REGISTER_FIELD_START, + SCI1_BRR, + SCI1_RDR, + SCI1_SCR, + SCI1_SMR, + SCI1_SSR, + SCI1_TDR, +) +from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS +from .peripherals.p9_bus import P9_ACK_BIT +from .sci import SCI1 + + +@dataclass +class MemoryAccess: + address: int + size: int + value: int + kind: str + region: str + + +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.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] = {} + self.access_log: list[MemoryAccess] = [] + + self._set_register(SCI1_SMR, self.sci1.smr) + self._set_register(SCI1_BRR, self.sci1.brr) + self._set_register(SCI1_SCR, self.sci1.scr) + self._set_register(SCI1_TDR, self.sci1.tdr) + self._set_register(SCI1_SSR, self.sci1.ssr) + self._set_register(SCI1_RDR, self.sci1.rdr) + + def region(self, address: int) -> MemoryRegion: + return region_for(address & 0xFFFF) + + def read8(self, address: int) -> int: + address &= 0xFFFF + 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): + # 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 ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: + value = self.ram[address - ON_CHIP_RAM_START] + 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: + value = self.external.get(address, 0xFF) + self._log("read", address, 1, value) + return value + + def read16(self, address: int) -> int: + high = self.read8(address) + low = self.read8((address + 1) & 0xFFFF) + return (high << 8) | low + + def write8(self, address: int, value: int) -> None: + address &= 0xFFFF + value &= 0xFF + if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): + self.sci1.write(address, value) + 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 REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: + self._set_register(address, value) + elif self.rom.contains(address): + # The ROM image spans the whole address space, but the H8/536 map + # can place external RAM/peripherals in these ranges. Keep writes + # as external overrides while leaving instruction fetch immutable. + self.external[address] = value + else: + self.external[address] = value + self._log("write", address, 1, value) + + def write16(self, address: int, value: int) -> None: + self.write8(address, (value >> 8) & 0xFF) + self.write8((address + 1) & 0xFFFF, value & 0xFF) + + def _set_register(self, address: int, value: int) -> None: + self.registers[address - REGISTER_FIELD_START] = value & 0xFF + + def _log(self, kind: str, address: int, size: int, value: int) -> None: + self.access_log.append(MemoryAccess(address, size, value, kind, self.region(address).name)) + + +def describe_regions(regions: Iterable[MemoryRegion] = MEMORY_REGIONS) -> str: + return "\n".join(f"{h16(region.start)}-{h16(region.end)} {region.name} {region.kind}" for region in regions) diff --git a/h8536/emulator/peripherals/__init__.py b/h8536/emulator/peripherals/__init__.py new file mode 100644 index 0000000..e69a9dc --- /dev/null +++ b/h8536/emulator/peripherals/__init__.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from .lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS +from .p9_bus import P9_ACK_BIT + +__all__ = [ + "LCD_E_CLOCK_DATA", + "LCD_E_CLOCK_STATUS", + "P9_ACK_BIT", +] diff --git a/h8536/emulator/peripherals/lcd.py b/h8536/emulator/peripherals/lcd.py new file mode 100644 index 0000000..86e6239 --- /dev/null +++ b/h8536/emulator/peripherals/lcd.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +LCD_E_CLOCK_DATA = 0xF200 +LCD_E_CLOCK_STATUS = 0xF201 diff --git a/h8536/emulator/peripherals/p9_bus.py b/h8536/emulator/peripherals/p9_bus.py new file mode 100644 index 0000000..335bede --- /dev/null +++ b/h8536/emulator/peripherals/p9_bus.py @@ -0,0 +1,4 @@ +from __future__ import annotations + + +P9_ACK_BIT = 0x80 diff --git a/h8536/emulator.py b/h8536/emulator/runner.py similarity index 60% rename from h8536/emulator.py rename to h8536/emulator/runner.py index c2f517b..de15442 100644 --- a/h8536/emulator.py +++ b/h8536/emulator/runner.py @@ -1,260 +1,23 @@ from __future__ import annotations -import argparse from dataclasses import dataclass, field -from pathlib import Path -from typing import Iterable -from .decoder import H8536Decoder -from .formatting import h8, h16 -from .memory import MEMORY_REGIONS, MemoryRegion, region_for -from .rom import DecodeError, Rom -from .vectors import read_vectors_min - - -SCI1_SMR = 0xFED8 -SCI1_BRR = 0xFED9 -SCI1_SCR = 0xFEDA -SCI1_TDR = 0xFEDB -SCI1_SSR = 0xFEDC -SCI1_RDR = 0xFEDD -P9DDR = 0xFEFE -P9DR = 0xFEFF -IPRA = 0xFF00 -IPRE = 0xFF04 -WDT_TCSR_R = 0xFEEC - -SCI_SCR_TIE = 0x80 -SCI_SCR_RIE = 0x40 -SCI_SCR_TE = 0x20 -SCI_SCR_RE = 0x10 -SCI_SSR_TDRE = 0x80 -SCI_SSR_RDRF = 0x40 -SCI_SSR_ORER = 0x20 -SCI_SSR_FER = 0x10 -SCI_SSR_PER = 0x08 - -ON_CHIP_RAM_START = 0xF680 -ON_CHIP_RAM_END = 0xFE7F -REGISTER_FIELD_START = 0xFE80 -REGISTER_FIELD_END = 0xFFFF -RAMCR = 0xFF11 -HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) -VECTOR_INTERVAL_TIMER = 0x0042 -VECTOR_SCI1_TXI = 0x0084 - - -class EmulatorError(Exception): - pass - - -class UnsupportedInstruction(EmulatorError): - def __init__(self, pc: int, raw: bytes, text: str) -> None: - raw_text = " ".join(f"{byte:02X}" for byte in raw) - super().__init__(f"unsupported instruction at {h16(pc)}: {raw_text} {text}".rstrip()) - self.pc = pc - self.raw = raw - self.text = text - - -@dataclass -class SciTxEvent: - address: int - value: int - scr: int - ssr: int - emitted: bool - - -@dataclass -class SCI1: - """Small SCI1 model for the H8/536 serial path. - - Manual anchors: - - RDR/TDR/SCR/SSR live at H'FEDD/H'FEDB/H'FEDA/H'FEDC for 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. - """ - - smr: int = 0x00 - brr: int = 0xFF - scr: int = 0x0C - tdr: int = 0xFF - ssr: int = 0x87 - rdr: int = 0x00 - tx_bytes: list[int] = field(default_factory=list) - tx_events: list[SciTxEvent] = field(default_factory=list) - tx_frames: list[bytes] = field(default_factory=list) - _frame_buffer: bytearray = field(default_factory=bytearray) - tx_ready_delay: int = 0 - - def read(self, address: int) -> int: - if address == SCI1_SMR: - return self.smr - if address == SCI1_BRR: - return self.brr - if address == SCI1_SCR: - return self.scr - if address == SCI1_TDR: - return self.tdr - if address == SCI1_SSR: - return self.ssr - if address == SCI1_RDR: - return self.rdr - raise KeyError(address) - - def write(self, address: int, value: int) -> None: - value &= 0xFF - if address == SCI1_SMR: - self.smr = value - elif address == SCI1_BRR: - self.brr = value - elif address == SCI1_SCR: - self.scr = value - elif address == SCI1_TDR: - 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 - elif address == SCI1_RDR: - self.rdr = value - else: - raise KeyError(address) - - def _write_tdr(self, value: int) -> None: - emitted = bool(self.scr & SCI_SCR_TE) - if emitted: - self.tx_bytes.append(value) - self._frame_buffer.append(value) - 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_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) - - def inject_rx(self, value: int) -> None: - self.rdr = value & 0xFF - self.ssr |= SCI_SSR_RDRF - - def saw_heartbeat(self) -> bool: - return HEARTBEAT_FRAME in self.tx_frames - - def tick(self) -> None: - if self.tx_ready_delay: - self.tx_ready_delay -= 1 - if self.tx_ready_delay == 0: - self.ssr |= SCI_SSR_TDRE - - -@dataclass -class MemoryAccess: - address: int - size: int - value: int - kind: str - region: str - - -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.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] = {} - self.access_log: list[MemoryAccess] = [] - - self._set_register(SCI1_SMR, self.sci1.smr) - self._set_register(SCI1_BRR, self.sci1.brr) - self._set_register(SCI1_SCR, self.sci1.scr) - self._set_register(SCI1_TDR, self.sci1.tdr) - self._set_register(SCI1_SSR, self.sci1.ssr) - self._set_register(SCI1_RDR, self.sci1.rdr) - - def region(self, address: int) -> MemoryRegion: - return region_for(address & 0xFFFF) - - def read8(self, address: int) -> int: - address &= 0xFFFF - 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 (0xF200, 0xF201): - # 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 ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: - value = self.ram[address - ON_CHIP_RAM_START] - 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 &= 0x7F - elif self.rom.contains(address): - value = self.rom.u8(address) - else: - value = self.external.get(address, 0xFF) - self._log("read", address, 1, value) - return value - - def read16(self, address: int) -> int: - high = self.read8(address) - low = self.read8((address + 1) & 0xFFFF) - return (high << 8) | low - - def write8(self, address: int, value: int) -> None: - address &= 0xFFFF - value &= 0xFF - if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): - self.sci1.write(address, value) - 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 REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: - self._set_register(address, value) - elif self.rom.contains(address): - # The ROM image spans the whole address space, but the H8/536 map - # can place external RAM/peripherals in these ranges. Keep writes - # as external overrides while leaving instruction fetch immutable. - self.external[address] = value - else: - self.external[address] = value - self._log("write", address, 1, value) - - def write16(self, address: int, value: int) -> None: - self.write8(address, (value >> 8) & 0xFF) - self.write8((address + 1) & 0xFFFF, value & 0xFF) - - def _set_register(self, address: int, value: int) -> None: - self.registers[address - REGISTER_FIELD_START] = value & 0xFF - - def _log(self, kind: str, address: int, size: int, value: int) -> None: - self.access_log.append(MemoryAccess(address, size, value, kind, self.region(address).name)) - - -@dataclass -class CPUState: - pc: int = 0 - sr: int = 0 - br: int = 0 - regs: list[int] = field(default_factory=lambda: [0] * 8) - cycles: int = 0 - steps: int = 0 - z: bool = False - c: bool = False - n: bool = False - v: bool = False - interrupt_depth: int = 0 +from ..decoder import H8536Decoder +from ..formatting import h16 +from ..rom import DecodeError +from ..vectors import read_vectors_min +from .constants import ( + IPRA, + IPRE, + SCI_SCR_TIE, + SCI_SSR_TDRE, + VECTOR_INTERVAL_TIMER, + VECTOR_SCI1_TXI, +) +from .cpu import CPUState, mask, s8, s16, sign_bit +from .errors import EmulatorError, UnsupportedInstruction +from .memory import MemoryMap +from .sci import SCI1 @dataclass @@ -281,7 +44,9 @@ class RunReport: ] if self.unsupported: lines.append(f"unsupported={self.unsupported}") - lines.append("next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow") + lines.append( + "next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow" + ) return lines @@ -390,7 +155,7 @@ class H8536Emulator: def _execute_general(self, pc: int, next_pc: int) -> int: ea = self._decode_ea(pc) - op = self.memory.rom.u8(pc + ea["length"]) + op = self.memory.rom.u8(pc + int(ea["length"])) size = int(ea["size"]) raw = self.memory.rom.slice(pc, self._general_length(pc, ea, op)) @@ -481,8 +246,8 @@ class H8536Emulator: self._set_logic_flags(value, size) elif op == 0x1A: value = self._read_ea(ea, size) - result = (value << 1) & _mask(size) - self.cpu.c = bool(value & _sign_bit(size)) + result = (value << 1) & mask(size) + self.cpu.c = bool(value & sign_bit(size)) self._write_ea(ea, result, size) self._set_logic_flags(result, size) elif op == 0x1B: @@ -522,7 +287,7 @@ class H8536Emulator: def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int: cond = raw[0] & 0x0F - disp = _s8(raw[1]) + disp = s8(raw[1]) target = (pc + 2 + disp) & 0xFFFF if self._branch_condition(cond): return target @@ -530,7 +295,7 @@ class H8536Emulator: def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int: cond = raw[0] & 0x0F - disp = _s16(int.from_bytes(raw[1:3], "big")) + disp = s16(int.from_bytes(raw[1:3], "big")) target = (pc + 3 + disp) & 0xFFFF if self._branch_condition(cond): return target @@ -550,9 +315,9 @@ class H8536Emulator: def _direct_call(self, raw: bytes, next_pc: int) -> int: self._push16(next_pc) if raw[0] == 0x0E: - return (next_pc + _s8(raw[1])) & 0xFFFF + return (next_pc + s8(raw[1])) & 0xFFFF if raw[0] == 0x1E: - return (next_pc + _s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF + return (next_pc + s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF return int.from_bytes(raw[1:3], "big") def _scb(self, raw: bytes, pc: int, next_pc: int) -> int: @@ -564,7 +329,7 @@ class H8536Emulator: self.cpu.regs[reg] = value self.cpu.z = value == 0 if value != 0: - return (pc + 3 + _s8(raw[2])) & 0xFFFF + return (pc + 3 + s8(raw[2])) & 0xFFFF return next_pc def _tick_peripherals(self) -> None: @@ -631,14 +396,14 @@ class H8536Emulator: self.cpu.regs[7] = (sp + 2) & 0xFFFF return value - def _push_register_mask(self, mask: int) -> None: + def _push_register_mask(self, mask_value: int) -> None: for reg in range(8): - if mask & (1 << reg): + if mask_value & (1 << reg): self._push16(self.cpu.regs[reg]) - def _pop_register_mask(self, mask: int) -> None: + def _pop_register_mask(self, mask_value: int) -> None: for reg in reversed(range(8)): - if mask & (1 << reg): + if mask_value & (1 << reg): self.cpu.regs[reg] = self._pop16() def _decode_ea(self, pc: int) -> dict[str, int | str | None]: @@ -652,9 +417,9 @@ class H8536Emulator: if 0xD0 <= first <= 0xDF: return {"mode": "indirect", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None} if 0xE0 <= first <= 0xEF: - return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 2, "value": _s8(self.memory.rom.u8(pc + 1))} + return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 2, "value": s8(self.memory.rom.u8(pc + 1))} if 0xF0 <= first <= 0xFF: - return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 3, "value": _s16(self.memory.rom.u16(pc + 1))} + return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 3, "value": s16(self.memory.rom.u16(pc + 1))} if first in (0x04, 0x0C): size = 2 if first == 0x0C else 1 value = self.memory.rom.u16(pc + 1) if size == 2 else self.memory.rom.u8(pc + 1) @@ -697,7 +462,7 @@ class H8536Emulator: def _read_ea(self, ea: dict[str, int | str | None], size: int) -> int: mode = ea["mode"] if mode == "imm": - return int(ea["value"]) & _mask(size) + return int(ea["value"]) & mask(size) if mode == "reg": return self._reg_read(int(ea["reg"]), size) address = self._ea_address(ea, size) @@ -727,28 +492,28 @@ class H8536Emulator: self._set_sub_flags(lhs, rhs, lhs - rhs, size) def _set_logic_flags(self, value: int, size: int) -> None: - value &= _mask(size) + value &= mask(size) self.cpu.z = value == 0 - self.cpu.n = bool(value & _sign_bit(size)) + self.cpu.n = bool(value & sign_bit(size)) self.cpu.v = False def _set_add_flags(self, lhs: int, rhs: int, result: int, size: int) -> None: - mask = _mask(size) - sign = _sign_bit(size) - result &= mask - lhs &= mask - rhs &= mask + max_value = mask(size) + sign = sign_bit(size) + result &= max_value + lhs &= max_value + rhs &= max_value self.cpu.z = result == 0 self.cpu.n = bool(result & sign) - self.cpu.c = lhs + rhs > mask + self.cpu.c = lhs + rhs > max_value self.cpu.v = bool((~(lhs ^ rhs) & (lhs ^ result) & sign) != 0) def _set_sub_flags(self, lhs: int, rhs: int, result: int, size: int) -> None: - mask = _mask(size) - sign = _sign_bit(size) - result &= mask - lhs &= mask - rhs &= mask + max_value = mask(size) + sign = sign_bit(size) + result &= max_value + lhs &= max_value + rhs &= max_value self.cpu.z = result == 0 self.cpu.n = bool(result & sign) self.cpu.c = lhs < rhs @@ -756,15 +521,15 @@ class H8536Emulator: def _bit_operation(self, ea: dict[str, int | str | None], size: int, op_base: int, bit: int) -> None: value = self._read_ea(ea, size) - bit &= (15 if size == 2 else 7) - mask = 1 << bit - self.cpu.z = not bool(value & mask) + bit &= 15 if size == 2 else 7 + bit_mask = 1 << bit + self.cpu.z = not bool(value & bit_mask) if op_base == 0xC0: - self._write_ea(ea, value | mask, size) + self._write_ea(ea, value | bit_mask, size) elif op_base == 0xD0: - self._write_ea(ea, value & ~mask, size) + self._write_ea(ea, value & ~bit_mask, size) elif op_base == 0xE0: - self._write_ea(ea, value ^ mask, size) + self._write_ea(ea, value ^ bit_mask, size) def _branch_condition(self, cond: int) -> bool: if cond == 0x0: @@ -798,81 +563,3 @@ 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 _s8(value: int) -> int: - return value - 0x100 if value & 0x80 else value - - -def _s16(value: int) -> int: - return value - 0x10000 if value & 0x8000 else value - - -def _mask(size: int) -> int: - return 0xFFFF if size == 2 else 0xFF - - -def _sign_bit(size: int) -> int: - return 0x8000 if size == 2 else 0x80 - - -def discover_rom_path(root: Path) -> Path | None: - candidates = [ - root / "ROM" / "M27C512@DIP28_1.BIN", - root / "rom.bin", - ] - candidates.extend(sorted((root / "ROM").glob("*.BIN")) if (root / "ROM").exists() else []) - candidates.extend(sorted((root / "ROM").glob("*.bin")) if (root / "ROM").exists() else []) - for candidate in candidates: - if candidate.is_file(): - return candidate - return None - - -def load_rom(path: Path | None = None, root: Path | None = None) -> tuple[bytes, Path]: - root = root if root is not None else Path.cwd() - rom_path = path if path is not None else discover_rom_path(root) - if rom_path is None: - raise FileNotFoundError( - "could not discover ROM bytes; pass --rom PATH, expected ROM/M27C512@DIP28_1.BIN or another ROM/*.BIN" - ) - return rom_path.read_bytes(), rom_path - - -def describe_regions(regions: Iterable[MemoryRegion] = MEMORY_REGIONS) -> str: - return "\n".join(f"{h16(region.start)}-{h16(region.end)} {region.name} {region.kind}" for region in regions) - - -def build_arg_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Minimal H8/536 emulation harness scaffold") - parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present") - parser.add_argument("--max-steps", type=int, default=64, help="maximum CPU steps to execute") - parser.add_argument("--trace", action="store_true", help="print decoded/executed instruction trace") - 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") - 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 - - emulator = H8536Emulator(rom_bytes, interval_steps=args.interval_steps) - print(f"rom={rom_path}") - print(f"reset_vector={h16(emulator.reset_vector())}") - if args.memory_map: - print(describe_regions()) - report = emulator.run(args.max_steps, trace=args.trace, stop_on_heartbeat=args.stop_on_heartbeat) - if args.trace: - for line in report.trace: - print(line) - for line in report.summary_lines(): - print(line) - if not report.heartbeat_seen: - print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR") - return 0 diff --git a/h8536/emulator/sci.py b/h8536/emulator/sci.py new file mode 100644 index 0000000..4bb1504 --- /dev/null +++ b/h8536/emulator/sci.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from .constants import ( + HEARTBEAT_FRAME, + SCI1_BRR, + SCI1_RDR, + SCI1_SCR, + SCI1_SMR, + SCI1_SSR, + SCI1_TDR, + SCI_SCR_TE, + SCI_SSR_RDRF, + SCI_SSR_TDRE, +) + + +@dataclass +class SciTxEvent: + address: int + value: int + scr: int + ssr: int + emitted: bool + + +@dataclass +class SCI1: + """Small SCI1 model for the H8/536 serial path. + + Manual anchors: + - RDR/TDR/SCR/SSR live at H'FEDD/H'FEDB/H'FEDA/H'FEDC for 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. + """ + + smr: int = 0x00 + brr: int = 0xFF + scr: int = 0x0C + tdr: int = 0xFF + ssr: int = 0x87 + rdr: int = 0x00 + tx_bytes: list[int] = field(default_factory=list) + tx_events: list[SciTxEvent] = field(default_factory=list) + tx_frames: list[bytes] = field(default_factory=list) + _frame_buffer: bytearray = field(default_factory=bytearray) + tx_ready_delay: int = 0 + + def read(self, address: int) -> int: + if address == SCI1_SMR: + return self.smr + if address == SCI1_BRR: + return self.brr + if address == SCI1_SCR: + return self.scr + if address == SCI1_TDR: + return self.tdr + if address == SCI1_SSR: + return self.ssr + if address == SCI1_RDR: + return self.rdr + raise KeyError(address) + + def write(self, address: int, value: int) -> None: + value &= 0xFF + if address == SCI1_SMR: + self.smr = value + elif address == SCI1_BRR: + self.brr = value + elif address == SCI1_SCR: + self.scr = value + elif address == SCI1_TDR: + 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 + elif address == SCI1_RDR: + self.rdr = value + else: + raise KeyError(address) + + def _write_tdr(self, value: int) -> None: + emitted = bool(self.scr & SCI_SCR_TE) + if emitted: + self.tx_bytes.append(value) + self._frame_buffer.append(value) + 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_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) + + def inject_rx(self, value: int) -> None: + self.rdr = value & 0xFF + self.ssr |= SCI_SSR_RDRF + + def saw_heartbeat(self) -> bool: + return HEARTBEAT_FRAME in self.tx_frames + + def tick(self) -> None: + if self.tx_ready_delay: + self.tx_ready_delay -= 1 + if self.tx_ready_delay == 0: + self.ssr |= SCI_SSR_TDRE