1
0

EMualtor adjustments

This commit is contained in:
Aiden
2026-05-25 20:42:45 +10:00
parent d2e7609bbf
commit 3ab79648ff
17 changed files with 3047 additions and 83 deletions

View File

@@ -51,6 +51,14 @@ class P9FastPathEvent:
kind: str
pc: int
value: int | None = None
source: str | None = None
queue_depth: int | None = None
def line(self) -> str:
value = "" if self.value is None else f" value={self.value:02X}"
source = "" if self.source is None else f" source={self.source}"
queue_depth = "" if self.queue_depth is None else f" queued={self.queue_depth}"
return f"{self.kind} pc={self.pc:04X}{value}{source}{queue_depth}"
@dataclass
@@ -59,11 +67,26 @@ class P9FastPath:
config: P9FastPathConfig = field(default_factory=P9FastPathConfig)
input_bytes: list[int] = field(default_factory=list)
input_sources: list[str] = 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:
def __post_init__(self) -> None:
if len(self.input_sources) < len(self.input_bytes):
self.input_sources.extend(["initial"] * (len(self.input_bytes) - len(self.input_sources)))
elif len(self.input_sources) > len(self.input_bytes):
del self.input_sources[len(self.input_bytes) :]
def queue_input(self, *values: int, source: str = "queued") -> None:
self.input_bytes.extend(value & 0xFF for value in values)
self.input_sources.extend(source for _ in values)
def queue_input_script(self, name: str, values: list[int] | tuple[int, ...]) -> None:
self.queue_input(*values, source=f"script:{name}")
def trace_lines(self, limit: int | None = None) -> list[str]:
events = self.events if limit is None else self.events[-limit:]
return [event.line() for event in events]
def try_handle(self, emulator: Any) -> bool:
if not self.config.enabled:
@@ -102,9 +125,14 @@ class P9FastPath:
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
if self.input_bytes:
value = self.input_bytes.pop(0)
source = self.input_sources.pop(0) if self.input_sources else "queued"
else:
value = self.config.default_input_byte
source = "default_input_byte"
value &= 0xFF
self.events.append(P9FastPathEvent("read_byte", pc, value))
self.events.append(P9FastPathEvent("read_byte", pc, value, source, len(self.input_bytes)))
# 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.

View File

@@ -8,8 +8,12 @@ from pathlib import Path
from ..formatting import h16, parse_int
from .cli import load_rom
from .constants import (
ON_CHIP_RAM_END,
ON_CHIP_RAM_START,
P9DDR,
P9DR,
REGISTER_FIELD_END,
REGISTER_FIELD_START,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
@@ -26,6 +30,56 @@ from .runner import H8536Emulator
DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059)
TX_FRAME_WATCH_PCS = {
0xBA26: "builder_entry",
0xBA4E: "checksum_seed",
0xBA64: "checksum_store",
0xBA72: "first_tdr",
0xBAB5: "txi_tdr",
}
TX_FRAME_SNAPSHOT_START = 0xF850
TX_FRAME_START = 0xF858
TX_FRAME_LENGTH = 6
TX_FRAME_TRACE_START = TX_FRAME_SNAPSHOT_START
TX_FRAME_TRACE_END = TX_FRAME_START + TX_FRAME_LENGTH - 1
REPORT_QUEUE_START = 0xF870
REPORT_QUEUE_SLOTS = 0x80
REPORT_QUEUE_END = REPORT_QUEUE_START + (REPORT_QUEUE_SLOTS * 2) - 1
REPORT_QUEUE_HEAD = 0xF9B0
REPORT_QUEUE_TAIL = 0xF9B5
REPORT_QUEUE_ENTRY_PCS = {
0x3E54: "enqueue_entry",
0xBAF2: "dequeue_entry",
}
REPORT_QUEUE_FIRST_NONZERO_DISPLAY_LIMIT = 16
REPORT_GATE_PCS = {
0x4046: "gate_entry",
0x404A: "f9c4_nonzero_return_branch",
0x404C: "faa5_bit7_test",
0x4050: "faa5_clear_enqueue_branch",
0x4052: "f9c3_test",
0x4056: "f9c3_zero_enqueue_branch",
0x4058: "gate_return_no_enqueue",
0x4059: "queue_empty_check_start",
0x405F: "queue_empty_compare",
0x4063: "queue_not_empty_return_branch",
0x4067: "enqueue_report_zero",
0x406C: "advance_head",
0x4070: "mask_head",
0x4074: "gate_return",
}
REPORT_GATE_F9C4 = 0xF9C4
REPORT_GATE_F9C3 = 0xF9C3
REPORT_GATE_FAA5 = 0xFAA5
RAM_LIFECYCLE_DEFAULT_WATCHES = {
REPORT_GATE_F9C4: "F9C4_report_gate_timer",
REPORT_GATE_F9C3: "F9C3_rx_or_activity_gate",
REPORT_GATE_FAA5: "FAA5_report_flags",
0xF9C0: "F9C0_tx_or_report_gate",
0xF9C5: "F9C5_queue_reset_gate",
REPORT_QUEUE_HEAD: "F9B0_report_queue_head",
REPORT_QUEUE_TAIL: "F9B5_report_queue_tail",
}
SCI1_PROBE_REGISTERS = {
SCI1_SCR: "SCR",
SCI1_TDR: "TDR",
@@ -50,6 +104,72 @@ def _format_six_byte_frames(data: bytes, *, limit: int = 8) -> str:
return " | ".join(frame.hex(" ").upper() for frame in recent)
def parse_tx_frame(text: str) -> bytes:
normalized = (
text.strip()
.replace(",", " ")
.replace(":", " ")
.replace("-", " ")
.replace("_", " ")
)
parts = normalized.split()
if len(parts) == 1:
compact = parts[0]
if compact.lower().startswith("0x"):
compact = compact[2:]
if compact.upper().startswith("H'"):
compact = compact[2:]
if len(compact) % 2:
raise argparse.ArgumentTypeError("target frame compact hex must have an even number of digits")
parts = [compact[index : index + 2] for index in range(0, len(compact), 2)]
values: list[int] = []
for part in parts:
token = part
if token.lower().startswith("0x"):
token = token[2:]
if token.upper().startswith("H'"):
token = token[2:]
if not token or len(token) > 2:
raise argparse.ArgumentTypeError(f"invalid byte token {part!r}")
try:
value = int(token, 16)
except ValueError as exc:
raise argparse.ArgumentTypeError(f"invalid byte token {part!r}") from exc
if not 0 <= value <= 0xFF:
raise argparse.ArgumentTypeError(f"byte out of range {part!r}")
values.append(value)
if len(values) != TX_FRAME_LENGTH:
raise argparse.ArgumentTypeError(f"target frame must contain exactly {TX_FRAME_LENGTH} bytes")
return bytes(values)
def _frame_diff_text(current: bytes, target: bytes) -> str:
diffs = [
f"{index}:{current[index]:02X}!={target[index]:02X}"
for index in range(min(len(current), len(target)))
if current[index] != target[index]
]
if len(current) != len(target):
diffs.append(f"length:{len(current)}!={len(target)}")
return "none" if not diffs else " ".join(diffs)
def _target_comparison_line(current: bytes, target: bytes) -> str:
pre_checksum_diffs = [
str(index)
for index in range(TX_FRAME_LENGTH - 1)
if index < len(current) and index < len(target) and current[index] != target[index]
]
return (
"target_frame="
f"target={target.hex(' ').upper()} current={current.hex(' ').upper()} "
f"diffs={_frame_diff_text(current, target)} "
f"pre_checksum_diffs={','.join(pre_checksum_diffs) if pre_checksum_diffs else 'none'}"
)
@dataclass(frozen=True)
class SCI1Snapshot:
smr: int
@@ -118,6 +238,187 @@ class WatchSnapshot:
return f"step={self.step} pc={h16(self.pc)} sp={h16(self.sp)} {regs} stack=[{stack}] callers=[{callers}]"
@dataclass(frozen=True)
class TXFrameSnapshot:
step: int
pc: int
label: str
bytes_f850_f85d: bytes
computed_checksum: int
stored_checksum: int
checksum_ok: bool
@property
def frame_bytes(self) -> bytes:
return self.bytes_f850_f85d[TX_FRAME_START - TX_FRAME_SNAPSHOT_START :]
def line(self) -> str:
return (
f"step={self.step} pc={h16(self.pc)} {self.label} "
f"F850-F85D={self.bytes_f850_f85d.hex(' ').upper()} "
f"TX={self.frame_bytes.hex(' ').upper()} "
f"computed={self.computed_checksum:02X} stored={self.stored_checksum:02X} "
f"checksum_ok={int(self.checksum_ok)}"
)
@dataclass(frozen=True)
class TXFrameWriteTrace:
step: int
pc: int
address: int
old_value: int
new_value: int
frame_after: bytes
instruction: str
regs: tuple[int, ...]
target_value: int | None = None
@property
def area(self) -> str:
if TX_FRAME_START <= self.address < TX_FRAME_START + TX_FRAME_LENGTH:
return f"TX[{self.address - TX_FRAME_START}]"
return f"stage[{self.address - TX_FRAME_SNAPSHOT_START}]"
def line(self) -> str:
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
target = ""
if self.target_value is not None:
verdict = "match" if self.new_value == self.target_value else "DIFF"
target = f" target={self.target_value:02X} {verdict}"
return (
f"step={self.step} pc={h16(self.pc)} {self.area} {h16(self.address)} "
f"{self.old_value:02X}->{self.new_value:02X}{target} "
f"frame_after={self.frame_after.hex(' ').upper()} regs=[{regs}] "
f"instruction={self.instruction}"
)
@dataclass(frozen=True)
class ReportQueueTrace:
step: int
pc: int
kind: str
head: int
tail: int
instruction: str
regs: tuple[int, ...]
address: int | None = None
queue_index: int | None = None
old_value: int | None = None
new_value: int | None = None
old_word: int | None = None
new_word: int | None = None
candidate_word: int | None = None
def line(self) -> str:
parts = [
f"step={self.step}",
f"pc={h16(self.pc)}",
self.kind,
f"head={self.head:02X}",
f"tail={self.tail:02X}",
f"depth={_report_queue_depth(self.head, self.tail):02X}",
]
if self.queue_index is not None:
parts.append(f"slot={self.queue_index:02X}")
if self.address is not None:
parts.append(f"addr={h16(self.address)}")
if self.old_value is not None and self.new_value is not None:
parts.append(f"byte={self.old_value:02X}->{self.new_value:02X}")
if self.old_word is not None:
if self.new_word is None:
parts.append(f"word={h16(self.old_word)}")
else:
parts.append(f"word={h16(self.old_word)}->{h16(self.new_word)}")
if self.candidate_word is not None:
parts.append(f"candidate={h16(self.candidate_word)}")
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
parts.append(f"regs=[{regs}]")
parts.append(f"instruction={self.instruction}")
return " ".join(parts)
@dataclass(frozen=True)
class RAMLifecycleTrace:
step: int
pc: int
address: int
name: str
old_value: int
new_value: int
instruction: str
regs: tuple[int, ...]
def line(self) -> str:
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
return (
f"step={self.step} pc={h16(self.pc)} {self.name} {h16(self.address)} "
f"{self.old_value:02X}->{self.new_value:02X} regs=[{regs}] "
f"instruction={self.instruction}"
)
@dataclass(frozen=True)
class ReportGateTrace:
step: int
pc: int
label: str
f9c4: int
faa5: int
f9c3: int
head: int
tail: int
regs: tuple[int, ...]
z: bool
c: bool
n: bool
decision: str
f9c4_last_write_step: int | None = None
f9c4_last_write_pc: int | None = None
f9c4_last_write_value: int | None = None
f9c4_last_write_age: int | None = None
f9c4_last_nonzero_step: int | None = None
f9c4_last_nonzero_pc: int | None = None
f9c4_last_nonzero_value: int | None = None
f9c4_last_nonzero_age: int | None = None
def line(self) -> str:
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
lifecycle_parts = []
if (
self.f9c4_last_write_step is not None
and self.f9c4_last_write_pc is not None
and self.f9c4_last_write_value is not None
):
lifecycle_parts.append(
"F9C4_last="
f"step={self.f9c4_last_write_step}@{h16(self.f9c4_last_write_pc)} "
f"value={self.f9c4_last_write_value:02X} age={self.f9c4_last_write_age}"
)
if (
self.f9c4_last_nonzero_step is not None
and self.f9c4_last_nonzero_pc is not None
and self.f9c4_last_nonzero_value is not None
):
lifecycle_parts.append(
"F9C4_last_nonzero="
f"step={self.f9c4_last_nonzero_step}@{h16(self.f9c4_last_nonzero_pc)} "
f"value={self.f9c4_last_nonzero_value:02X} age={self.f9c4_last_nonzero_age}"
)
lifecycle = ""
if lifecycle_parts:
lifecycle = " " + " ".join(lifecycle_parts)
return (
f"step={self.step} pc={h16(self.pc)} {self.label} "
f"F9C4={self.f9c4:02X} FAA5={self.faa5:02X}/bit7={(self.faa5 >> 7) & 1} "
f"F9C3={self.f9c3:02X} head={self.head:02X} tail={self.tail:02X} "
f"depth={_report_queue_depth(self.head, self.tail):02X} "
f"Z={int(self.z)} C={int(self.c)} N={int(self.n)} "
f"decision={self.decision}{lifecycle} regs=[{regs}]"
)
@dataclass
class ProbeReport:
steps: int
@@ -133,6 +434,19 @@ class ProbeReport:
sci1: SCI1Snapshot | None = None
sci1_txi: SCI1TXISummary | None = None
watch_snapshots: list[WatchSnapshot] = field(default_factory=list)
tx_frame_snapshots: list[TXFrameSnapshot] = field(default_factory=list)
tx_frame_write_traces: list[TXFrameWriteTrace] = field(default_factory=list)
tx_target_divergences: list[TXFrameWriteTrace] = field(default_factory=list)
report_queue_traces: list[ReportQueueTrace] = field(default_factory=list)
report_queue_first_writes: list[ReportQueueTrace] = field(default_factory=list)
report_queue_first_nonzero_writes: list[ReportQueueTrace] = field(default_factory=list)
report_queue_watch_hits: list[ReportQueueTrace] = field(default_factory=list)
report_gate_traces: list[ReportGateTrace] = field(default_factory=list)
ram_lifecycle_traces: list[RAMLifecycleTrace] = field(default_factory=list)
ram_lifecycle_last_writes: list[RAMLifecycleTrace] = field(default_factory=list)
ram_lifecycle_last_nonzero_writes: list[RAMLifecycleTrace] = field(default_factory=list)
final_tx_frame: bytes = b""
target_frame: bytes | None = None
unsupported: str | None = None
def lines(self, hot_limit: int = 12) -> list[str]:
@@ -153,12 +467,51 @@ class ProbeReport:
lines.append(self.sci1_txi.line())
if self.unsupported:
lines.append(f"unsupported={self.unsupported}")
if self.target_frame is not None:
current = self.final_tx_frame
if not current and self.tx_frame_snapshots:
current = self.tx_frame_snapshots[-1].frame_bytes
lines.append(_target_comparison_line(current, self.target_frame))
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.tx_frame_snapshots:
lines.append("recent_tx_frame_snapshots:")
lines.extend(" " + snapshot.line() for snapshot in self.tx_frame_snapshots)
if self.tx_frame_write_traces:
lines.append("recent_tx_frame_writes:")
lines.extend(" " + trace.line() for trace in self.tx_frame_write_traces)
if self.tx_target_divergences:
lines.append("first_target_divergences:")
lines.extend(" " + trace.line() for trace in self.tx_target_divergences)
if self.report_queue_traces:
lines.append("recent_report_queue:")
lines.extend(" " + trace.line() for trace in self.report_queue_traces)
if self.report_queue_first_nonzero_writes:
lines.append("first_report_queue_nonzero_writes:")
shown = self.report_queue_first_nonzero_writes[:REPORT_QUEUE_FIRST_NONZERO_DISPLAY_LIMIT]
lines.extend(" " + trace.line() for trace in shown)
remaining = len(self.report_queue_first_nonzero_writes) - len(shown)
if remaining > 0:
lines.append(f" ... {remaining} more first nonzero queue writes")
if self.report_queue_watch_hits:
lines.append("report_queue_watch_hits:")
lines.extend(" " + trace.line() for trace in self.report_queue_watch_hits)
if self.report_gate_traces:
lines.append("recent_report_gates:")
lines.extend(" " + trace.line() for trace in self.report_gate_traces)
if self.ram_lifecycle_traces:
lines.append("recent_ram_lifecycle:")
lines.extend(" " + trace.line() for trace in self.ram_lifecycle_traces)
if self.ram_lifecycle_last_writes:
lines.append("ram_lifecycle_last_writes:")
lines.extend(" " + trace.line() for trace in self.ram_lifecycle_last_writes)
if self.ram_lifecycle_last_nonzero_writes:
lines.append("ram_lifecycle_last_nonzero_writes:")
lines.extend(" " + trace.line() for trace in self.ram_lifecycle_last_nonzero_writes)
if self.watch_snapshots:
lines.append("recent_watch_snapshots:")
lines.extend(" " + snapshot.line() for snapshot in self.watch_snapshots)
@@ -238,6 +591,308 @@ def _watch_snapshot(emulator: H8536Emulator, *, stack_words: int = 6) -> WatchSn
)
def _ram_window(emulator: H8536Emulator, start: int, length: int) -> bytes:
offset = start - ON_CHIP_RAM_START
return bytes(emulator.memory.ram[offset : offset + length])
def _tx_frame_checksum(frame: bytes) -> int:
checksum = 0x5A
for byte in frame[: TX_FRAME_LENGTH - 1]:
checksum ^= byte
return checksum & 0xFF
def _tx_frame_snapshot(emulator: H8536Emulator, label: str) -> TXFrameSnapshot:
data = _ram_window(emulator, TX_FRAME_SNAPSHOT_START, 0x0E)
frame = data[TX_FRAME_START - TX_FRAME_SNAPSHOT_START :]
computed = _tx_frame_checksum(frame)
stored = frame[TX_FRAME_LENGTH - 1]
return TXFrameSnapshot(
step=emulator.cpu.steps,
pc=emulator.cpu.pc & 0xFFFF,
label=label,
bytes_f850_f85d=data,
computed_checksum=computed,
stored_checksum=stored,
checksum_ok=computed == stored,
)
def _watched_frame_values(emulator: H8536Emulator) -> dict[int, int]:
data = _ram_window(emulator, TX_FRAME_TRACE_START, TX_FRAME_TRACE_END - TX_FRAME_TRACE_START + 1)
return {TX_FRAME_TRACE_START + index: value for index, value in enumerate(data)}
def _frame_from_watched_values(values: dict[int, int]) -> bytes:
return bytes(values.get(TX_FRAME_START + index, 0) & 0xFF for index in range(TX_FRAME_LENGTH))
def _report_queue_depth(head: int, tail: int) -> int:
return (head - tail) & 0x7F
def _report_queue_index(address: int) -> int | None:
if not REPORT_QUEUE_START <= address <= REPORT_QUEUE_END:
return None
return (address - REPORT_QUEUE_START) // 2
def _watched_report_queue_values(emulator: H8536Emulator) -> dict[int, int]:
data = _ram_window(emulator, REPORT_QUEUE_START, REPORT_QUEUE_END - REPORT_QUEUE_START + 1)
values = {REPORT_QUEUE_START + index: value for index, value in enumerate(data)}
values[REPORT_QUEUE_HEAD] = _ram_window(emulator, REPORT_QUEUE_HEAD, 1)[0]
values[REPORT_QUEUE_TAIL] = _ram_window(emulator, REPORT_QUEUE_TAIL, 1)[0]
return values
def _report_queue_word(values: dict[int, int], queue_index: int) -> int:
address = REPORT_QUEUE_START + ((queue_index & 0x7F) * 2)
return ((values.get(address, 0) & 0xFF) << 8) | (values.get(address + 1, 0) & 0xFF)
def _report_queue_state(values: dict[int, int]) -> tuple[int, int]:
return values.get(REPORT_QUEUE_HEAD, 0) & 0x7F, values.get(REPORT_QUEUE_TAIL, 0) & 0x7F
def _report_queue_entry_trace(emulator: H8536Emulator, values: dict[int, int], label: str) -> ReportQueueTrace:
head, tail = _report_queue_state(values)
queue_index = head if label == "enqueue_entry" else tail
candidate = emulator.cpu.regs[3] if label == "enqueue_entry" else _report_queue_word(values, queue_index)
return ReportQueueTrace(
step=emulator.cpu.steps,
pc=emulator.cpu.pc & 0xFFFF,
kind=label,
head=head,
tail=tail,
queue_index=queue_index,
old_word=_report_queue_word(values, queue_index),
candidate_word=candidate & 0xFFFF,
instruction="<entry>",
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
)
def _report_queue_access_traces(
emulator: H8536Emulator,
*,
pc: int,
instruction: str,
accesses: list,
values: dict[int, int],
) -> list[ReportQueueTrace]:
queue_accesses: dict[int, dict[str, object]] = {}
traces: list[ReportQueueTrace] = []
regs = tuple(register & 0xFFFF for register in emulator.cpu.regs)
for access in accesses:
queue_index = _report_queue_index(access.address)
if queue_index is not None:
group = queue_accesses.setdefault(
queue_index,
{
"address": REPORT_QUEUE_START + queue_index * 2,
"old_word": _report_queue_word(values, queue_index),
"read": False,
"write": False,
},
)
group[access.kind] = True
if access.kind == "write":
values[access.address] = access.value & 0xFF
continue
if access.kind != "write" or access.address not in (REPORT_QUEUE_HEAD, REPORT_QUEUE_TAIL):
continue
old_value = values.get(access.address, 0) & 0xFF
values[access.address] = access.value & 0xFF
head, tail = _report_queue_state(values)
traces.append(
ReportQueueTrace(
step=emulator.cpu.steps,
pc=pc,
kind="cursor_head_write" if access.address == REPORT_QUEUE_HEAD else "cursor_tail_write",
address=access.address,
old_value=old_value,
new_value=access.value & 0xFF,
head=head,
tail=tail,
instruction=instruction,
regs=regs,
)
)
for queue_index, group in sorted(queue_accesses.items()):
head, tail = _report_queue_state(values)
write = bool(group["write"])
traces.append(
ReportQueueTrace(
step=emulator.cpu.steps,
pc=pc,
kind="queue_write" if write else "queue_read",
address=int(group["address"]),
queue_index=queue_index,
old_word=int(group["old_word"]),
new_word=_report_queue_word(values, queue_index) if write else None,
head=head,
tail=tail,
instruction=instruction,
regs=regs,
)
)
return traces
def _report_queue_trace_matches_watch(trace: ReportQueueTrace, watch_ids: set[int]) -> bool:
if not watch_ids:
return False
candidates = (trace.old_word, trace.new_word, trace.candidate_word)
return any(value is not None and (value & 0xFFFF) in watch_ids for value in candidates)
def _ram_byte(emulator: H8536Emulator, address: int) -> int:
return _ram_window(emulator, address, 1)[0]
def _raw_memory_byte(emulator: H8536Emulator, address: int) -> int:
address &= 0xFFFF
if ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
return emulator.memory.ram[address - ON_CHIP_RAM_START]
if REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
return emulator.memory.registers[address - REGISTER_FIELD_START]
if address in emulator.memory.external:
return emulator.memory.external[address]
if emulator.memory.rom.contains(address):
return emulator.memory.rom.u8(address)
return 0
def _ram_lifecycle_watch_names(extra_addresses: list[int] | tuple[int, ...]) -> dict[int, str]:
names = dict(RAM_LIFECYCLE_DEFAULT_WATCHES)
for address in extra_addresses:
normalized = address & 0xFFFF
names.setdefault(normalized, f"ram_{normalized:04X}")
return names
def _ram_lifecycle_access_traces(
emulator: H8536Emulator,
*,
pc: int,
instruction: str,
accesses: list,
values: dict[int, int],
watch_names: dict[int, str],
) -> list[RAMLifecycleTrace]:
traces: list[RAMLifecycleTrace] = []
regs = tuple(register & 0xFFFF for register in emulator.cpu.regs)
for access in accesses:
address = access.address & 0xFFFF
if access.kind != "write" or address not in watch_names:
continue
old_value = values.get(address, 0) & 0xFF
new_value = access.value & 0xFF
values[address] = new_value
traces.append(
RAMLifecycleTrace(
step=emulator.cpu.steps,
pc=pc,
address=address,
name=watch_names[address],
old_value=old_value,
new_value=new_value,
instruction=instruction,
regs=regs,
)
)
return traces
def _report_gate_decision(pc: int, *, f9c4: int, faa5: int, f9c3: int, head: int, tail: int, z: bool) -> str:
if pc == 0x4046:
return "f9c4_zero_continue" if f9c4 == 0 else "f9c4_nonzero_return"
if pc == 0x404A:
return "return_f9c4_nonzero" if not z else "continue_f9c4_zero"
if pc == 0x404C:
return "faa5_bit7_clear_skips_f9c3" if not (faa5 & 0x80) else "faa5_bit7_set_check_f9c3"
if pc == 0x4050:
return "enqueue_candidate_faa5_clear" if z else "check_f9c3_faa5_set"
if pc == 0x4052:
return "f9c3_zero_enqueue" if f9c3 == 0 else "f9c3_nonzero_return"
if pc == 0x4056:
return "enqueue_candidate_f9c3_zero" if z else "return_f9c3_nonzero"
if pc == 0x4059:
return "check_empty_queue"
if pc == 0x405F:
return "queue_empty" if head == tail else "queue_not_empty"
if pc == 0x4063:
return "return_queue_not_empty" if not z else "enqueue_zero_report"
if pc == 0x4067:
return "write_report_00ff_to_queue_slot"
if pc == 0x406C:
return "advance_report_queue_head"
if pc == 0x4070:
return "mask_report_queue_head_bit7"
if pc in (0x4058, 0x4074):
return "return"
return "snapshot"
def _ram_lifecycle_age(emulator: H8536Emulator, trace: RAMLifecycleTrace | None) -> int | None:
if trace is None:
return None
return emulator.cpu.steps - trace.step
def _report_gate_trace(
emulator: H8536Emulator,
label: str,
*,
last_ram_lifecycle_writes: dict[int, RAMLifecycleTrace] | None = None,
last_ram_lifecycle_nonzero_writes: dict[int, RAMLifecycleTrace] | None = None,
) -> ReportGateTrace:
f9c4 = _ram_byte(emulator, REPORT_GATE_F9C4)
faa5 = _ram_byte(emulator, REPORT_GATE_FAA5)
f9c3 = _ram_byte(emulator, REPORT_GATE_F9C3)
head = _ram_byte(emulator, REPORT_QUEUE_HEAD) & 0x7F
tail = _ram_byte(emulator, REPORT_QUEUE_TAIL) & 0x7F
pc = emulator.cpu.pc & 0xFFFF
last_f9c4 = (last_ram_lifecycle_writes or {}).get(REPORT_GATE_F9C4)
last_nonzero_f9c4 = (last_ram_lifecycle_nonzero_writes or {}).get(REPORT_GATE_F9C4)
return ReportGateTrace(
step=emulator.cpu.steps,
pc=pc,
label=label,
f9c4=f9c4,
faa5=faa5,
f9c3=f9c3,
head=head,
tail=tail,
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
z=emulator.cpu.z,
c=emulator.cpu.c,
n=emulator.cpu.n,
decision=_report_gate_decision(
pc,
f9c4=f9c4,
faa5=faa5,
f9c3=f9c3,
head=head,
tail=tail,
z=emulator.cpu.z,
),
f9c4_last_write_step=last_f9c4.step if last_f9c4 is not None else None,
f9c4_last_write_pc=last_f9c4.pc if last_f9c4 is not None else None,
f9c4_last_write_value=last_f9c4.new_value if last_f9c4 is not None else None,
f9c4_last_write_age=_ram_lifecycle_age(emulator, last_f9c4),
f9c4_last_nonzero_step=last_nonzero_f9c4.step if last_nonzero_f9c4 is not None else None,
f9c4_last_nonzero_pc=last_nonzero_f9c4.pc if last_nonzero_f9c4 is not None else None,
f9c4_last_nonzero_value=last_nonzero_f9c4.new_value if last_nonzero_f9c4 is not None else None,
f9c4_last_nonzero_age=_ram_lifecycle_age(emulator, last_nonzero_f9c4),
)
def run_probe(
rom_bytes: bytes,
*,
@@ -254,6 +909,20 @@ def run_probe(
watch_snapshot_limit: int = 32,
watch_pc_limit: int = 8,
watch_min_interval: int = 1024,
tx_frame_watch: bool = True,
tx_frame_snapshot_limit: int = 32,
trace_frame_sources: bool = False,
frame_write_trace_limit: int = 64,
target_frame: bytes | None = None,
trace_report_queue: bool = False,
report_queue_trace_limit: int = 64,
watch_report_ids: list[int] | tuple[int, ...] = (),
report_queue_watch_hit_limit: int = 32,
trace_report_gates: bool = False,
report_gate_trace_limit: int = 64,
trace_ram_lifecycle: bool = False,
ram_watch_addresses: list[int] | tuple[int, ...] = (),
ram_lifecycle_trace_limit: int = 64,
) -> ProbeReport:
emulator = H8536Emulator(
rom_bytes,
@@ -267,6 +936,25 @@ def run_probe(
p9_accesses: list[str] = []
sci_accesses: list[str] = []
snapshots: list[WatchSnapshot] = []
tx_frame_snapshots: list[TXFrameSnapshot] = []
tx_frame_write_traces: list[TXFrameWriteTrace] = []
first_target_divergences: dict[int, TXFrameWriteTrace] = {}
watched_frame_values = _watched_frame_values(emulator)
report_queue_traces: list[ReportQueueTrace] = []
first_report_queue_writes: dict[int, ReportQueueTrace] = {}
first_report_queue_nonzero_writes: dict[int, ReportQueueTrace] = {}
report_queue_watch_hits: list[ReportQueueTrace] = []
report_gate_traces: list[ReportGateTrace] = []
ram_lifecycle_traces: list[RAMLifecycleTrace] = []
last_ram_lifecycle_writes: dict[int, RAMLifecycleTrace] = {}
last_ram_lifecycle_nonzero_writes: dict[int, RAMLifecycleTrace] = {}
ram_lifecycle_watch_names = _ram_lifecycle_watch_names(ram_watch_addresses)
ram_lifecycle_values = {
address: _raw_memory_byte(emulator, address) for address in ram_lifecycle_watch_names
}
watch_report_id_set = {value & 0xFFFF for value in watch_report_ids}
watched_report_queue_values = _watched_report_queue_values(emulator)
tx_builder_seen = False
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] = {}
@@ -277,6 +965,34 @@ def run_probe(
for _ in range(max_steps):
pc = emulator.cpu.pc
hot_pcs[pc] += 1
if pc == 0xBA26:
tx_builder_seen = True
if trace_report_queue and pc in REPORT_QUEUE_ENTRY_PCS:
report_queue_traces.append(
_report_queue_entry_trace(emulator, watched_report_queue_values, REPORT_QUEUE_ENTRY_PCS[pc])
)
if (
_report_queue_trace_matches_watch(report_queue_traces[-1], watch_report_id_set)
and len(report_queue_watch_hits) < report_queue_watch_hit_limit
):
report_queue_watch_hits.append(report_queue_traces[-1])
if len(report_queue_traces) > report_queue_trace_limit:
del report_queue_traces[: len(report_queue_traces) - report_queue_trace_limit]
if trace_report_gates and pc in REPORT_GATE_PCS:
report_gate_traces.append(
_report_gate_trace(
emulator,
REPORT_GATE_PCS[pc],
last_ram_lifecycle_writes=last_ram_lifecycle_writes,
last_ram_lifecycle_nonzero_writes=last_ram_lifecycle_nonzero_writes,
)
)
if len(report_gate_traces) > report_gate_trace_limit:
del report_gate_traces[: len(report_gate_traces) - report_gate_trace_limit]
if tx_frame_watch and pc in TX_FRAME_WATCH_PCS:
tx_frame_snapshots.append(_tx_frame_snapshot(emulator, TX_FRAME_WATCH_PCS[pc]))
if len(tx_frame_snapshots) > tx_frame_snapshot_limit:
del tx_frame_snapshots[: len(tx_frame_snapshots) - tx_frame_snapshot_limit]
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:
@@ -287,13 +1003,91 @@ def run_probe(
watch_last_step[pc] = emulator.cpu.steps
last_access_index = len(emulator.memory.access_log)
try:
emulator.step()
instruction = emulator.step()
except UnsupportedInstruction as exc:
stopped_reason = "unsupported_instruction"
unsupported = str(exc)
break
for access in emulator.memory.access_log[last_access_index:]:
recent_accesses = emulator.memory.access_log[last_access_index:]
if trace_report_queue:
new_report_queue_traces = _report_queue_access_traces(
emulator,
pc=pc,
instruction=instruction,
accesses=recent_accesses,
values=watched_report_queue_values,
)
for trace in new_report_queue_traces:
if trace.kind == "queue_write" and trace.queue_index not in first_report_queue_writes:
first_report_queue_writes[trace.queue_index] = trace
if (
trace.kind == "queue_write"
and trace.queue_index not in first_report_queue_nonzero_writes
and trace.new_word is not None
and trace.new_word != 0
):
first_report_queue_nonzero_writes[trace.queue_index] = trace
if (
_report_queue_trace_matches_watch(trace, watch_report_id_set)
and len(report_queue_watch_hits) < report_queue_watch_hit_limit
):
report_queue_watch_hits.append(trace)
report_queue_traces.extend(new_report_queue_traces)
if len(report_queue_traces) > report_queue_trace_limit:
del report_queue_traces[: len(report_queue_traces) - report_queue_trace_limit]
if trace_ram_lifecycle:
new_ram_lifecycle_traces = _ram_lifecycle_access_traces(
emulator,
pc=pc,
instruction=instruction,
accesses=recent_accesses,
values=ram_lifecycle_values,
watch_names=ram_lifecycle_watch_names,
)
for trace in new_ram_lifecycle_traces:
last_ram_lifecycle_writes[trace.address] = trace
if trace.new_value != 0:
last_ram_lifecycle_nonzero_writes[trace.address] = trace
ram_lifecycle_traces.extend(new_ram_lifecycle_traces)
if len(ram_lifecycle_traces) > ram_lifecycle_trace_limit:
del ram_lifecycle_traces[: len(ram_lifecycle_traces) - ram_lifecycle_trace_limit]
for access in recent_accesses:
if (
trace_frame_sources
and access.kind == "write"
and TX_FRAME_TRACE_START <= access.address <= TX_FRAME_TRACE_END
):
old_value = watched_frame_values.get(access.address, 0)
watched_frame_values[access.address] = access.value & 0xFF
target_value = None
if target_frame is not None and TX_FRAME_START <= access.address < TX_FRAME_START + TX_FRAME_LENGTH:
target_value = target_frame[access.address - TX_FRAME_START]
tx_frame_write_traces.append(
TXFrameWriteTrace(
step=emulator.cpu.steps,
pc=pc,
address=access.address,
old_value=old_value,
new_value=access.value,
frame_after=_frame_from_watched_values(watched_frame_values),
instruction=instruction,
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
target_value=target_value,
)
)
trace = tx_frame_write_traces[-1]
if (
tx_builder_seen
and target_value is not None
and access.value != target_value
and access.address not in first_target_divergences
):
first_target_divergences[access.address] = trace
if len(tx_frame_write_traces) > frame_write_trace_limit:
del tx_frame_write_traces[: len(tx_frame_write_traces) - frame_write_trace_limit]
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:
@@ -328,6 +1122,25 @@ def run_probe(
sci1=_sci1_snapshot(emulator),
sci1_txi=_sci1_txi_summary(emulator),
watch_snapshots=snapshots,
tx_frame_snapshots=tx_frame_snapshots,
tx_frame_write_traces=tx_frame_write_traces,
tx_target_divergences=[first_target_divergences[address] for address in sorted(first_target_divergences)],
report_queue_traces=report_queue_traces,
report_queue_first_writes=[first_report_queue_writes[index] for index in sorted(first_report_queue_writes)],
report_queue_first_nonzero_writes=[
first_report_queue_nonzero_writes[index] for index in sorted(first_report_queue_nonzero_writes)
],
report_queue_watch_hits=report_queue_watch_hits,
report_gate_traces=report_gate_traces,
ram_lifecycle_traces=ram_lifecycle_traces,
ram_lifecycle_last_writes=[
last_ram_lifecycle_writes[address] for address in sorted(last_ram_lifecycle_writes)
],
ram_lifecycle_last_nonzero_writes=[
last_ram_lifecycle_nonzero_writes[address] for address in sorted(last_ram_lifecycle_nonzero_writes)
],
final_tx_frame=_ram_window(emulator, TX_FRAME_START, TX_FRAME_LENGTH),
target_frame=target_frame,
unsupported=unsupported,
)
@@ -355,6 +1168,57 @@ def build_arg_parser() -> argparse.ArgumentParser:
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)
parser.add_argument(
"--tx-frame-watch",
action=argparse.BooleanOptionalAction,
default=True,
help="snapshot F850-F85D at known TX builder/send PCs",
)
parser.add_argument("--tx-frame-snapshot-limit", type=int, default=32)
parser.add_argument(
"--trace-frame-sources",
action="store_true",
help="trace writes to TX staging/frame RAM H'F850-H'F85D with PC/register provenance",
)
parser.add_argument("--frame-write-trace-limit", type=int, default=64)
parser.add_argument(
"--target-frame",
type=parse_tx_frame,
help="6-byte frame to compare against, e.g. \"00 00 00 00 80 DA\" or 0000000080DA",
)
parser.add_argument(
"--trace-report-queue",
action="store_true",
help="trace autonomous report queue entries/cursors at H'F870-H'F96F, H'F9B0, and H'F9B5",
)
parser.add_argument("--report-queue-trace-limit", type=int, default=64)
parser.add_argument(
"--watch-report-id",
action="append",
type=parse_watch_pc,
default=[],
help="highlight queue traces involving a logical report id, e.g. 00FF or 0x00FF",
)
parser.add_argument("--report-queue-watch-hit-limit", type=int, default=32)
parser.add_argument(
"--trace-report-gates",
action="store_true",
help="trace loc_4046 report enqueue gates: F9C4, FAA5.bit7, F9C3, and F9B0/F9B5",
)
parser.add_argument("--report-gate-trace-limit", type=int, default=64)
parser.add_argument(
"--trace-ram-lifecycle",
action="store_true",
help="trace writes to key RAM lifecycle bytes such as F9C4/F9C3/FAA5/F9B0/F9B5",
)
parser.add_argument(
"--ram-watch",
action="append",
type=parse_watch_pc,
default=[],
help="additional byte address to include in RAM lifecycle tracing, e.g. F9C4 or 0xF9C4",
)
parser.add_argument("--ram-lifecycle-trace-limit", type=int, default=64)
return parser
@@ -381,6 +1245,20 @@ def main(argv: list[str] | None = None) -> int:
watch_snapshot_limit=args.watch_snapshot_limit,
watch_pc_limit=args.watch_pc_limit,
watch_min_interval=args.watch_min_interval,
tx_frame_watch=args.tx_frame_watch,
tx_frame_snapshot_limit=args.tx_frame_snapshot_limit,
trace_frame_sources=args.trace_frame_sources or args.target_frame is not None,
frame_write_trace_limit=args.frame_write_trace_limit,
target_frame=args.target_frame,
trace_report_queue=args.trace_report_queue,
report_queue_trace_limit=args.report_queue_trace_limit,
watch_report_ids=tuple(args.watch_report_id),
report_queue_watch_hit_limit=args.report_queue_watch_hit_limit,
trace_report_gates=args.trace_report_gates,
report_gate_trace_limit=args.report_gate_trace_limit,
trace_ram_lifecycle=args.trace_ram_lifecycle,
ram_watch_addresses=tuple(args.ram_watch),
ram_lifecycle_trace_limit=args.ram_lifecycle_trace_limit,
)
for line in report.lines(hot_limit=args.hot_limit):
print(line)

View File

@@ -278,15 +278,17 @@ class H8536Emulator:
self._set_logic_flags(result, size)
elif op == 0x16:
self._set_logic_flags(self._read_ea(ea, size), size)
elif op == 0x10:
value = self._read_ea(ea, size)
result = ((value & 0xFF) << 8) | ((value >> 8) & 0xFF)
self._write_ea(ea, result, 2)
elif op in (0x10, 0x11, 0x12) and ea["mode"] == "reg" and size == 1:
reg = int(ea["reg"])
value = self.cpu.regs[reg] & 0xFFFF
if op == 0x10:
result = ((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF)
elif op == 0x11:
result = s8(value & 0xFF) & 0xFFFF
else:
result = value & 0x00FF
self.cpu.regs[reg] = result
self._set_logic_flags(result, 2)
elif op == 0x12:
value = self._read_ea(ea, size) & 0xFF
self._write_ea(ea, value, 2 if size == 2 else 1)
self._set_logic_flags(value, size)
elif op == 0x1A:
value = self._read_ea(ea, size)
result = (value << 1) & mask(size)

View File

@@ -19,6 +19,7 @@ KEY_STATE_ADDRESSES: tuple[int, ...] = (
0xF9C0,
0xF9C1,
0xF9C3,
0xF9C4,
0xF9C5,
0xF9C6,
0xF9C8,
@@ -52,6 +53,7 @@ def analyze_serial_gate(payload: dict[str, Any]) -> JsonObject:
"queue_send_gate_loc_BAF2": _queue_send_gate(by_address),
"resend_gate_path": _resend_gate_path(by_address),
"rx_session_maintenance": _rx_session_maintenance(by_address),
"idle_heartbeat_gate_loc_4046": _idle_heartbeat_gate(payload, by_address),
"timer_tick_evidence": _timer_tick_evidence(payload, by_address),
}
access_summary = _state_access_summary(instructions, labels)
@@ -101,6 +103,15 @@ def format_text_report(analysis: dict[str, Any]) -> str:
lines.append(" Candidate timer roles:")
for role in roles:
lines.append(f" - {role['address_hex']}: {role['role']}")
timer = section.get("timer")
if isinstance(timer, dict):
source = timer.get("source")
handler = timer.get("handler_address_hex")
ocra = timer.get("ocra_value_hex")
period = timer.get("observed_period_ms_candidate")
timer_bits = [str(part) for part in (source, handler, f"OCRA={ocra}" if ocra else "", f"observed period ~= {period}ms" if period else "") if part]
if timer_bits:
lines.append(f" Timer: {', '.join(timer_bits)}")
lines.extend(["", "State address readers/writers:"])
for entry in analysis.get("state_accesses", []):
@@ -273,6 +284,76 @@ def _rx_session_maintenance(by_address: dict[int, JsonObject]) -> JsonObject:
}
def _idle_heartbeat_gate(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
vector = _vector_entry(payload, 0x006A, "frt2_ocia")
handler = _int_field(vector, "target") if vector else None
if handler is None:
handler = 0xBF23
addresses = [
0x4046,
0x404A,
0x404C,
0x4050,
0x4052,
0x4056,
0x4058,
0x4059,
0x405F,
0x4063,
0x4067,
0x406C,
0x4070,
0x40E0,
0xBA31,
handler,
0xBF27,
0xBF2D,
]
present = _has_all(by_address, (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D))
return {
"title": "loc_4046 idle heartbeat/report gate",
"present": present,
"summary": (
"F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send "
"reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 "
"can enqueue H'00FF if the queue is empty and the FAA5/F9C3 RX gate permits it. With "
"FRT2 OCRA H'7A12 and CKS=phi/32, a phi near 10 MHz gives about 0.7s for H'07, matching "
"the observed heartbeat cadence."
),
"items": _items(by_address, addresses),
"gate_address_hex": h16(0x4046),
"queue_write_address_hex": h16(0x4067),
"initial_reload_address_hex": h16(0x40E0),
"post_tx_reload_address_hex": h16(0xBA31),
"tick_handler_address_hex": h16(handler),
"decrement_address_hex": h16(0xBF2D),
"initial_reload_value_hex": "H'14",
"post_tx_reload_value_hex": "H'07",
"timer": {
"source": "FRT2 OCIA",
"vector_address_hex": h16(0x006A),
"handler_address_hex": h16(handler),
"vector_target_label": str(vector.get("target_label", "")) if vector else "",
"tcr_address_hex": h16(0xFEA0),
"tcsr_address_hex": h16(0xFEA1),
"ocra_address_hex": h16(0xFEA4),
"ocra_value_hex": "H'7A12",
"clock_select": "CKS1=1 CKS0=0 => phi/32",
"observed_period_ms_candidate": 700,
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
},
"candidate_timer_roles": [
{
"address": 0xF9C4,
"address_hex": h16(0xF9C4),
"role": "candidate idle heartbeat/report gate countdown",
"evidence_address_hex": h16(0xBF2D),
}
],
"required_addresses_hex": [h16(address) for address in (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D)],
}
def _timer_tick_evidence(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
vector = _vector_entry(payload, 0x0062, "frt1_ocia")
handler = _int_field(vector, "target") if vector else None

View File

@@ -668,6 +668,15 @@ def _gate_queue_predicate_function_lines(value: object) -> list[str]:
" return MEM8[0xF9B5u] != MEM8[0xF9B0u];",
"}",
"",
"static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)",
"{",
" bool idle_timer_clear = MEM8[0xF9C4u] == 0u;",
" bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;",
" bool queue_empty = MEM8[0xF9B0u] == MEM8[0xF9B5u];",
"",
" return idle_timer_clear && rx_gate_open && queue_empty;",
"}",
"",
"static bool sci1_candidate_periodic_resend_gate_open(void)",
"{",
" bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;",
@@ -759,12 +768,21 @@ def _timer_architecture_comment_lines(
if not model:
return []
vector = str(model.get("vector_address_hex") or "H'BEEA")
source = str(model.get("source") or "FRT1 OCIA")
lines = [f"{prefix}interrupt/timer architecture candidate:"]
lines.append(
f"{prefix}- {source} {vector} appears to be a periodic tick ISR for serial gate/cadence counters.",
)
sources = _timer_source_models(model)
if sources:
for source_model in sources:
source = str(source_model.get("source") or "timer")
handler = str(source_model.get("handler_address_hex") or source_model.get("vector_address_hex") or "")
details = f" {handler}" if handler else ""
summary = _comment_text(str(source_model.get("summary") or "appears to be a periodic tick ISR for serial counters."))
lines.append(f"{prefix}- {source}{details}: {summary}")
else:
vector = str(model.get("vector_address_hex") or model.get("handler_address_hex") or "H'BEEA")
source = str(model.get("source") or "FRT1 OCIA")
lines.append(
f"{prefix}- {source} {vector} appears to be a periodic tick ISR for serial gate/cadence counters.",
)
counters = _timer_counter_models(model)
for counter in counters:
address = counter.get("address_hex") or _h(_int_field(counter, "address", 0))
@@ -781,14 +799,39 @@ def _timer_architecture_function_lines(protocol: JsonObject) -> list[str]:
model = _timer_architecture_model(protocol)
if not model:
return []
sources = _timer_source_models(model)
if sources:
lines: list[str] = []
for source_model in sources:
counters = _timer_counter_models(source_model)
if not counters:
continue
source = str(source_model.get("source") or "timer")
handler = str(source_model.get("handler_address_hex") or source_model.get("vector_address_hex") or "")
lines.extend(
_timer_tick_function_lines(
_timer_source_function_name(source),
counters,
f"Candidate periodic tick at {handler or source}: decrement nonzero serial counters.",
)
)
return lines
counters = _timer_counter_models(model)
if not counters:
return []
return _timer_tick_function_lines(
"frt1_ocia_candidate_tick_isr",
counters,
"Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters.",
)
def _timer_tick_function_lines(function_name: str, counters: list[JsonObject], summary: str) -> list[str]:
lines = [
"void frt1_ocia_candidate_tick_isr(void)",
f"void {function_name}(void)",
"{",
" /* Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters. */",
f" /* {_comment_text(summary)} */",
]
for counter in counters:
address = _int_field(counter, "address", 0)
@@ -808,14 +851,23 @@ def _timer_architecture_function_lines(protocol: JsonObject) -> list[str]:
return lines
def _timer_source_models(model: JsonObject) -> list[JsonObject]:
return _object_list(model.get("sources"))
def _timer_source_function_name(source: str) -> str:
root = _safe_identifier(source.lower().replace(" ", "_"))
return f"{root}_candidate_tick_isr"
def _timer_architecture_model(protocol: JsonObject) -> JsonObject:
model = protocol.get("timer_interrupt_model")
if isinstance(model, dict):
return model
if isinstance(protocol.get("gate_queue_model"), dict) or isinstance(protocol.get("periodic_resend_model"), dict):
return {
"source": "FRT1 OCIA",
"vector_address_hex": "H'BEEA",
"source": "FRT1/FRT2 OCIA",
"vector_address_hex": "H'BEEA/H'BF23",
"counters": [
{
"address": 0xF9C0,
@@ -835,6 +887,12 @@ def _timer_architecture_model(protocol: JsonObject) -> JsonObject:
"name_candidate": "periodic_resend_cadence_counter_candidate",
"role": "candidate periodic resend/heartbeat cadence counter.",
},
{
"address": 0xF9C4,
"address_hex": "H'F9C4",
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
"role": "candidate idle/default report enqueue countdown.",
},
],
}
return {}
@@ -863,6 +921,12 @@ def _timer_counter_models(model: JsonObject) -> list[JsonObject]:
"name_candidate": "periodic_resend_cadence_counter_candidate",
"role": "candidate periodic resend/heartbeat cadence counter.",
},
{
"address": 0xF9C4,
"address_hex": "H'F9C4",
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
"role": "candidate idle/default report enqueue countdown.",
},
]

View File

@@ -26,9 +26,14 @@ AUTONOMOUS_TX_REPORT_LABEL = "loc_BB43"
MAIN_REPORT_GATE_ENTRY = 0x3FD3
MAIN_REPORT_GATE_CALL = 0x3FEB
SESSION_GATE_ENTRY = 0x3FEF
IDLE_REPORT_GATE_ENTRY = 0x4046
IDLE_REPORT_QUEUE_WRITE = 0x4067
IDLE_REPORT_GATE_END = 0x4070
QUEUE_REPORT_ENTRY = 0xBAF2
RESEND_GATE_ENTRY = 0xBE9E
PERIODIC_RESEND_ENTRY = 0xBED5
FRT1_OCIA_ENTRY = 0xBEEA
FRT2_OCIA_ENTRY = 0xBF23
INDEX_DECODER_ADDRESS = 0x622B
INDEX_DECODER_LABEL = "loc_622B"
CHECKSUM_SEED = 0x5A
@@ -82,6 +87,8 @@ STATE_VARIABLES = {
0xF9B5: "event_queue_write_or_pending_cursor_candidate",
0xF9B9: "event_queue_base_or_current_slot_candidate",
0xF9C0: "serial_tx_busy_timer_candidate",
0xF9C4: "idle_heartbeat_gate_countdown_candidate",
0xF9C5: "rx_session_timeout_candidate",
0xF9C6: "autonomous_report_period_timer_candidate",
0xF9C8: "autonomous_report_resend_countdown_candidate",
}
@@ -132,6 +139,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"gate_queue_model": None,
"tx_report_model": None,
"periodic_resend_model": None,
"timer_interrupt_model": None,
"confidence": "low",
"confidence_score": 0.0,
"caveat": "No protocol semantics are emitted without both RX and TX serial reconstruction candidates.",
@@ -148,6 +156,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
gate_queue_model = _gate_queue_model(ordered, commands)
tx_report_model = _tx_report_model(ordered, responses)
periodic_resend_model = _periodic_resend_model(ordered, responses)
timer_interrupt_model = _timer_interrupt_model(ordered)
evidence = _top_level_evidence(ordered, dispatch, responses, rx_candidate, tx_candidate)
confidence_score = _confidence_score(frame_supported, dispatch, responses, commands)
@@ -202,6 +211,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"gate_queue_model": gate_queue_model,
"tx_report_model": tx_report_model,
"periodic_resend_model": periodic_resend_model,
"timer_interrupt_model": timer_interrupt_model,
"evidence": evidence,
}
return {
@@ -222,6 +232,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"gate_queue_model": protocol["gate_queue_model"],
"tx_report_model": protocol["tx_report_model"],
"periodic_resend_model": protocol["periodic_resend_model"],
"timer_interrupt_model": protocol["timer_interrupt_model"],
"confidence": protocol["confidence"],
"confidence_score": protocol["confidence_score"],
"caveat": protocol["caveat"],
@@ -1308,10 +1319,17 @@ def _logical_table_map_candidates(ordered: list[JsonObject]) -> list[JsonObject]
def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
candidates: list[JsonObject] = []
state_regions = [
(SERIAL_HANDLER_START, SERIAL_HANDLER_END),
(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END),
(0x40E0, 0x40E0),
(FRT1_OCIA_ENTRY, 0xBF08),
(FRT2_OCIA_ENTRY, 0xBF37),
]
serial_region = [
ins
for ins in ordered
if SERIAL_HANDLER_START <= int(ins.get("address", -1)) <= SERIAL_HANDLER_END
if any(start <= int(ins.get("address", -1)) <= end for start, end in state_regions)
]
if not any(
_has_ref_in_range(ins, min(STATE_VARIABLES), max(STATE_VARIABLES))
@@ -1366,8 +1384,8 @@ def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
"evidence_addresses_hex": _hlist(evidence),
"confidence": "candidate-medium",
"caveat": (
"Role is inferred from references in the serial handler region and remains "
"a state-variable candidate."
"Role is inferred from references in serial handler, gate, and timer regions "
"and remains a state-variable candidate."
),
}
)
@@ -1621,6 +1639,7 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
evidence = _dedupe_ints(
_addresses_in_ranges(ordered, [(MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)], MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)
+ _addresses_in_ranges(ordered, [(SESSION_GATE_ENTRY, 0x4007)], SESSION_GATE_ENTRY, 0x4007)
+ _addresses_in_ranges(ordered, [(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)], IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)
+ _addresses_in_ranges(ordered, [(QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)], QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)
+ _addresses_in_ranges(ordered, [(RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)], RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)
)
@@ -1655,6 +1674,26 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
MAIN_REPORT_GATE_CALL,
),
},
{
"name": "idle_heartbeat_report_may_enqueue",
"entry_label": "loc_4046",
"target_label": "loc_4067",
"condition_candidate": (
"F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5"
),
"summary": (
"Idle/default report gate; when the FRT2 countdown clears and the queue is "
"empty, loc_4046 can enqueue H'00FF for the later loc_BAF2 -> loc_BA26 send path."
),
"state_addresses_hex": [_h16(0xF9C4), _h16(0xFAA5), _h16(0xF9C3), _h16(0xF9B0), _h16(0xF9B5)],
"enqueued_report_candidate_hex": _h16(0x00FF),
"evidence_addresses": _addresses_in_ranges(
ordered,
[(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)],
IDLE_REPORT_GATE_ENTRY,
IDLE_REPORT_GATE_END,
),
},
{
"name": "queue_has_pending_report",
"entry_label": "loc_BAF2",
@@ -1705,6 +1744,20 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
0x4007,
),
},
{
"name": "idle_heartbeat_gate_initial_delay_loaded",
"summary": "Startup/init loads F9C4 with H'14 before the first idle/default report can be queued.",
"state_addresses_hex": [_h16(0xF9C4)],
"reload_value_hex": _h16(0x14, width=2),
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C4, 0x14),
},
{
"name": "idle_heartbeat_gate_post_send_delay_loaded",
"summary": "loc_BA26 reloads F9C4 with H'07 after each send, matching the observed heartbeat spacing.",
"state_addresses_hex": [_h16(0xF9C4)],
"reload_value_hex": _h16(0x07, width=2),
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C4, 0x07),
},
{
"name": "host_ack_can_advance_queue",
"summary": "Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.",
@@ -1729,6 +1782,111 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
}
def _timer_interrupt_model(ordered: list[JsonObject]) -> JsonObject | None:
present_addresses = {int(ins["address"]) for ins in ordered if isinstance(ins.get("address"), int)}
frt1_evidence = _dedupe_ints(
_addresses_in_ranges(ordered, [(FRT1_OCIA_ENTRY, 0xBF08)], FRT1_OCIA_ENTRY, 0xBF08)
)
frt2_evidence = _dedupe_ints(
_addresses_in_ranges(ordered, [(FRT2_OCIA_ENTRY, 0xBF37)], FRT2_OCIA_ENTRY, 0xBF37)
)
sources: list[JsonObject] = []
if frt1_evidence:
frt1_counters = [
counter
for counter in (
_timer_counter(0xF9C0, "tx_report_gate_counter_candidate", "candidate gate counter used before entering the report builder.", 0xBEF4),
_timer_counter(0xF9C1, "rx_interbyte_timeout_candidate", "candidate RX interbyte timeout counter.", 0xBEFE),
_timer_counter(0xF9C6, "periodic_resend_cadence_counter_candidate", "candidate periodic resend/heartbeat cadence counter.", 0xBF08),
)
if int(counter["evidence_address"]) in present_addresses
]
sources.append(
{
"source": "FRT1 OCIA",
"vector_address_hex": _h16(0x0062),
"handler_address": FRT1_OCIA_ENTRY,
"handler_address_hex": _h16(FRT1_OCIA_ENTRY),
"summary": "Candidate periodic tick ISR for serial busy, interbyte, and resend counters.",
"counters": frt1_counters,
"evidence_addresses": frt1_evidence,
"evidence_addresses_hex": _hlist(frt1_evidence),
}
)
if frt2_evidence:
frt2_counters = [
counter
for counter in (
_timer_counter(0xF9C4, "idle_heartbeat_gate_countdown_candidate", "candidate idle/default report enqueue countdown.", 0xBF2D),
_timer_counter(0xF9C5, "rx_session_timeout_candidate", "candidate RX/session maintenance timeout counter.", 0xBF37),
)
if int(counter["evidence_address"]) in present_addresses
]
sources.append(
{
"source": "FRT2 OCIA",
"vector_address_hex": _h16(0x006A),
"handler_address": FRT2_OCIA_ENTRY,
"handler_address_hex": _h16(FRT2_OCIA_ENTRY),
"summary": "Candidate periodic tick ISR for idle heartbeat/report and RX session counters.",
"clock_select": "CKS1=1 CKS0=0 => phi/32",
"ocra_value_hex": "H'7A12",
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
"counters": frt2_counters,
"evidence_addresses": frt2_evidence,
"evidence_addresses_hex": _hlist(frt2_evidence),
}
)
if not sources:
return None
counters = _dedupe_timer_counters(
counter
for source in sources
for counter in source.get("counters", [])
if isinstance(counter, dict)
)
evidence = _dedupe_ints(
addr
for source in sources
for addr in source.get("evidence_addresses", [])
if isinstance(addr, int)
)
return {
"kind": "timer_interrupt_model_candidate",
"source": " / ".join(str(source["source"]) for source in sources),
"summary": "FRT compare-match handlers decrement serial gate, timeout, and cadence counters.",
"sources": sources,
"counters": counters,
"evidence_addresses": evidence,
"evidence_addresses_hex": _hlist(evidence),
"confidence": "candidate-medium",
}
def _timer_counter(address: int, name: str, role: str, evidence_address: int) -> JsonObject:
return {
"address": address,
"address_hex": _h16(address),
"name_candidate": name,
"role": role,
"evidence_address": evidence_address,
"evidence_address_hex": _h16(evidence_address),
}
def _dedupe_timer_counters(counters: Iterable[JsonObject]) -> list[JsonObject]:
output = []
seen: set[int] = set()
for counter in counters:
address = counter.get("address")
if not isinstance(address, int) or address in seen:
continue
seen.add(address)
output.append(counter)
return output
def _tx_report_model(ordered: list[JsonObject], responses: list[JsonObject]) -> JsonObject | None:
report_responses = [
response for response in responses