1
0

Emulator learnings folded back into decompiler

This commit is contained in:
Aiden
2026-05-25 19:11:22 +10:00
parent 1fabf6587d
commit d2e7609bbf
16 changed files with 1468 additions and 87 deletions

View File

@@ -72,11 +72,43 @@ class SciProtocolTest(unittest.TestCase):
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2200),
"enable SCI1 TX interrupt (TIE)",
"enable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2204),
"disable SCI1 TX interrupt (TIE)",
"disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE",
)
def test_sci1_transmit_path_comments_tdr_tdre_and_tie_timing(self):
instructions = {
0xBA72: ins(0xBA72, "MOV:G.B", "R0, @SCI1_TDR", references=[0xFEDB]),
0xBA7B: ins(0xBA7B, "BCLR.B", "#7, @SCI1_SSR", references=[0xFEDC]),
0xBA7F: ins(0xBA7F, "BSET.B", "#7, @SCI1_SCR", references=[0xFEDA]),
0xBAB5: ins(0xBAB5, "MOV:G.B", "R1, @SCI1_TDR", references=[0xFEDB]),
0xBABB: ins(0xBABB, "BCLR.B", "#7, @SCI1_SSR", references=[0xFEDC]),
}
analysis = analyze_sci_protocol(instructions)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0xBA72),
"write RS232/SCI byte to SCI1 TDR for transmission",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0xBA7B),
"clear SCI1 TDRE after TDR write; TXI can fire again when hardware reasserts TDRE",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0xBA7F),
"enable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0xBAB5),
"write RS232/SCI byte to SCI1 TDR for transmission",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0xBABB),
"clear SCI1 TDRE after TDR write; TXI can fire again when hardware reasserts TDRE",
)
def test_receive_path_clears_rdrf_then_reads_received_byte(self):
@@ -89,7 +121,7 @@ class SciProtocolTest(unittest.TestCase):
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2300),
"clear SCI1 receive-data-full flag (RDRF)",
"clear SCI1 RDRF with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2304),
@@ -107,15 +139,15 @@ class SciProtocolTest(unittest.TestCase):
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2400),
"clear SCI1 overrun error flag (ORER)",
"clear SCI1 ORER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2404),
"clear SCI1 framing error flag (FER)",
"clear SCI1 FER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
)
self.assertEqual(
sci_protocol_comment_for_instruction(analysis, 0x2408),
"clear SCI1 parity error flag (PER)",
"clear SCI1 PER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
)
def test_immediate_scr_write_reports_protocol_control_bits(self):
@@ -126,7 +158,10 @@ class SciProtocolTest(unittest.TestCase):
analysis = analyze_sci_protocol(instructions)
comment = sci_protocol_comment_for_instruction(analysis, 0x2500)
self.assertIn("enable SCI2 TX interrupt (TIE)", comment)
self.assertIn(
"enable SCI2 TX interrupt (TIE); gates TXI when hardware sets TDRE",
comment,
)
self.assertIn("disable SCI2 receive and receive-error interrupts (RIE)", comment)
self.assertIn("enable SCI2 transmitter (TE)", comment)
self.assertIn("enable SCI2 receiver (RE)", comment)

View File

@@ -73,8 +73,19 @@ def fixture_payload() -> dict[str, object]:
ins(0xBECB, "BTST.B #7, @H'FAA3", references=[0xFAA3]),
ins(0xBED1, "CLR.B @H'F9C3", references=[0xF9C3]),
ins(0xBED5, "BSR loc_BA26", targets=[0xBA26]),
ins(0xBEEA, "BCLR.B #5, @FRT1_TCSR"),
ins(0xBEEE, "TST.B @H'F9C0", references=[0xF9C0]),
ins(0xBEF2, "BEQ loc_BEF8", targets=[0xBEF8]),
ins(0xBEF4, "ADD:Q.B #-1, @H'F9C0", references=[0xF9C0]),
ins(0xBEF8, "TST.B @H'F9C1", references=[0xF9C1]),
ins(0xBEFC, "BEQ loc_BF02", targets=[0xBF02]),
ins(0xBEFE, "ADD:Q.B #-1, @H'F9C1", references=[0xF9C1]),
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]),
]
return {
"vectors": [{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"}],
"call_graph": {"nodes": [{"start": 0x3FD3, "label": "loc_3FD3"}, {"start": 0xBAF2, "label": "loc_BAF2"}]},
"instructions": rows,
}
@@ -95,12 +106,26 @@ class SerialGateTest(unittest.TestCase):
self.assertTrue(analysis["evidence"]["rx_session_maintenance"]["present"])
self.assertTrue(any("0x0007" in caveat and "0x0015" in caveat for caveat in analysis["caveats"]))
def test_json_analysis_includes_frt1_tick_timer_roles(self):
analysis = analyze_serial_gate(fixture_payload())
tick = analysis["evidence"]["timer_tick_evidence"]
self.assertTrue(tick["present"])
self.assertEqual(tick["vector_address_hex"], "H'0062")
self.assertEqual(tick["handler_address_hex"], "H'BEEA")
self.assertIn("FRT1_TCSR.OCFA", tick["summary"])
roles = {role["address"]: role for role in tick["candidate_timer_roles"]}
self.assertIn("post-TX/report delay", roles[0xF9C0]["role"])
self.assertIn("secondary delay", roles[0xF9C1]["role"])
self.assertIn("periodic report/heartbeat", roles[0xF9C6]["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"]}
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[0xFAA3]["write_count"], 1)
self.assertIn("sample_accesses", accesses[0xFAA2])
@@ -111,6 +136,15 @@ class SerialGateTest(unittest.TestCase):
self.assertIn("capture overlays/runtime queue entries", text)
self.assertIn("H'F9B5", text)
def test_text_report_mentions_frt1_tick_timer_roles(self):
text = format_text_report(analyze_serial_gate(fixture_payload()))
self.assertIn("FRT1 OCIA periodic tick countdowns: present", text)
self.assertIn("H'BEEA: BCLR.B #5, @FRT1_TCSR", text)
self.assertIn("H'F9C0: candidate post-TX/report delay countdown", text)
self.assertIn("H'F9C1: candidate secondary delay countdown", text)
self.assertIn("H'F9C6: candidate periodic report/heartbeat countdown", text)
def test_cli_json_output_and_out_file(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"

View File

@@ -160,12 +160,23 @@ class SerialPseudocodeTest(unittest.TestCase):
self.assertIn("No SCI1 RXI/TXI DTC vector entries are present", text)
self.assertIn("MAX202 pin 11 traces to H8 pin 66", text)
self.assertIn("Manual/0900766b802125d0.md:15823 TDR transmit data register", text)
self.assertIn("#define SCI1_TX_FRAME_LENGTH 6u", text)
self.assertIn("#define SCI1_TX_FRAME_BASE 0xF858u", text)
self.assertIn("#define SCI1_TX_FRAME_BYTE(n) MEM8[(u16)(SCI1_TX_FRAME_BASE + (n))]", text)
self.assertIn("#define SCI1_TX_FRAME_CHECKSUM SCI1_TX_FRAME_BYTE(5u)", text)
self.assertIn("#define SCI1_TX_INDEX MEM8[0xF9C2u]", text)
self.assertIn("#define TX_FRAME(n) MEM8[(u16)(0xF858u + (n))]", text)
self.assertIn("#define RX_CAPTURE(n) MEM8[(u16)(0xF868u + (n))]", text)
self.assertIn("checksum ^= TX_FRAME(4);", text)
self.assertIn("TX_FRAME(5) = sci1_tx_candidate_checksum();", text)
self.assertIn("First byte is sent synchronously; TIE enables TXI for the remaining bytes.", text)
self.assertIn("SCI1_TDR = TX_FRAME(0);", text)
self.assertIn("TX_INDEX = 1u;", text)
self.assertIn("SCI1_SCR |= SCI_SCR_TIE;", text)
self.assertIn("void sci1_txi_candidate_isr(void)", text)
self.assertIn("TXI runs after hardware reasserts SSR.TDRE", text)
self.assertIn("if ((SCI1_SSR & SCI_SSR_TDRE) == 0u)", text)
self.assertIn("SCI1_TDR = TX_FRAME(TX_INDEX);", text)
self.assertIn("SCI1_SSR &= (u8)~SCI_SSR_RDRF;\n byte = SCI1_RDR;", text)
self.assertIn("RX_CAPTURE(RX_INDEX) = byte;", text)
self.assertIn("return sci1_process_rx_candidate_frame();", text)
@@ -344,6 +355,31 @@ class SerialPseudocodeTest(unittest.TestCase):
},
"evidence_addresses_hex": ["H'BE90", "H'BED5"],
},
"timer_interrupt_model": {
"source": "FRT1 OCIA",
"vector_address_hex": "H'BEEA",
"counters": [
{
"address": 0xF9C0,
"address_hex": "H'F9C0",
"name_candidate": "tx_report_gate_counter_candidate",
"role": "candidate report gate counter.",
},
{
"address": 0xF9C1,
"address_hex": "H'F9C1",
"name_candidate": "rx_interbyte_timeout_candidate",
"role": "candidate RX interbyte timeout counter.",
},
{
"address": 0xF9C6,
"address_hex": "H'F9C6",
"name_candidate": "periodic_resend_cadence_counter_candidate",
"role": "candidate periodic resend cadence counter.",
},
],
"evidence_addresses_hex": ["H'BEEA", "H'BEF4"],
},
}
]
}
@@ -374,6 +410,15 @@ class SerialPseudocodeTest(unittest.TestCase):
self.assertIn("heartbeat/periodic resend candidate:", text)
self.assertIn("F9C6 reload H'01F4", text)
self.assertIn("BED5 resend path", text)
self.assertIn("interrupt/timer architecture candidate:", text)
self.assertIn("FRT1 OCIA H'BEEA appears to be a periodic tick ISR", text)
self.assertIn("H'F9C0 tx_report_gate_counter_candidate: candidate report gate counter.", text)
self.assertIn("H'F9C1 rx_interbyte_timeout_candidate: candidate RX interbyte timeout counter.", text)
self.assertIn("H'F9C6 periodic_resend_cadence_counter_candidate: candidate periodic resend cadence counter.", text)
self.assertIn("void frt1_ocia_candidate_tick_isr(void)", text)
self.assertIn("MEM8[0xF9C0u] = (u8)(MEM8[0xF9C0u] - 1u);", text)
self.assertIn("MEM8[0xF9C1u] = (u8)(MEM8[0xF9C1u] - 1u);", text)
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
def test_tx_only_option_omits_rx_functions(self):

View File

@@ -61,6 +61,17 @@ class SerialReconstructionTest(unittest.TestCase):
self.assertEqual(candidate["checksum_address"], 0xF85D)
self.assertEqual(candidate["tx_index_address"], 0xF9C2)
self.assertEqual(candidate["checksum_seed"], 0x5A)
self.assertEqual(
[role["name"] for role in candidate["roles"]],
["tx_frame", "tx_checksum", "tx_index"],
)
self.assertEqual(candidate["roles"][0]["address"], 0xF858)
self.assertEqual(candidate["roles"][1]["address"], 0xF85D)
self.assertEqual(candidate["roles"][2]["address"], 0xF9C2)
self.assertEqual(candidate["tx_path"]["kind"], "interrupt_driven_txi")
self.assertEqual(candidate["tx_path"]["initial_tdr_write_address"], 0x3008)
self.assertEqual(candidate["tx_path"]["txi_indexed_tdr_write_address"], 0x3108)
self.assertIn("TDRE", candidate["tx_path"]["summary"])
self.assertEqual(candidate["confidence"], "high")
self.assertEqual(candidate["confidence_score"], 0.95)
self.assertEqual(candidate["missing_evidence"], [])
@@ -84,8 +95,43 @@ class SerialReconstructionTest(unittest.TestCase):
payload = serial_reconstruction_json_payload(analysis)
self.assertEqual(payload["candidates"][0]["frame_length"], 6)
self.assertEqual(payload["candidates"][0]["roles"][1]["name"], "tx_checksum")
json.dumps(payload)
def test_candidate_timer_ram_roles_from_frt1_ocia_tick(self):
instructions = {
0xBEEA: ins(0xBEEA, "BCLR.B", "#5, @FRT1_TCSR", [0xFE91]),
0xBEEE: ins(0xBEEE, "TST.B", "@H'F9C0", [0xF9C0]),
0xBEF4: ins(0xBEF4, "ADD:Q.B", "#-1, @H'F9C0", [0xF9C0]),
0xBEF8: ins(0xBEF8, "TST.B", "@H'F9C1", [0xF9C1]),
0xBEFE: ins(0xBEFE, "ADD:Q.B", "#-1, @H'F9C1", [0xF9C1]),
0xBF02: ins(0xBF02, "TST.W", "@H'F9C6", [0xF9C6]),
0xBF08: ins(0xBF08, "ADD:Q.W", "#-1, @H'F9C6", [0xF9C6]),
}
analysis = analyze_serial_reconstruction(instructions)
self.assertEqual(analysis["candidates"], [])
roles = {role["name"]: role for role in analysis["ram_roles"]}
self.assertEqual(set(roles), {"post_tx_report_delay", "secondary_tx_report_delay", "periodic_report_countdown"})
self.assertEqual(roles["post_tx_report_delay"]["address"], 0xF9C0)
self.assertEqual(roles["secondary_tx_report_delay"]["address"], 0xF9C1)
self.assertEqual(roles["periodic_report_countdown"]["address"], 0xF9C6)
self.assertEqual(roles["periodic_report_countdown"]["width_bits"], 16)
self.assertIn("candidate/evidence-supported", roles["post_tx_report_delay"]["confidence"])
self.assertIn("emulator-guided timer behavior", roles["post_tx_report_delay"]["caveat"])
payload = serial_reconstruction_json_payload(analysis)
self.assertEqual(payload["ram_roles"][0]["name"], "post_tx_report_delay")
json.dumps(payload)
decrement_comment = serial_reconstruction_comment_for_instruction(analysis, 0xBEF4)
self.assertIn("RAM role post_tx_report_delay", decrement_comment)
self.assertIn("FRT1 OCIA periodic tick ISR", decrement_comment)
metadata = serial_reconstruction_metadata_for_instruction(analysis, 0xBF08)
self.assertEqual(metadata[0]["action"], "serial_reconstruction_ram_role")
self.assertEqual(metadata[0]["role_name"], "periodic_report_countdown")
def test_candidate_sci1_rx_frame_length_and_checksum_validation_pattern(self):
instructions = {
0x4FF0: ins(0x4FF0, "BSET.B", "#7, @H'FAA4", [0xFAA4]),

View File

@@ -32,6 +32,13 @@ class SerialReconstructionIntegrationTest(unittest.TestCase):
0x3038: ins(0x3038, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]),
0x303C: ins(0x303C, "ADD:Q.B", "#1, @H'F9C2", [0xF9C2]),
0x3040: ins(0x3040, "CMP:G.B", "#6, @H'F9C2", [0xF9C2]),
0xBEEA: ins(0xBEEA, "BCLR.B", "#5, @FRT1_TCSR", [0xFE91]),
0xBEEE: ins(0xBEEE, "TST.B", "@H'F9C0", [0xF9C0]),
0xBEF4: ins(0xBEF4, "ADD:Q.B", "#-1, @H'F9C0", [0xF9C0]),
0xBEF8: ins(0xBEF8, "TST.B", "@H'F9C1", [0xF9C1]),
0xBEFE: ins(0xBEFE, "ADD:Q.B", "#-1, @H'F9C1", [0xF9C1]),
0xBF02: ins(0xBF02, "TST.W", "@H'F9C6", [0xF9C6]),
0xBF08: ins(0xBF08, "ADD:Q.W", "#-1, @H'F9C6", [0xF9C6]),
}
analysis = analyze_serial_reconstruction(instructions)
@@ -47,7 +54,11 @@ class SerialReconstructionIntegrationTest(unittest.TestCase):
)
self.assertIn("; Serial Protocol Reconstruction", listing)
self.assertIn("TX candidate: 6 bytes", listing)
self.assertIn("TX path: initial byte is written from the TX frame buffer", listing)
self.assertIn("; Serial RAM role candidates", listing)
self.assertIn("periodic_report_countdown", listing)
self.assertIn("candidate/evidence-supported SCI1 6-byte TX frame", listing)
self.assertIn("RAM role post_tx_report_delay", listing)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "out.json"
@@ -55,14 +66,19 @@ class SerialReconstructionIntegrationTest(unittest.TestCase):
payload = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(payload["serial_reconstruction"]["candidates"][0]["frame_length"], 6)
self.assertEqual(payload["serial_reconstruction"]["candidates"][0]["tx_path"]["kind"], "interrupt_driven_txi")
self.assertEqual(payload["serial_reconstruction"]["ram_roles"][2]["name"], "periodic_report_countdown")
tdr_instruction = next(item for item in payload["instructions"] if item["address"] == 0x3038)
self.assertIn("serial_reconstruction", tdr_instruction)
timer_instruction = next(item for item in payload["instructions"] if item["address"] == 0xBF08)
self.assertEqual(timer_instruction["serial_reconstruction"][0]["role_name"], "periodic_report_countdown")
pseudocode = generate_pseudocode(
payload,
options=PseudocodeOptions(include_addresses=False, include_asm=False, structured=False),
)
self.assertIn("candidate/evidence-supported SCI1 6-byte TX frame", pseudocode)
self.assertIn("candidate/evidence-supported RAM role periodic_report_countdown", pseudocode)
if __name__ == "__main__":