1
0

EMualtor im

This commit is contained in:
Aiden
2026-05-25 18:43:36 +10:00
parent 81f5d7a150
commit 05e1237acc
18 changed files with 993 additions and 20 deletions

View File

@@ -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",

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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)

260
h8536/emulator/probe.py Normal file
View File

@@ -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

View File

@@ -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)}: {'<p9-fast-path>':<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