EMualtor adjustments
This commit is contained in:
63
tests/test_emulator_addressing.py
Normal file
63
tests/test_emulator_addressing.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import unittest
|
||||
|
||||
from h8536.emulator import H8536Emulator
|
||||
|
||||
|
||||
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
|
||||
rom = bytearray([0xFF] * size)
|
||||
rom[0:2] = reset.to_bytes(2, "big")
|
||||
return rom
|
||||
|
||||
|
||||
class EmulatorAddressingTest(unittest.TestCase):
|
||||
def test_txi_indexed_byte_load_uses_signed_word_displacement_and_full_index_register(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1004] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
|
||||
|
||||
emulator = H8536Emulator(bytes(rom))
|
||||
emulator.cpu.regs[0] = 5
|
||||
emulator.memory.write8(0xF85D, 0xA6)
|
||||
emulator.step()
|
||||
|
||||
self.assertEqual(emulator.cpu.regs[0], 0x00A6)
|
||||
self.assertEqual(emulator.cpu.pc, 0x1004)
|
||||
|
||||
def test_txi_indexed_byte_load_preserves_destination_high_byte(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1004] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
|
||||
|
||||
emulator = H8536Emulator(bytes(rom))
|
||||
emulator.cpu.regs[0] = 0x0105
|
||||
emulator.memory.write8(0xF95D, 0x4B)
|
||||
emulator.step()
|
||||
|
||||
self.assertEqual(emulator.cpu.regs[0], 0x014B)
|
||||
|
||||
def test_extu_byte_clears_high_byte_before_txi_indexed_load(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1002] = b"\xA0\x12" # EXTU.B R0
|
||||
rom[0x1002:0x1006] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
|
||||
|
||||
emulator = H8536Emulator(bytes(rom))
|
||||
emulator.cpu.regs[0] = 0x0105
|
||||
emulator.memory.write8(0xF85D, 0xC7)
|
||||
emulator.memory.write8(0xF95D, 0x99)
|
||||
emulator.run(max_steps=2)
|
||||
|
||||
self.assertEqual(emulator.cpu.regs[0], 0x00C7)
|
||||
self.assertEqual(emulator.cpu.pc, 0x1006)
|
||||
|
||||
def test_signed_byte_displacement_addresses_below_base(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1003] = b"\xE1\xFE\x81" # MOV:G.B @(H'-02,R1), R1
|
||||
|
||||
emulator = H8536Emulator(bytes(rom))
|
||||
emulator.cpu.regs[1] = 0xFE90
|
||||
emulator.memory.write8(0xFE8E, 0x37)
|
||||
emulator.step()
|
||||
|
||||
self.assertEqual(emulator.cpu.regs[1], 0xFE37)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -64,6 +64,8 @@ class P9FastPathTest(unittest.TestCase):
|
||||
self.assertEqual(emulator.cpu.regs[7], 0xFE7E)
|
||||
self.assertEqual(fast_path.events[-1].kind, "read_byte")
|
||||
self.assertEqual(fast_path.events[-1].value, 0x3C)
|
||||
self.assertEqual(fast_path.events[-1].source, "initial")
|
||||
self.assertEqual(fast_path.events[-1].queue_depth, 0)
|
||||
self.assertFalse(emulator.cpu.z)
|
||||
self.assertFalse(emulator.cpu.n)
|
||||
self.assertFalse(emulator.cpu.v)
|
||||
@@ -79,9 +81,30 @@ class P9FastPathTest(unittest.TestCase):
|
||||
self.assertTrue(fast_path.try_handle(emulator))
|
||||
self.assertEqual(emulator.cpu.regs[5], 0x0081)
|
||||
self.assertEqual(emulator.cpu.pc, 0x5678)
|
||||
self.assertEqual(fast_path.events[-1].source, "default_input_byte")
|
||||
self.assertEqual(fast_path.events[-1].queue_depth, 0)
|
||||
self.assertIn("source=default_input_byte", fast_path.events[-1].line())
|
||||
self.assertFalse(emulator.cpu.z)
|
||||
self.assertTrue(emulator.cpu.n)
|
||||
|
||||
def test_named_input_script_records_read_source_and_remaining_depth(self):
|
||||
emulator = H8536Emulator(bytes(rom_with_reset()))
|
||||
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
|
||||
emulator.cpu.regs[7] = 0xFE82
|
||||
emulator.memory.write16(0xFE82, 0x6789)
|
||||
|
||||
fast_path = P9FastPath(P9FastPathConfig(enabled=True))
|
||||
fast_path.queue_input_script("idle-panel", [0x00, 0x80])
|
||||
|
||||
self.assertTrue(fast_path.try_handle(emulator))
|
||||
event = fast_path.events[-1]
|
||||
self.assertEqual(emulator.cpu.regs[5], 0x0000)
|
||||
self.assertEqual(event.kind, "read_byte")
|
||||
self.assertEqual(event.value, 0x00)
|
||||
self.assertEqual(event.source, "script:idle-panel")
|
||||
self.assertEqual(event.queue_depth, 1)
|
||||
self.assertEqual(fast_path.trace_lines(), ["read_byte pc=C0DB value=00 source=script:idle-panel queued=1"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import unittest
|
||||
from collections import Counter
|
||||
|
||||
from h8536.emulator.probe import DEFAULT_WATCH_PCS, parse_watch_pc, run_probe
|
||||
from h8536.emulator.probe import (
|
||||
DEFAULT_WATCH_PCS,
|
||||
ProbeReport,
|
||||
RAMLifecycleTrace,
|
||||
ReportGateTrace,
|
||||
ReportQueueTrace,
|
||||
TXFrameSnapshot,
|
||||
TXFrameWriteTrace,
|
||||
parse_watch_pc,
|
||||
parse_tx_frame,
|
||||
run_probe,
|
||||
)
|
||||
|
||||
|
||||
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
|
||||
@@ -15,6 +27,13 @@ class EmulatorProbeTest(unittest.TestCase):
|
||||
self.assertEqual(parse_watch_pc("0xC08B"), 0xC08B)
|
||||
self.assertEqual(parse_watch_pc("H'C08B"), 0xC08B)
|
||||
|
||||
def test_parse_tx_frame_accepts_spaced_and_compact_hex(self):
|
||||
expected = bytes.fromhex("00 00 00 00 80 DA")
|
||||
|
||||
self.assertEqual(parse_tx_frame("00 00 00 00 80 DA"), expected)
|
||||
self.assertEqual(parse_tx_frame("0000000080DA"), expected)
|
||||
self.assertEqual(parse_tx_frame("00,00,00,00,80,DA"), expected)
|
||||
|
||||
def test_default_watch_pcs_include_bit_bang_transfer_path(self):
|
||||
self.assertIn(0xC08B, DEFAULT_WATCH_PCS)
|
||||
self.assertIn(0xC0DB, DEFAULT_WATCH_PCS)
|
||||
@@ -51,6 +70,291 @@ class EmulatorProbeTest(unittest.TestCase):
|
||||
self.assertIn("H'1005<-H'1003", snapshot.line())
|
||||
self.assertTrue(any("recent_watch_snapshots:" == line for line in report.lines()))
|
||||
|
||||
def test_report_lines_include_compact_tx_frame_snapshots(self):
|
||||
staging_and_frame = bytes.fromhex("11 22 33 44 55 66 77 88 00 00 00 00 80 DA")
|
||||
report = ProbeReport(
|
||||
steps=99,
|
||||
pc=0xBA72,
|
||||
stopped_reason="tx",
|
||||
hot_pcs=Counter({0xBA72: 1}),
|
||||
tx_frame_snapshots=[
|
||||
TXFrameSnapshot(
|
||||
step=98,
|
||||
pc=0xBA72,
|
||||
label="first_tdr",
|
||||
bytes_f850_f85d=staging_and_frame,
|
||||
computed_checksum=0xDA,
|
||||
stored_checksum=0xDA,
|
||||
checksum_ok=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
lines = report.lines()
|
||||
|
||||
self.assertIn("recent_tx_frame_snapshots:", lines)
|
||||
self.assertIn(
|
||||
" step=98 pc=H'BA72 first_tdr "
|
||||
"F850-F85D=11 22 33 44 55 66 77 88 00 00 00 00 80 DA "
|
||||
"TX=00 00 00 00 80 DA computed=DA stored=DA checksum_ok=1",
|
||||
lines,
|
||||
)
|
||||
|
||||
def test_report_lines_include_target_frame_diff_and_write_sources(self):
|
||||
report = ProbeReport(
|
||||
steps=10,
|
||||
pc=0x1004,
|
||||
stopped_reason="max_steps",
|
||||
hot_pcs=Counter({0x1000: 1}),
|
||||
final_tx_frame=bytes.fromhex("00 01 7F 00 00 24"),
|
||||
target_frame=bytes.fromhex("00 00 00 00 80 DA"),
|
||||
tx_frame_write_traces=[
|
||||
TXFrameWriteTrace(
|
||||
step=2,
|
||||
pc=0x1003,
|
||||
address=0xF859,
|
||||
old_value=0x00,
|
||||
new_value=0x01,
|
||||
frame_after=bytes.fromhex("00 01 00 00 00 00"),
|
||||
instruction="H'1003: 1D F8 58 90 MOV:G.W R0, @H'F858",
|
||||
regs=(0x017F, 0, 0, 0, 0, 0, 0, 0),
|
||||
target_value=0x00,
|
||||
)
|
||||
],
|
||||
tx_target_divergences=[
|
||||
TXFrameWriteTrace(
|
||||
step=2,
|
||||
pc=0x1003,
|
||||
address=0xF859,
|
||||
old_value=0x00,
|
||||
new_value=0x01,
|
||||
frame_after=bytes.fromhex("00 01 00 00 00 00"),
|
||||
instruction="H'1003: 1D F8 58 90 MOV:G.W R0, @H'F858",
|
||||
regs=(0x017F, 0, 0, 0, 0, 0, 0, 0),
|
||||
target_value=0x00,
|
||||
)
|
||||
],
|
||||
report_queue_traces=[
|
||||
ReportQueueTrace(
|
||||
step=3,
|
||||
pc=0x1007,
|
||||
kind="queue_write",
|
||||
head=0,
|
||||
tail=0,
|
||||
queue_index=0,
|
||||
address=0xF870,
|
||||
old_word=0x0000,
|
||||
new_word=0x00FF,
|
||||
instruction="H'1003: 1D F8 70 93 MOV:G.W R3, @H'F870",
|
||||
regs=(0, 0, 0, 0x00FF, 0, 0, 0, 0),
|
||||
)
|
||||
],
|
||||
report_gate_traces=[
|
||||
ReportGateTrace(
|
||||
step=4,
|
||||
pc=0x4050,
|
||||
label="faa5_clear_enqueue_branch",
|
||||
f9c4=0,
|
||||
faa5=0,
|
||||
f9c3=0,
|
||||
head=0,
|
||||
tail=0,
|
||||
regs=(0, 0, 0, 0, 0, 0, 0, 0),
|
||||
z=True,
|
||||
c=False,
|
||||
n=False,
|
||||
decision="enqueue_candidate_faa5_clear",
|
||||
f9c4_last_write_step=3,
|
||||
f9c4_last_write_pc=0x40E0,
|
||||
f9c4_last_write_value=0x00,
|
||||
f9c4_last_write_age=1,
|
||||
f9c4_last_nonzero_step=2,
|
||||
f9c4_last_nonzero_pc=0x40D8,
|
||||
f9c4_last_nonzero_value=0x14,
|
||||
f9c4_last_nonzero_age=2,
|
||||
)
|
||||
],
|
||||
ram_lifecycle_traces=[
|
||||
RAMLifecycleTrace(
|
||||
step=3,
|
||||
pc=0x40E0,
|
||||
address=0xF9C4,
|
||||
name="F9C4_report_gate_timer",
|
||||
old_value=0x14,
|
||||
new_value=0x00,
|
||||
instruction="H'40E0: 15 F9 C4 13 CLR.B @H'F9C4",
|
||||
regs=(0, 0, 0, 0, 0, 0, 0, 0),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
lines = report.lines()
|
||||
|
||||
self.assertIn(
|
||||
"target_frame=target=00 00 00 00 80 DA current=00 01 7F 00 00 24 "
|
||||
"diffs=1:01!=00 2:7F!=00 4:00!=80 5:24!=DA pre_checksum_diffs=1,2,4",
|
||||
lines,
|
||||
)
|
||||
self.assertIn("recent_tx_frame_writes:", lines)
|
||||
self.assertIn("first_target_divergences:", lines)
|
||||
self.assertIn("recent_report_queue:", lines)
|
||||
self.assertIn("recent_report_gates:", lines)
|
||||
self.assertIn("recent_ram_lifecycle:", lines)
|
||||
self.assertTrue(any("TX[1]" in line and "target=00 DIFF" in line for line in lines))
|
||||
self.assertTrue(any("queue_write" in line and "word=H'0000->H'00FF" in line for line in lines))
|
||||
self.assertTrue(any("decision=enqueue_candidate_faa5_clear" in line for line in lines))
|
||||
self.assertTrue(any("F9C4_last=step=3@H'40E0 value=00 age=1" in line for line in lines))
|
||||
self.assertTrue(any("F9C4_last_nonzero=step=2@H'40D8 value=14 age=2" in line for line in lines))
|
||||
self.assertTrue(any("F9C4_report_gate_timer H'F9C4 14->00" in line for line in lines))
|
||||
|
||||
def test_run_probe_captures_tx_frame_watch_pc_and_bad_checksum(self):
|
||||
rom = rom_with_reset(reset=0xBA26, size=0xBB00)
|
||||
rom[0xBA26] = 0xFF
|
||||
|
||||
report = run_probe(
|
||||
bytes(rom),
|
||||
max_steps=1,
|
||||
interval_steps=512,
|
||||
stop_on_tx=False,
|
||||
p9_log_limit=8,
|
||||
watch_pcs=(),
|
||||
tx_frame_snapshot_limit=4,
|
||||
)
|
||||
|
||||
self.assertEqual(len(report.tx_frame_snapshots), 1)
|
||||
snapshot = report.tx_frame_snapshots[0]
|
||||
self.assertEqual(snapshot.pc, 0xBA26)
|
||||
self.assertEqual(snapshot.label, "builder_entry")
|
||||
self.assertEqual(snapshot.frame_bytes, b"\x00\x00\x00\x00\x00\x00")
|
||||
self.assertEqual(snapshot.computed_checksum, 0x5A)
|
||||
self.assertEqual(snapshot.stored_checksum, 0x00)
|
||||
self.assertFalse(snapshot.checksum_ok)
|
||||
|
||||
def test_run_probe_traces_tx_frame_writes_against_target(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1003] = b"\x58\x01\x7F" # MOV:I.W #H'017F, R0
|
||||
rom[0x1003:0x1007] = b"\x1D\xF8\x58\x90" # MOV:G.W R0, @H'F858
|
||||
|
||||
report = run_probe(
|
||||
bytes(rom),
|
||||
max_steps=2,
|
||||
interval_steps=512,
|
||||
stop_on_tx=False,
|
||||
p9_log_limit=8,
|
||||
watch_pcs=(),
|
||||
trace_frame_sources=True,
|
||||
target_frame=bytes.fromhex("00 00 00 00 80 DA"),
|
||||
frame_write_trace_limit=4,
|
||||
)
|
||||
|
||||
self.assertEqual(report.final_tx_frame[:3], b"\x01\x7F\x00")
|
||||
self.assertEqual(len(report.tx_frame_write_traces), 2)
|
||||
self.assertEqual(report.tx_frame_write_traces[0].address, 0xF858)
|
||||
self.assertEqual(report.tx_frame_write_traces[0].target_value, 0x00)
|
||||
self.assertIn("MOV:G.W R0, @H'F858", report.tx_frame_write_traces[0].instruction)
|
||||
self.assertEqual(report.tx_target_divergences, [])
|
||||
self.assertTrue(any("target_frame=" in line for line in report.lines()))
|
||||
|
||||
def test_run_probe_traces_report_queue_writes_and_cursors(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1003] = b"\x5B\x00\xFF" # MOV:I.W #H'00FF, R3
|
||||
rom[0x1003:0x1007] = b"\x1D\xF8\x70\x93" # MOV:G.W R3, @H'F870
|
||||
rom[0x1007:0x100B] = b"\x15\xF9\xB0\x08" # ADD:Q.B #1, @H'F9B0
|
||||
|
||||
report = run_probe(
|
||||
bytes(rom),
|
||||
max_steps=3,
|
||||
interval_steps=512,
|
||||
stop_on_tx=False,
|
||||
p9_log_limit=8,
|
||||
watch_pcs=(),
|
||||
trace_report_queue=True,
|
||||
report_queue_trace_limit=8,
|
||||
watch_report_ids=(0x00FF,),
|
||||
)
|
||||
|
||||
queue_writes = [trace for trace in report.report_queue_traces if trace.kind == "queue_write"]
|
||||
cursor_writes = [trace for trace in report.report_queue_traces if trace.kind == "cursor_head_write"]
|
||||
|
||||
self.assertEqual(len(queue_writes), 1)
|
||||
self.assertEqual(queue_writes[0].queue_index, 0)
|
||||
self.assertEqual(queue_writes[0].old_word, 0x0000)
|
||||
self.assertEqual(queue_writes[0].new_word, 0x00FF)
|
||||
self.assertEqual(report.report_queue_first_writes, queue_writes)
|
||||
self.assertEqual(report.report_queue_first_nonzero_writes, queue_writes)
|
||||
self.assertEqual(report.report_queue_watch_hits, queue_writes)
|
||||
self.assertEqual(len(cursor_writes), 1)
|
||||
self.assertEqual(cursor_writes[0].old_value, 0x00)
|
||||
self.assertEqual(cursor_writes[0].new_value, 0x01)
|
||||
self.assertIn("recent_report_queue:", report.lines())
|
||||
|
||||
def test_run_probe_traces_ram_lifecycle_writes_and_last_nonzero_value(self):
|
||||
rom = rom_with_reset()
|
||||
rom[0x1000:0x1005] = b"\x15\xF9\xC4\x06\x14" # MOV:G.B #H'14, @H'F9C4
|
||||
rom[0x1005:0x1009] = b"\x15\xF9\xC4\x13" # CLR.B @H'F9C4
|
||||
|
||||
report = run_probe(
|
||||
bytes(rom),
|
||||
max_steps=2,
|
||||
interval_steps=512,
|
||||
stop_on_tx=False,
|
||||
p9_log_limit=8,
|
||||
watch_pcs=(),
|
||||
trace_ram_lifecycle=True,
|
||||
ram_lifecycle_trace_limit=4,
|
||||
)
|
||||
|
||||
self.assertEqual(len(report.ram_lifecycle_traces), 2)
|
||||
set_trace, clear_trace = report.ram_lifecycle_traces
|
||||
self.assertEqual(set_trace.address, 0xF9C4)
|
||||
self.assertEqual(set_trace.old_value, 0x00)
|
||||
self.assertEqual(set_trace.new_value, 0x14)
|
||||
self.assertEqual(clear_trace.old_value, 0x14)
|
||||
self.assertEqual(clear_trace.new_value, 0x00)
|
||||
self.assertEqual(report.ram_lifecycle_last_writes, [clear_trace])
|
||||
self.assertEqual(report.ram_lifecycle_last_nonzero_writes, [set_trace])
|
||||
lines = report.lines()
|
||||
self.assertIn("recent_ram_lifecycle:", lines)
|
||||
self.assertIn("ram_lifecycle_last_writes:", lines)
|
||||
self.assertIn("ram_lifecycle_last_nonzero_writes:", lines)
|
||||
|
||||
def test_run_probe_traces_loc_4046_report_gates(self):
|
||||
rom = rom_with_reset(reset=0x4046, size=0x4080)
|
||||
rom[0x4046:0x404A] = b"\x15\xF9\xC4\x16" # TST.B @H'F9C4
|
||||
rom[0x404A:0x404C] = b"\x26\x0C" # BNE H'4058
|
||||
rom[0x404C:0x4050] = b"\x15\xFA\xA5\xF7" # BTST.B #7, @H'FAA5
|
||||
rom[0x4050:0x4052] = b"\x27\x07" # BEQ H'4059
|
||||
rom[0x4052:0x4056] = b"\x15\xF9\xC3\x16" # TST.B @H'F9C3
|
||||
rom[0x4056:0x4058] = b"\x27\x01" # BEQ H'4059
|
||||
rom[0x4058] = 0x19 # RTS
|
||||
rom[0x4059:0x405D] = b"\x15\xF9\xB0\x82" # MOV:G.B @H'F9B0, R2
|
||||
rom[0x405D:0x405F] = b"\xA2\x12" # EXTU.B R2
|
||||
rom[0x405F:0x4063] = b"\x15\xF9\xB5\x72" # CMP:G.B @H'F9B5, R2
|
||||
rom[0x4063:0x4065] = b"\x26\x0F" # BNE H'4074
|
||||
rom[0x4065:0x4067] = b"\xA2\x1A" # SHLL.B R2
|
||||
rom[0x4067:0x406C] = b"\xFA\xF8\x70\x06\x00" # MOV:G.W #H'00, @(-H'0790,R2)
|
||||
rom[0x406C:0x4070] = b"\x15\xF9\xB0\x08" # ADD:Q.B #1, @H'F9B0
|
||||
rom[0x4070:0x4074] = b"\x15\xF9\xB0\xD7" # BCLR.B #7, @H'F9B0
|
||||
|
||||
report = run_probe(
|
||||
bytes(rom),
|
||||
max_steps=10,
|
||||
interval_steps=512,
|
||||
stop_on_tx=False,
|
||||
p9_log_limit=8,
|
||||
watch_pcs=(),
|
||||
trace_report_gates=True,
|
||||
report_gate_trace_limit=16,
|
||||
)
|
||||
|
||||
decisions = {trace.pc: trace.decision for trace in report.report_gate_traces}
|
||||
|
||||
self.assertEqual(decisions[0x4046], "f9c4_zero_continue")
|
||||
self.assertEqual(decisions[0x4050], "enqueue_candidate_faa5_clear")
|
||||
self.assertEqual(decisions[0x4063], "enqueue_zero_report")
|
||||
self.assertEqual(decisions[0x4067], "write_report_00ff_to_queue_slot")
|
||||
self.assertTrue(any("recent_report_gates:" == line for line in report.lines()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -41,6 +41,20 @@ def fixture_payload() -> dict[str, object]:
|
||||
ins(0x3FEF, "TST.B @H'F9C5", references=[0xF9C5]),
|
||||
ins(0x3FF5, "CLR.B @H'F9B5", references=[0xF9B5]),
|
||||
ins(0x3FF9, "CLR.B @H'F9B0", references=[0xF9B0]),
|
||||
ins(0x4046, "TST.B @H'F9C4", references=[0xF9C4]),
|
||||
ins(0x404A, "BNE loc_4058", targets=[0x4058]),
|
||||
ins(0x404C, "BTST.B #7, @H'FAA5", references=[0xFAA5]),
|
||||
ins(0x4050, "BEQ loc_4059", targets=[0x4059]),
|
||||
ins(0x4052, "TST.B @H'F9C3", references=[0xF9C3]),
|
||||
ins(0x4056, "BEQ loc_4059", targets=[0x4059]),
|
||||
ins(0x4058, "RTS"),
|
||||
ins(0x4059, "MOV:G.B @H'F9B0, R2", references=[0xF9B0]),
|
||||
ins(0x405F, "CMP:G.B @H'F9B5, R2", references=[0xF9B5]),
|
||||
ins(0x4063, "BNE loc_4074", targets=[0x4074]),
|
||||
ins(0x4067, "MOV:G.W #H'00, @(-H'0790,R2)"),
|
||||
ins(0x406C, "ADD:Q.B #1, @H'F9B0", references=[0xF9B0]),
|
||||
ins(0x4070, "BCLR.B #7, @H'F9B0", references=[0xF9B0]),
|
||||
ins(0x40E0, "MOV:G.B #H'14, @H'F9C4", references=[0xF9C4]),
|
||||
ins(0xBAF2, "MOV:G.B @H'F9B5, R1", references=[0xF9B5]),
|
||||
ins(0xBAF8, "CMP:G.B @H'F9B0, R1", references=[0xF9B0]),
|
||||
ins(0xBAFC, "BNE loc_BB00", targets=[0xBB00]),
|
||||
@@ -67,6 +81,7 @@ def fixture_payload() -> dict[str, object]:
|
||||
ins(0xBEA5, "AND.B @H'FAA3, R0", references=[0xFAA3]),
|
||||
ins(0xBEA9, "MOV:G.B R0, @H'FAA3", references=[0xFAA3]),
|
||||
ins(0xBEAF, "CLR.B @H'FAA2", references=[0xFAA2]),
|
||||
ins(0xBA31, "MOV:G.B #H'07, @H'F9C4", references=[0xF9C4]),
|
||||
ins(0xBEB5, "TST.W @H'F9C6", references=[0xF9C6]),
|
||||
ins(0xBEBB, "TST.B @H'F9C8", references=[0xF9C8]),
|
||||
ins(0xBEC5, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]),
|
||||
@@ -83,9 +98,15 @@ def fixture_payload() -> dict[str, object]:
|
||||
ins(0xBF02, "TST.W @H'F9C6", references=[0xF9C6]),
|
||||
ins(0xBF06, "BEQ loc_BF0C", targets=[0xBF0C]),
|
||||
ins(0xBF08, "ADD:Q.W #-1, @H'F9C6", references=[0xF9C6]),
|
||||
ins(0xBF23, "BCLR.B #5, @FRT2_TCSR"),
|
||||
ins(0xBF27, "TST.B @H'F9C4", references=[0xF9C4]),
|
||||
ins(0xBF2D, "ADD:Q.B #-1, @H'F9C4", references=[0xF9C4]),
|
||||
]
|
||||
return {
|
||||
"vectors": [{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"}],
|
||||
"vectors": [
|
||||
{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"},
|
||||
{"address": 0x006A, "name": "frt2_ocia", "target": 0xBF23, "target_label": "vec_frt2_ocia_BF23"},
|
||||
],
|
||||
"call_graph": {"nodes": [{"start": 0x3FD3, "label": "loc_3FD3"}, {"start": 0xBAF2, "label": "loc_BAF2"}]},
|
||||
"instructions": rows,
|
||||
}
|
||||
@@ -119,6 +140,17 @@ class SerialGateTest(unittest.TestCase):
|
||||
self.assertIn("secondary delay", roles[0xF9C1]["role"])
|
||||
self.assertIn("periodic report/heartbeat", roles[0xF9C6]["role"])
|
||||
|
||||
def test_json_analysis_includes_frt2_idle_heartbeat_gate(self):
|
||||
analysis = analyze_serial_gate(fixture_payload())
|
||||
gate = analysis["evidence"]["idle_heartbeat_gate_loc_4046"]
|
||||
|
||||
self.assertTrue(gate["present"])
|
||||
self.assertEqual(gate["timer"]["handler_address_hex"], "H'BF23")
|
||||
self.assertEqual(gate["post_tx_reload_value_hex"], "H'07")
|
||||
self.assertIn("0.7s", gate["summary"])
|
||||
roles = {role["address"]: role for role in gate["candidate_timer_roles"]}
|
||||
self.assertIn("heartbeat", roles[0xF9C4]["role"])
|
||||
|
||||
def test_summarizes_key_state_readers_and_writers(self):
|
||||
analysis = analyze_serial_gate(fixture_payload())
|
||||
accesses = {entry["address"]: entry for entry in analysis["state_accesses"]}
|
||||
@@ -126,6 +158,7 @@ class SerialGateTest(unittest.TestCase):
|
||||
self.assertGreaterEqual(accesses[0xF9B5]["read_count"], 1)
|
||||
self.assertGreaterEqual(accesses[0xF9B5]["read_write_count"], 1)
|
||||
self.assertGreaterEqual(accesses[0xF9C1]["read_write_count"], 1)
|
||||
self.assertGreaterEqual(accesses[0xF9C4]["read_write_count"], 1)
|
||||
self.assertGreaterEqual(accesses[0xFAA3]["write_count"], 1)
|
||||
self.assertIn("sample_accesses", accesses[0xFAA2])
|
||||
|
||||
@@ -145,6 +178,14 @@ class SerialGateTest(unittest.TestCase):
|
||||
self.assertIn("H'F9C1: candidate secondary delay countdown", text)
|
||||
self.assertIn("H'F9C6: candidate periodic report/heartbeat countdown", text)
|
||||
|
||||
def test_text_report_mentions_frt2_idle_heartbeat_gate(self):
|
||||
text = format_text_report(analyze_serial_gate(fixture_payload()))
|
||||
|
||||
self.assertIn("loc_4046 idle heartbeat/report gate: present", text)
|
||||
self.assertIn("FRT2 OCIA", text)
|
||||
self.assertIn("H'F9C4: candidate idle heartbeat/report gate countdown", text)
|
||||
self.assertIn("observed period ~= 700ms", text)
|
||||
|
||||
def test_cli_json_output_and_out_file(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
input_path = Path(tmp) / "rom.json"
|
||||
|
||||
@@ -403,6 +403,7 @@ class SerialPseudocodeTest(unittest.TestCase):
|
||||
self.assertIn("host_ack_can_advance_queue: Commands 0x05/0x06 can ack or advance F9B5", text)
|
||||
self.assertIn("static bool sci1_candidate_main_report_gate_open(void)", text)
|
||||
self.assertIn("static bool sci1_candidate_report_queue_nonempty(void)", text)
|
||||
self.assertIn("static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)", text)
|
||||
self.assertIn("static bool sci1_candidate_periodic_resend_gate_open(void)", text)
|
||||
self.assertIn("TX/autonomous report model candidate:", text)
|
||||
self.assertIn("loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate", text)
|
||||
@@ -421,6 +422,41 @@ class SerialPseudocodeTest(unittest.TestCase):
|
||||
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
|
||||
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
|
||||
|
||||
def test_timer_source_models_emit_separate_tick_isrs(self):
|
||||
analysis = {
|
||||
"protocol_semantics": [
|
||||
{
|
||||
"confidence": "medium",
|
||||
"confidence_score": 0.6,
|
||||
"timer_interrupt_model": {
|
||||
"sources": [
|
||||
{
|
||||
"source": "FRT2 OCIA",
|
||||
"handler_address_hex": "H'BF23",
|
||||
"summary": "Candidate periodic tick ISR for idle heartbeat/report counters.",
|
||||
"counters": [
|
||||
{
|
||||
"address": 0xF9C4,
|
||||
"address_hex": "H'F9C4",
|
||||
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
|
||||
"role": "candidate idle/default report enqueue countdown.",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch("h8536.serial_pseudocode.analyze_serial_semantics", return_value=analysis):
|
||||
text = generate_serial_pseudocode(candidate_payload())
|
||||
|
||||
self.assertIn("FRT2 OCIA H'BF23", text)
|
||||
self.assertIn("H'F9C4 idle_heartbeat_gate_countdown_candidate", text)
|
||||
self.assertIn("void frt2_ocia_candidate_tick_isr(void)", text)
|
||||
self.assertIn("MEM8[0xF9C4u] = (u8)(MEM8[0xF9C4u] - 1u);", text)
|
||||
|
||||
def test_tx_only_option_omits_rx_functions(self):
|
||||
text = generate_serial_pseudocode(
|
||||
candidate_payload(),
|
||||
|
||||
@@ -117,6 +117,16 @@ def planned_semantics_payload() -> dict:
|
||||
instruction(0x3FF9, "CLR.B", "@H'F9B0", [0xF9B0]),
|
||||
instruction(0x3FFD, "BCLR.B", "#7, @H'FAA5", [0xFAA5]),
|
||||
instruction(0x4007, "BSET.B", "#7, @H'FAA5", [0xFAA5]),
|
||||
instruction(0x4046, "TST.B", "@H'F9C4", [0xF9C4]),
|
||||
instruction(0x404C, "BTST.B", "#7, @H'FAA5", [0xFAA5]),
|
||||
instruction(0x4050, "BEQ", "loc_4059", targets=[0x4059]),
|
||||
instruction(0x4052, "TST.B", "@H'F9C3", [0xF9C3]),
|
||||
instruction(0x405F, "CMP:G.B", "@H'F9B5, R2", [0xF9B5]),
|
||||
instruction(0x4067, "MOV:G.W", "#H'00, @(-H'0790,R2)"),
|
||||
instruction(0x406C, "ADD:Q.B", "#1, @H'F9B0", [0xF9B0]),
|
||||
instruction(0x4070, "BCLR.B", "#7, @H'F9B0", [0xF9B0]),
|
||||
instruction(0x40E0, "MOV:G.B", "#H'14, @H'F9C4", [0xF9C4]),
|
||||
instruction(0xBA31, "MOV:G.B", "#H'07, @H'F9C4", [0xF9C4]),
|
||||
instruction(0xC000, "MOV:G.B", "@H'F861, R1", [0xF861]),
|
||||
instruction(0xC004, "MOV:G.B", "@H'F862, R2", [0xF862]),
|
||||
instruction(0xC008, "BSR", "loc_622B", targets=[0x622B]),
|
||||
@@ -319,6 +329,8 @@ class SerialSemanticsTest(unittest.TestCase):
|
||||
self.assertIn("faa2", state_text)
|
||||
self.assertIn("f9b5", state_text)
|
||||
self.assertIn("f9c0", state_text)
|
||||
self.assertIn("f9c4", state_text)
|
||||
self.assertIn("idle_heartbeat_gate_countdown", state_text)
|
||||
|
||||
def test_planned_retry_error_model_identifies_retransmit_and_checksum_error(self):
|
||||
semantics = only_semantics(self, planned_semantics_payload())
|
||||
@@ -368,6 +380,8 @@ class SerialSemanticsTest(unittest.TestCase):
|
||||
self.assertIn("baf2", gate_text)
|
||||
self.assertIn("faa2 == 0", gate_text)
|
||||
self.assertIn("f9c0 == 0", gate_text)
|
||||
self.assertIn("f9c4 == 0", gate_text)
|
||||
self.assertIn("h'00ff", gate_text)
|
||||
self.assertIn("f9b5 != f9b0", gate_text)
|
||||
self.assertIn("bb43", gate_text)
|
||||
self.assertIn("be9e", gate_text)
|
||||
@@ -377,6 +391,28 @@ class SerialSemanticsTest(unittest.TestCase):
|
||||
self.assertIn("0x06", gate_text)
|
||||
self.assertIn("not rom constants", gate_text)
|
||||
|
||||
def test_timer_interrupt_model_surfaces_frt2_idle_heartbeat_counter(self):
|
||||
semantics = only_semantics(
|
||||
self,
|
||||
base_payload(
|
||||
[
|
||||
instruction(0xBF23, "BCLR.B", "#5, @FRT2_TCSR"),
|
||||
instruction(0xBF27, "TST.B", "@H'F9C4", [0xF9C4]),
|
||||
instruction(0xBF2D, "ADD:Q.B", "#-1, @H'F9C4", [0xF9C4]),
|
||||
instruction(0xBF31, "TST.B", "@H'F9C5", [0xF9C5]),
|
||||
instruction(0xBF37, "ADD:Q.B", "#-1, @H'F9C5", [0xF9C5]),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
timer = semantics["timer_interrupt_model"]
|
||||
timer_text = semantic_text(timer)
|
||||
|
||||
self.assertIn("frt2 ocia", timer_text)
|
||||
self.assertIn("f9c4", timer_text)
|
||||
self.assertIn("idle_heartbeat_gate_countdown", timer_text)
|
||||
self.assertIn("phi/32", timer_text)
|
||||
|
||||
def test_missing_serial_reconstruction_candidates_emit_no_protocol_semantics(self):
|
||||
payload = {
|
||||
"serial_reconstruction": {"candidates": []},
|
||||
|
||||
Reference in New Issue
Block a user