EMualtor im
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
124
h8536/emulator/fast_paths.py
Normal file
124
h8536/emulator/fast_paths.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
260
h8536/emulator/probe.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user