1
0

Emualtor improements

This commit is contained in:
Aiden
2026-05-25 18:55:50 +10:00
parent 05e1237acc
commit 1fabf6587d
10 changed files with 413 additions and 15 deletions

View File

@@ -78,8 +78,8 @@ To start the current emulator harness:
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`. - 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. - 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. - 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, MOV condition-code updates, `SCB/F`, stack/call/return support, scaffolded SCI1 TXI/interval/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path. - Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, scaffolded SCI1 TXI/interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path.
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, and captured P9 byte candidates while running the real ROM. - Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
Current serial observations: Current serial observations:
@@ -189,8 +189,9 @@ python h8536_emulator_probe.py --help
- `--trace`: print executed instructions. - `--trace`: print executed instructions.
- `--stop-on-heartbeat`: stop only if `00 00 00 00 80 DA` is emitted through SCI1 TDR. - `--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. - `--interval-steps N`: tune the scaffolded interval timer cadence.
- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: tune rough FRT compare-interrupt cadence.
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration. - `--p9-fast-path`: shortcut known P9 transfer routines for exploration.
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT2 OCIA, but does not yet reach the SCI1 heartbeat. The next emulator work is validating the P9 fast-path semantics against real captures/board behavior and tightening SCI RX/TX interrupt cadence. - Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT1/FRT2 OCIA. The probe now reaches the SCI1 transmit path, but still needs more timing/peripheral fidelity before it emits the full observed heartbeat frame.
## Code Layout ## Code Layout

View File

@@ -5,6 +5,8 @@ from .constants import (
HEARTBEAT_FRAME, HEARTBEAT_FRAME,
FRT_TCR_OCIEA, FRT_TCR_OCIEA,
FRT_TCSR_OCFA, FRT_TCSR_OCFA,
FRT1_TCR,
FRT1_TCSR,
FRT2_TCR, FRT2_TCR,
FRT2_TCSR, FRT2_TCSR,
IPRA, IPRA,
@@ -32,6 +34,7 @@ from .constants import (
SCI1_SMR, SCI1_SMR,
SCI1_SSR, SCI1_SSR,
SCI1_TDR, SCI1_TDR,
VECTOR_FRT1_OCIA,
VECTOR_INTERVAL_TIMER, VECTOR_INTERVAL_TIMER,
VECTOR_FRT2_OCIA, VECTOR_FRT2_OCIA,
VECTOR_SCI1_TXI, VECTOR_SCI1_TXI,
@@ -47,6 +50,8 @@ from .sci import SCI1, SciTxEvent
__all__ = [ __all__ = [
"CPUState", "CPUState",
"EmulatorError", "EmulatorError",
"FRT1_TCR",
"FRT1_TCSR",
"FRT2_TCR", "FRT2_TCR",
"FRT2_TCSR", "FRT2_TCSR",
"FRT_TCR_OCIEA", "FRT_TCR_OCIEA",
@@ -87,6 +92,7 @@ __all__ = [
"SCI_SSR_TDRE", "SCI_SSR_TDRE",
"SciTxEvent", "SciTxEvent",
"UnsupportedInstruction", "UnsupportedInstruction",
"VECTOR_FRT1_OCIA",
"VECTOR_INTERVAL_TIMER", "VECTOR_INTERVAL_TIMER",
"VECTOR_FRT2_OCIA", "VECTOR_FRT2_OCIA",
"VECTOR_SCI1_TXI", "VECTOR_SCI1_TXI",

View File

@@ -39,6 +39,7 @@ 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("--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("--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("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt")
parser.add_argument("--frt1-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT1 OCIA interrupt")
parser.add_argument("--frt2-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT2 OCIA 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-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") parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine")
@@ -56,6 +57,7 @@ def main(argv: list[str] | None = None) -> int:
emulator = H8536Emulator( emulator = H8536Emulator(
rom_bytes, rom_bytes,
interval_steps=args.interval_steps, interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps,
p9_fast_path_enabled=args.p9_fast_path, p9_fast_path_enabled=args.p9_fast_path,
p9_fast_default_input_byte=args.p9_fast_input, p9_fast_default_input_byte=args.p9_fast_input,

View File

@@ -16,6 +16,8 @@ IPRC = 0xFF02
IPRE = 0xFF04 IPRE = 0xFF04
WDT_TCSR_R = 0xFEEC WDT_TCSR_R = 0xFEEC
FRT1_TCR = 0xFE90
FRT1_TCSR = 0xFE91
FRT2_TCR = 0xFEA0 FRT2_TCR = 0xFEA0
FRT2_TCSR = 0xFEA1 FRT2_TCSR = 0xFEA1
@@ -39,5 +41,6 @@ RAMCR = 0xFF11
HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA])
VECTOR_INTERVAL_TIMER = 0x0042 VECTOR_INTERVAL_TIMER = 0x0042
VECTOR_FRT1_OCIA = 0x0062
VECTOR_FRT2_OCIA = 0x006A VECTOR_FRT2_OCIA = 0x006A
VECTOR_SCI1_TXI = 0x0084 VECTOR_SCI1_TXI = 0x0084

View File

@@ -7,12 +7,93 @@ from pathlib import Path
from ..formatting import h16, parse_int from ..formatting import h16, parse_int
from .cli import load_rom from .cli import load_rom
from .constants import P9DDR, P9DR, SCI1_TDR 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 .errors import UnsupportedInstruction
from .runner import H8536Emulator from .runner import H8536Emulator
DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059) 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) @dataclass(frozen=True)
@@ -49,6 +130,8 @@ class ProbeReport:
p9_fast_events: int = 0 p9_fast_events: int = 0
p9_accesses: list[str] = field(default_factory=list) p9_accesses: list[str] = field(default_factory=list)
sci_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) watch_snapshots: list[WatchSnapshot] = field(default_factory=list)
unsupported: str | None = None unsupported: str | None = None
@@ -57,12 +140,17 @@ class ProbeReport:
f"steps={self.steps}", f"steps={self.steps}",
f"pc={h16(self.pc)}", f"pc={h16(self.pc)}",
f"stopped={self.stopped_reason}", f"stopped={self.stopped_reason}",
"tx_bytes=" + self.tx_bytes.hex(" ").upper(), "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_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:]), "p9_fast_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_fast_bytes[-32:]),
f"p9_fast_events={self.p9_fast_events}", 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)), "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: if self.unsupported:
lines.append(f"unsupported={self.unsupported}") lines.append(f"unsupported={self.unsupported}")
if self.p9_accesses: if self.p9_accesses:
@@ -77,6 +165,31 @@ class ProbeReport:
return lines 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: def parse_watch_pc(text: str) -> int:
try: try:
value = parse_int(text) value = parse_int(text)
@@ -132,9 +245,11 @@ def run_probe(
interval_steps: int, interval_steps: int,
stop_on_tx: bool, stop_on_tx: bool,
p9_log_limit: int, p9_log_limit: int,
frt1_ocia_steps: int = 1024,
frt2_ocia_steps: int = 1024, frt2_ocia_steps: int = 1024,
p9_fast_path: bool = False, p9_fast_path: bool = False,
p9_fast_input: int = 0xFF, p9_fast_input: int = 0xFF,
sci_log_limit: int = 32,
watch_pcs: list[int] | tuple[int, ...] | None = None, watch_pcs: list[int] | tuple[int, ...] | None = None,
watch_snapshot_limit: int = 32, watch_snapshot_limit: int = 32,
watch_pc_limit: int = 8, watch_pc_limit: int = 8,
@@ -143,6 +258,7 @@ def run_probe(
emulator = H8536Emulator( emulator = H8536Emulator(
rom_bytes, rom_bytes,
interval_steps=interval_steps, interval_steps=interval_steps,
frt1_ocia_steps=frt1_ocia_steps,
frt2_ocia_steps=frt2_ocia_steps, frt2_ocia_steps=frt2_ocia_steps,
p9_fast_path_enabled=p9_fast_path, p9_fast_path_enabled=p9_fast_path,
p9_fast_default_input_byte=p9_fast_input, p9_fast_default_input_byte=p9_fast_input,
@@ -183,7 +299,12 @@ def run_probe(
if len(p9_accesses) > p9_log_limit: if len(p9_accesses) > p9_log_limit:
del p9_accesses[: len(p9_accesses) - p9_log_limit] del p9_accesses[: len(p9_accesses) - p9_log_limit]
elif access.address == SCI1_TDR: elif access.address == SCI1_TDR:
sci_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}") 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) last_access_index = len(emulator.memory.access_log)
if stop_on_tx and emulator.sci1.tx_bytes: if stop_on_tx and emulator.sci1.tx_bytes:
@@ -204,6 +325,8 @@ def run_probe(
p9_fast_events=len(emulator.p9_fast_path.events), p9_fast_events=len(emulator.p9_fast_path.events),
p9_accesses=p9_accesses, p9_accesses=p9_accesses,
sci_accesses=sci_accesses, sci_accesses=sci_accesses,
sci1=_sci1_snapshot(emulator),
sci1_txi=_sci1_txi_summary(emulator),
watch_snapshots=snapshots, watch_snapshots=snapshots,
unsupported=unsupported, unsupported=unsupported,
) )
@@ -214,11 +337,13 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to the repo ROM image") 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("--max-steps", type=int, default=250_000)
parser.add_argument("--interval-steps", type=int, default=512) 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("--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("--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-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-fast-input", type=parse_int, default=0xFF)
parser.add_argument("--p9-log-limit", type=int, default=80) 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("--hot-limit", type=int, default=12)
parser.add_argument( parser.add_argument(
"--watch-pc", "--watch-pc",
@@ -245,11 +370,13 @@ def main(argv: list[str] | None = None) -> int:
rom_bytes, rom_bytes,
max_steps=args.max_steps, max_steps=args.max_steps,
interval_steps=args.interval_steps, interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps,
stop_on_tx=args.stop_on_tx, stop_on_tx=args.stop_on_tx,
p9_log_limit=args.p9_log_limit, p9_log_limit=args.p9_log_limit,
p9_fast_path=args.p9_fast_path, p9_fast_path=args.p9_fast_path,
p9_fast_input=args.p9_fast_input, 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_pcs=tuple(dict.fromkeys((*DEFAULT_WATCH_PCS, *args.watch_pc))),
watch_snapshot_limit=args.watch_snapshot_limit, watch_snapshot_limit=args.watch_snapshot_limit,
watch_pc_limit=args.watch_pc_limit, watch_pc_limit=args.watch_pc_limit,

View File

@@ -9,6 +9,8 @@ from ..vectors import read_vectors_min
from .constants import ( from .constants import (
FRT_TCR_OCIEA, FRT_TCR_OCIEA,
FRT_TCSR_OCFA, FRT_TCSR_OCFA,
FRT1_TCR,
FRT1_TCSR,
FRT2_TCR, FRT2_TCR,
FRT2_TCSR, FRT2_TCSR,
IPRA, IPRA,
@@ -16,6 +18,7 @@ from .constants import (
IPRE, IPRE,
SCI_SCR_TIE, SCI_SCR_TIE,
SCI_SSR_TDRE, SCI_SSR_TDRE,
VECTOR_FRT1_OCIA,
VECTOR_FRT2_OCIA, VECTOR_FRT2_OCIA,
VECTOR_INTERVAL_TIMER, VECTOR_INTERVAL_TIMER,
VECTOR_SCI1_TXI, VECTOR_SCI1_TXI,
@@ -63,6 +66,7 @@ class H8536Emulator:
rom_bytes: bytes, rom_bytes: bytes,
*, *,
interval_steps: int = 2048, interval_steps: int = 2048,
frt1_ocia_steps: int = 1024,
frt2_ocia_steps: int = 1024, frt2_ocia_steps: int = 1024,
p9_fast_path: P9FastPath | None = None, p9_fast_path: P9FastPath | None = None,
p9_fast_path_enabled: bool = False, p9_fast_path_enabled: bool = False,
@@ -78,8 +82,10 @@ class H8536Emulator:
self.cpu = CPUState() self.cpu = CPUState()
self.vectors = read_vectors_min(self.memory.rom) self.vectors = read_vectors_min(self.memory.rom)
self.interval_steps = max(1, interval_steps) self.interval_steps = max(1, interval_steps)
self.frt1_ocia_steps = max(1, frt1_ocia_steps)
self.frt2_ocia_steps = max(1, frt2_ocia_steps) self.frt2_ocia_steps = max(1, frt2_ocia_steps)
self._interval_counter = 0 self._interval_counter = 0
self._frt1_ocia_counter = 0
self._frt2_ocia_counter = 0 self._frt2_ocia_counter = 0
self.reset() self.reset()
@@ -372,6 +378,7 @@ class H8536Emulator:
def _tick_peripherals(self) -> None: def _tick_peripherals(self) -> None:
self.sci1.tick() self.sci1.tick()
self._interval_counter += 1 self._interval_counter += 1
self._frt1_ocia_counter += 1
self._frt2_ocia_counter += 1 self._frt2_ocia_counter += 1
self._service_pending_interrupt() self._service_pending_interrupt()
@@ -387,6 +394,10 @@ class H8536Emulator:
target = self._vector_target(VECTOR_INTERVAL_TIMER) target = self._vector_target(VECTOR_INTERVAL_TIMER)
if target is not None: if target is not None:
candidates.append((self._interval_priority(), target, "interval_timer")) candidates.append((self._interval_priority(), target, "interval_timer"))
if self._frt1_ocia_pending():
target = self._vector_target(VECTOR_FRT1_OCIA)
if target is not None:
candidates.append((self._frt1_priority(), target, "frt1_ocia"))
if self._frt2_ocia_pending(): if self._frt2_ocia_pending():
target = self._vector_target(VECTOR_FRT2_OCIA) target = self._vector_target(VECTOR_FRT2_OCIA)
if target is not None: if target is not None:
@@ -399,6 +410,9 @@ class H8536Emulator:
return return
if source == "interval_timer": if source == "interval_timer":
self._interval_counter = 0 self._interval_counter = 0
elif source == "frt1_ocia":
self._frt1_ocia_counter = 0
self.memory.write8(FRT1_TCSR, self.memory.read8(FRT1_TCSR) | FRT_TCSR_OCFA)
elif source == "frt2_ocia": elif source == "frt2_ocia":
self._frt2_ocia_counter = 0 self._frt2_ocia_counter = 0
self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA) self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA)
@@ -430,6 +444,14 @@ class H8536Emulator:
def _interval_priority(self) -> int: def _interval_priority(self) -> int:
return (self.memory.read8(IPRA) >> 4) & 0x07 return (self.memory.read8(IPRA) >> 4) & 0x07
def _frt1_ocia_pending(self) -> bool:
if self._frt1_ocia_counter < self.frt1_ocia_steps:
return False
return bool(self.memory.read8(FRT1_TCR) & FRT_TCR_OCIEA)
def _frt1_priority(self) -> int:
return (self.memory.read8(IPRC) >> 4) & 0x07
def _frt2_ocia_pending(self) -> bool: def _frt2_ocia_pending(self) -> bool:
if self._frt2_ocia_counter < self.frt2_ocia_steps: if self._frt2_ocia_counter < self.frt2_ocia_steps:
return False return False

View File

@@ -11,6 +11,9 @@ from .constants import (
SCI1_SSR, SCI1_SSR,
SCI1_TDR, SCI1_TDR,
SCI_SCR_TE, SCI_SCR_TE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF, SCI_SSR_RDRF,
SCI_SSR_TDRE, SCI_SSR_TDRE,
) )
@@ -34,6 +37,8 @@ class SCI1:
- SCR bit 7 TIE, bit 6 RIE, bit 5 TE, bit 4 RE. - 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. - SSR bit 7 TDRE, bit 6 RDRF, bits 5..3 ORER/FER/PER.
- Software normally writes TDR after TDRE=1, then clears SSR.TDRE. - Software normally writes TDR after TDRE=1, then clears SSR.TDRE.
- SSR status flags are R/(W)*: writing 0 clears a latched flag, while
writing 1 leaves the hardware-owned flag unchanged.
""" """
smr: int = 0x00 smr: int = 0x00
@@ -47,6 +52,8 @@ class SCI1:
tx_frames: list[bytes] = field(default_factory=list) tx_frames: list[bytes] = field(default_factory=list)
_frame_buffer: bytearray = field(default_factory=bytearray) _frame_buffer: bytearray = field(default_factory=bytearray)
tx_ready_delay: int = 0 tx_ready_delay: int = 0
tx_ready_ticks: int = 2
_tx_ready_pending: bool = False
def read(self, address: int) -> int: def read(self, address: int) -> int:
if address == SCI1_SMR: if address == SCI1_SMR:
@@ -75,10 +82,7 @@ class SCI1:
self.tdr = value self.tdr = value
self._write_tdr(value) self._write_tdr(value)
elif address == SCI1_SSR: elif address == SCI1_SSR:
# The real SSR is R/(W)*: writable zeroes clear latched flags after self._write_ssr(value)
# 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: elif address == SCI1_RDR:
self.rdr = value self.rdr = value
else: else:
@@ -92,10 +96,14 @@ class SCI1:
if len(self._frame_buffer) == len(HEARTBEAT_FRAME): if len(self._frame_buffer) == len(HEARTBEAT_FRAME):
self.tx_frames.append(bytes(self._frame_buffer)) self.tx_frames.append(bytes(self._frame_buffer))
self._frame_buffer.clear() self._frame_buffer.clear()
self.ssr |= SCI_SSR_TDRE self.tx_ready_delay = max(0, self.tx_ready_ticks)
self.tx_ready_delay = 2 self._tx_ready_pending = True
self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted))
def _write_ssr(self, value: int) -> None:
writable_zero_flags = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER
self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags)
def inject_rx(self, value: int) -> None: def inject_rx(self, value: int) -> None:
self.rdr = value & 0xFF self.rdr = value & 0xFF
self.ssr |= SCI_SSR_RDRF self.ssr |= SCI_SSR_RDRF
@@ -104,7 +112,8 @@ class SCI1:
return HEARTBEAT_FRAME in self.tx_frames return HEARTBEAT_FRAME in self.tx_frames
def tick(self) -> None: def tick(self) -> None:
if self.tx_ready_delay: if self._tx_ready_pending and self.tx_ready_delay:
self.tx_ready_delay -= 1 self.tx_ready_delay -= 1
if self.tx_ready_delay == 0: if self._tx_ready_pending and self.tx_ready_delay == 0 and not (self.ssr & SCI_SSR_TDRE):
self.ssr |= SCI_SSR_TDRE self.ssr |= SCI_SSR_TDRE
self._tx_ready_pending = False

View File

@@ -0,0 +1,104 @@
import unittest
from collections import Counter
from h8536.emulator.constants import IPRE, SCI1_SCR, SCI1_TDR, SCI_SCR_TE
from h8536.emulator.probe import ProbeReport, SCI1Snapshot, SCI1TXISummary, run_probe
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1040) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorProbeSciTest(unittest.TestCase):
def test_report_lines_include_compact_sci_state_and_txi_summary(self):
report = ProbeReport(
steps=12,
pc=0xBA68,
stopped_reason="max_steps",
hot_pcs=Counter({0xBA68: 7}),
tx_bytes=b"\x00\x03",
sci_accesses=[
"old write SCR=00",
"H'BA60 read SSR=00",
"H'BA64 write SCR=A0",
"H'BA68 write TDR=03",
],
sci1=SCI1Snapshot(
smr=0x00,
brr=0x07,
scr=0xA0,
ssr=0x80,
tdr=0x03,
rdr=0x00,
tx_ready_delay=1,
),
sci1_txi=SCI1TXISummary(
tie=True,
te=True,
tdre=True,
vector_target=0xBA68,
priority=6,
interrupt_mask=2,
interrupt_depth=0,
),
)
lines = report.lines()
self.assertIn("sci1=SMR=00 BRR=07 SCR=A0 SSR=80 TDR=03 RDR=00 tx_ready_delay=1", lines)
self.assertIn(
"sci1_txi=TIE=1 TE=1 TDRE=1 vector=H'BA68 priority=6 mask=2 depth=0 pending=1 serviceable=1",
lines,
)
self.assertIn("recent_sci:", lines)
self.assertIn(" H'BA60 read SSR=00", lines)
self.assertIn(" H'BA68 write TDR=03", lines)
def test_report_lines_bound_recent_sci_accesses(self):
report = ProbeReport(
steps=1,
pc=0x1000,
stopped_reason="max_steps",
sci_accesses=[f"H'{idx:04X} read SSR={idx:02X}" for idx in range(20)],
)
lines = report.lines()
self.assertNotIn(" H'0000 read SSR=00", lines)
self.assertIn(" H'0004 read SSR=04", lines)
self.assertIn(" H'0013 read SSR=13", lines)
def test_run_probe_tracks_sci_register_accesses_and_final_txi_state(self):
rom = rom_with_reset()
rom[0x0084:0x0086] = (0x1010).to_bytes(2, "big")
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1008] = bytes([0x15, (IPRE >> 8) & 0xFF, IPRE & 0xFF, 0x06, 0x50])
rom[0x1008:0x100D] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, 0xA0])
rom[0x100D] = 0x00
rom[0x1010:0x1015] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, SCI_SCR_TE])
rom[0x1015:0x101A] = bytes([0x15, (SCI1_TDR >> 8) & 0xFF, SCI1_TDR & 0xFF, 0x06, 0x42])
rom[0x101A] = 0x0A
report = run_probe(
bytes(rom),
max_steps=8,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
sci_log_limit=8,
watch_pcs=(),
)
self.assertEqual(report.tx_bytes, b"\x42")
self.assertIsNotNone(report.sci1)
self.assertIsNotNone(report.sci1_txi)
self.assertTrue(any("write SCR=A0" in line for line in report.sci_accesses))
self.assertTrue(any("write SCR=20" in line for line in report.sci_accesses))
self.assertTrue(any("write TDR=42" in line for line in report.sci_accesses))
self.assertTrue(any(line.startswith("sci1=SMR=00") for line in report.lines()))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,71 @@
import unittest
from h8536.emulator import (
HEARTBEAT_FRAME,
SCI1_SCR,
SCI1_SSR,
SCI1_TDR,
SCI_SCR_TE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
SCI1,
)
class SciTimingTest(unittest.TestCase):
def test_ssr_write_one_does_not_set_hardware_flags(self):
sci = SCI1()
sci.ssr = 0x00
sci.write(SCI1_SSR, SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER)
self.assertEqual(sci.read(SCI1_SSR) & 0xF8, 0x00)
def test_ssr_write_zero_clears_selected_writable_flags(self):
sci = SCI1()
sci.ssr = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER | 0x07
sci.write(SCI1_SSR, sci.ssr & ~SCI_SSR_RDRF)
self.assertEqual(sci.read(SCI1_SSR) & SCI_SSR_RDRF, 0x00)
self.assertEqual(
sci.read(SCI1_SSR) & (SCI_SSR_TDRE | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER),
SCI_SSR_TDRE | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER,
)
def test_tdr_write_then_ssr_clear_delays_tdre_until_ticks(self):
sci = SCI1(tx_ready_ticks=2)
sci.write(SCI1_SCR, sci.read(SCI1_SCR) | SCI_SCR_TE)
self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
sci.write(SCI1_TDR, 0x42)
sci.write(SCI1_SSR, sci.read(SCI1_SSR) & ~SCI_SSR_TDRE)
self.assertFalse(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
sci.tick()
self.assertFalse(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
sci.tick()
self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
def test_transmit_capture_requires_te_enabled(self):
sci = SCI1()
sci.write(SCI1_TDR, 0x33)
self.assertEqual(sci.tx_bytes, [])
self.assertFalse(sci.tx_events[-1].emitted)
sci.write(SCI1_SCR, sci.read(SCI1_SCR) | SCI_SCR_TE)
for byte in HEARTBEAT_FRAME:
sci.write(SCI1_TDR, byte)
self.assertEqual(bytes(sci.tx_bytes), HEARTBEAT_FRAME)
self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME])
self.assertTrue(sci.saw_heartbeat())
if __name__ == "__main__":
unittest.main()

View File

@@ -4,9 +4,12 @@ from h8536.emulator import H8536Emulator, ON_CHIP_RAM_START
from h8536.emulator.constants import ( from h8536.emulator.constants import (
FRT_TCR_OCIEA, FRT_TCR_OCIEA,
FRT_TCSR_OCFA, FRT_TCSR_OCFA,
FRT1_TCR,
FRT1_TCSR,
FRT2_TCR, FRT2_TCR,
FRT2_TCSR, FRT2_TCSR,
IPRC, IPRC,
VECTOR_FRT1_OCIA,
VECTOR_FRT2_OCIA, VECTOR_FRT2_OCIA,
) )
@@ -23,6 +26,56 @@ def write_mov_b_abs_imm(rom: bytearray, address: int, target: int, value: int) -
class Frt2OciaTimerTest(unittest.TestCase): class Frt2OciaTimerTest(unittest.TestCase):
def test_frt1_ocia_vector_can_fire_and_decrement_ram(self):
rom = rom_with_reset()
rom[VECTOR_FRT1_OCIA : VECTOR_FRT1_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
# IPRC bits 6..4 are FRT1 and bits 2..0 are FRT2, so H'60 makes
# only the FRT1 priority field high enough to pass interrupt mask 0.
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x60)
pc = write_mov_b_abs_imm(rom, pc, FRT1_TCR, FRT_TCR_OCIEA)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
isr = 0x1020
rom[isr : isr + 4] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C])
isr += 4
rom[isr : isr + 4] = bytes([0x15, (FRT1_TCSR >> 8) & 0xFF, FRT1_TCSR & 0xFF, 0xD5])
rom[isr + 4] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt1_ocia_steps=2)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=5)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 2)
self.assertFalse(emulator.memory.read8(FRT1_TCSR) & FRT_TCSR_OCFA)
self.assertEqual(emulator.cpu.pc, isr + 4)
def test_frt1_ocia_does_not_fire_when_ociea_disabled(self):
rom = rom_with_reset()
rom[VECTOR_FRT1_OCIA : VECTOR_FRT1_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x60)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
rom[0x1020 : 0x1024] = bytes(
[0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C]
)
rom[0x1024] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt1_ocia_steps=1)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=8)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 3)
self.assertEqual(emulator.memory.read8(FRT1_TCSR) & FRT_TCSR_OCFA, 0)
self.assertEqual(emulator.cpu.pc, pc)
def test_frt2_ocia_vector_can_fire_and_decrement_ram(self): def test_frt2_ocia_vector_can_fire_and_decrement_ram(self):
rom = rom_with_reset() rom = rom_with_reset()
rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big") rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")