388 lines
14 KiB
Python
388 lines
14 KiB
Python
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_BRR,
|
|
SCI1_RDR,
|
|
SCI1_SCR,
|
|
SCI1_SMR,
|
|
SCI1_SSR,
|
|
SCI1_TDR,
|
|
SCI_SCR_TE,
|
|
SCI_SCR_TIE,
|
|
SCI_SSR_TDRE,
|
|
VECTOR_SCI1_TXI,
|
|
)
|
|
from .errors import UnsupportedInstruction
|
|
from .runner import H8536Emulator
|
|
|
|
|
|
DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059)
|
|
SCI1_PROBE_REGISTERS = {
|
|
SCI1_SCR: "SCR",
|
|
SCI1_TDR: "TDR",
|
|
SCI1_SSR: "SSR",
|
|
SCI1_RDR: "RDR",
|
|
}
|
|
|
|
|
|
def _format_bytes(data: bytes, *, limit: int = 96) -> str:
|
|
if len(data) <= limit:
|
|
return data.hex(" ").upper()
|
|
head = data[: limit // 2].hex(" ").upper()
|
|
tail = data[-(limit // 2) :].hex(" ").upper()
|
|
return f"{head} ... {tail} ({len(data)} bytes)"
|
|
|
|
|
|
def _format_six_byte_frames(data: bytes, *, limit: int = 8) -> str:
|
|
frames = [data[index : index + 6] for index in range(0, len(data) - 5, 6)]
|
|
if not frames:
|
|
return ""
|
|
recent = frames[-limit:]
|
|
return " | ".join(frame.hex(" ").upper() for frame in recent)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SCI1Snapshot:
|
|
smr: int
|
|
brr: int
|
|
scr: int
|
|
ssr: int
|
|
tdr: int
|
|
rdr: int
|
|
tx_ready_delay: int | None = None
|
|
|
|
def line(self) -> str:
|
|
fields = [
|
|
f"SMR={self.smr:02X}",
|
|
f"BRR={self.brr:02X}",
|
|
f"SCR={self.scr:02X}",
|
|
f"SSR={self.ssr:02X}",
|
|
f"TDR={self.tdr:02X}",
|
|
f"RDR={self.rdr:02X}",
|
|
]
|
|
if self.tx_ready_delay is not None:
|
|
fields.append(f"tx_ready_delay={self.tx_ready_delay}")
|
|
return "sci1=" + " ".join(fields)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SCI1TXISummary:
|
|
tie: bool
|
|
te: bool
|
|
tdre: bool
|
|
vector_target: int | None
|
|
priority: int
|
|
interrupt_mask: int
|
|
interrupt_depth: int
|
|
|
|
def line(self) -> str:
|
|
pending = self.tie and self.tdre and self.vector_target is not None
|
|
serviceable = pending and self.interrupt_depth == 0 and self.priority > self.interrupt_mask
|
|
vector = h16(self.vector_target) if self.vector_target is not None else "none"
|
|
return (
|
|
"sci1_txi="
|
|
f"TIE={int(self.tie)} TE={int(self.te)} TDRE={int(self.tdre)} "
|
|
f"vector={vector} priority={self.priority} mask={self.interrupt_mask} "
|
|
f"depth={self.interrupt_depth} pending={int(pending)} serviceable={int(serviceable)}"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
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)
|
|
sci1: SCI1Snapshot | None = None
|
|
sci1_txi: SCI1TXISummary | None = None
|
|
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=" + _format_bytes(self.tx_bytes),
|
|
"tx_6byte_recent=" + _format_six_byte_frames(self.tx_bytes),
|
|
"p9_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_bytes[-32:]),
|
|
"p9_fast_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_fast_bytes[-32:]),
|
|
f"p9_fast_events={self.p9_fast_events}",
|
|
"hot_pcs=" + ", ".join(f"{h16(pc)}:{count}" for pc, count in self.hot_pcs.most_common(hot_limit)),
|
|
]
|
|
if self.sci1:
|
|
lines.append(self.sci1.line())
|
|
if self.sci1_txi:
|
|
lines.append(self.sci1_txi.line())
|
|
if self.unsupported:
|
|
lines.append(f"unsupported={self.unsupported}")
|
|
if self.p9_accesses:
|
|
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 _sci1_snapshot(emulator: H8536Emulator) -> SCI1Snapshot:
|
|
sci1 = emulator.sci1
|
|
return SCI1Snapshot(
|
|
smr=sci1.smr,
|
|
brr=sci1.brr,
|
|
scr=sci1.scr,
|
|
ssr=sci1.ssr,
|
|
tdr=sci1.tdr,
|
|
rdr=sci1.rdr,
|
|
tx_ready_delay=getattr(sci1, "tx_ready_delay", None),
|
|
)
|
|
|
|
|
|
def _sci1_txi_summary(emulator: H8536Emulator) -> SCI1TXISummary:
|
|
return SCI1TXISummary(
|
|
tie=bool(emulator.sci1.scr & SCI_SCR_TIE),
|
|
te=bool(emulator.sci1.scr & SCI_SCR_TE),
|
|
tdre=bool(emulator.sci1.ssr & SCI_SSR_TDRE),
|
|
vector_target=emulator._vector_target(VECTOR_SCI1_TXI),
|
|
priority=emulator._sci1_priority(),
|
|
interrupt_mask=emulator._interrupt_mask(),
|
|
interrupt_depth=emulator.cpu.interrupt_depth,
|
|
)
|
|
|
|
|
|
def parse_watch_pc(text: str) -> int:
|
|
try:
|
|
value = parse_int(text)
|
|
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,
|
|
frt1_ocia_steps: int = 1024,
|
|
frt2_ocia_steps: int = 1024,
|
|
p9_fast_path: bool = False,
|
|
p9_fast_input: int = 0xFF,
|
|
sci_log_limit: int = 32,
|
|
watch_pcs: list[int] | tuple[int, ...] | None = None,
|
|
watch_snapshot_limit: int = 32,
|
|
watch_pc_limit: int = 8,
|
|
watch_min_interval: int = 1024,
|
|
) -> ProbeReport:
|
|
emulator = H8536Emulator(
|
|
rom_bytes,
|
|
interval_steps=interval_steps,
|
|
frt1_ocia_steps=frt1_ocia_steps,
|
|
frt2_ocia_steps=frt2_ocia_steps,
|
|
p9_fast_path_enabled=p9_fast_path,
|
|
p9_fast_default_input_byte=p9_fast_input,
|
|
)
|
|
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} TDR={access.value:02X}")
|
|
elif access.address in SCI1_PROBE_REGISTERS:
|
|
name = SCI1_PROBE_REGISTERS[access.address]
|
|
sci_accesses.append(f"{h16(pc)} {access.kind} {name}={access.value:02X}")
|
|
if len(sci_accesses) > sci_log_limit:
|
|
del sci_accesses[: len(sci_accesses) - sci_log_limit]
|
|
last_access_index = len(emulator.memory.access_log)
|
|
|
|
if stop_on_tx and emulator.sci1.tx_bytes:
|
|
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,
|
|
sci1=_sci1_snapshot(emulator),
|
|
sci1_txi=_sci1_txi_summary(emulator),
|
|
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("--frt1-ocia-steps", type=int, default=1024)
|
|
parser.add_argument("--frt2-ocia-steps", type=int, default=1024)
|
|
parser.add_argument("--stop-on-tx", action="store_true", help="stop when SCI1 TDR emits the first byte")
|
|
parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration")
|
|
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF)
|
|
parser.add_argument("--p9-log-limit", type=int, default=80)
|
|
parser.add_argument("--sci-log-limit", type=int, default=32)
|
|
parser.add_argument("--hot-limit", type=int, default=12)
|
|
parser.add_argument(
|
|
"--watch-pc",
|
|
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,
|
|
frt1_ocia_steps=args.frt1_ocia_steps,
|
|
frt2_ocia_steps=args.frt2_ocia_steps,
|
|
stop_on_tx=args.stop_on_tx,
|
|
p9_log_limit=args.p9_log_limit,
|
|
p9_fast_path=args.p9_fast_path,
|
|
p9_fast_input=args.p9_fast_input,
|
|
sci_log_limit=args.sci_log_limit,
|
|
watch_pcs=tuple(dict.fromkeys((*DEFAULT_WATCH_PCS, *args.watch_pc))),
|
|
watch_snapshot_limit=args.watch_snapshot_limit,
|
|
watch_pc_limit=args.watch_pc_limit,
|
|
watch_min_interval=args.watch_min_interval,
|
|
)
|
|
for line in report.lines(hot_limit=args.hot_limit):
|
|
print(line)
|
|
return 0
|