diff --git a/README.md b/README.md index 49b2967..2e5a41a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ To start the current emulator harness: - Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates. - Decodes observed serial byte captures into six-byte frames, validates checksums, labels capture-observed heartbeat/call/camera-power candidates, and summarizes heartbeat cadence. - Accepts both analyzer-style lines such as `RX 006 bytes ...` and the idle reference `frame 006 ...` format in `ROM/rcp-txd-idle-only.txt`. -- Reconstructs the autonomous serial gate/queue state-machine around `loc_3FD3`, `loc_BAF2`, `F9B0/F9B5`, `FAA2/FAA3/FAA5`, and the resend path through `BE9E/BED5`. +- Reconstructs the autonomous serial gate/queue state-machine around `loc_3FD3`, `loc_BAF2`, `F9B0/F9B5`, `FAA2/FAA3/FAA5`, the `F9C4`/FRT2 idle heartbeat gate at `loc_4046`, and the resend path through `BE9E/BED5`. - Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations. - Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations. - Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver. @@ -79,7 +79,7 @@ To start the current emulator harness: - Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes. - Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns. - Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, scaffolded SCI1 TXI/interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path. -- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM. +- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM. Current serial observations: @@ -87,6 +87,7 @@ Current serial observations: - Idle frame: `00 00 00 00 80 DA`. - Capture-side label: `heartbeat_alive_candidate`. - Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms. +- Static/runtime finding: `F9C4` is a candidate idle heartbeat/report countdown. Init loads `H'14`, `loc_BA26` reloads `H'07` after a send, FRT2 OCIA decrements it, and `loc_4046` can enqueue report `H'00FF` when it reaches zero and the queue is empty. - Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM. The generated listing is written to: @@ -191,6 +192,8 @@ python h8536_emulator_probe.py --help - `--interval-steps N`: tune the scaffolded interval timer cadence. - `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: tune rough FRT compare-interrupt cadence. - `--p9-fast-path`: shortcut known P9 transfer routines for exploration. +- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history. +- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame. - Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT1/FRT2 OCIA. The probe now reaches the SCI1 transmit path, but still needs more timing/peripheral fidelity before it emits the full observed heartbeat frame. ## Code Layout diff --git a/build/rom_decompiled.json b/build/rom_decompiled.json index cb855cd..df5cbfa 100644 --- a/build/rom_decompiled.json +++ b/build/rom_decompiled.json @@ -189311,7 +189311,7 @@ "H'BEAF" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -189434,7 +189434,7 @@ "H'BECB" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -189505,15 +189505,15 @@ "H'BE29" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", "name_candidate": "serial_retry_enable_or_mode_flags_candidate", "address": 64165, "address_hex": "H'FAA5", - "access_count": 3, - "read_count": 3, + "access_count": 4, + "read_count": 4, "write_count": 0, "bit_candidates": [ 7 @@ -189525,6 +189525,16 @@ "H'0007" ], "accesses": [ + { + "instruction_address": 16460, + "instruction_address_hex": "H'404C", + "access": "read", + "mnemonic": "BTST.B", + "instruction": "BTST.B #7, @H'FAA5", + "bit": 7, + "immediate": 7, + "immediate_hex": "H'07" + }, { "instruction_address": 47754, "instruction_address_hex": "H'BA8A", @@ -189554,17 +189564,19 @@ } ], "evidence_addresses": [ + 16460, 47754, 48685, 48798 ], "evidence_addresses_hex": [ + "H'404C", "H'BA8A", "H'BE2D", "H'BE9E" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -189621,7 +189633,7 @@ "H'BE37" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -189681,15 +189693,15 @@ "H'BE99" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", "name_candidate": "event_queue_write_or_pending_cursor_candidate", "address": 63925, "address_hex": "H'F9B5", - "access_count": 7, - "read_count": 1, + "access_count": 8, + "read_count": 2, "write_count": 6, "bit_candidates": [ 7 @@ -189703,6 +189715,13 @@ "H'0007" ], "accesses": [ + { + "instruction_address": 16479, + "instruction_address_hex": "H'405F", + "access": "read", + "mnemonic": "CMP:G.B", + "instruction": "CMP:G.B @H'F9B5, R2" + }, { "instruction_address": 47858, "instruction_address_hex": "H'BAF2", @@ -189769,6 +189788,7 @@ } ], "evidence_addresses": [ + 16479, 47858, 48493, 48497, @@ -189778,6 +189798,7 @@ 48631 ], "evidence_addresses_hex": [ + "H'405F", "H'BAF2", "H'BD6D", "H'BD71", @@ -189787,7 +189808,7 @@ "H'BDF7" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -189816,7 +189837,7 @@ "H'BE70" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -189954,22 +189975,169 @@ "H'BEF4" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." + }, + { + "kind": "serial_state_variable_candidate", + "name_candidate": "idle_heartbeat_gate_countdown_candidate", + "address": 63940, + "address_hex": "H'F9C4", + "access_count": 5, + "read_count": 2, + "write_count": 3, + "bit_candidates": [], + "immediate_values": [ + 20, + 7, + 65535 + ], + "immediate_values_hex": [ + "H'0014", + "H'0007", + "H'FFFF" + ], + "accesses": [ + { + "instruction_address": 16454, + "instruction_address_hex": "H'4046", + "access": "read", + "mnemonic": "TST.B", + "instruction": "TST.B @H'F9C4" + }, + { + "instruction_address": 16608, + "instruction_address_hex": "H'40E0", + "access": "write", + "mnemonic": "MOV:G.B", + "instruction": "MOV:G.B #H'14, @H'F9C4", + "immediate": 20, + "immediate_hex": "H'14" + }, + { + "instruction_address": 47665, + "instruction_address_hex": "H'BA31", + "access": "write", + "mnemonic": "MOV:G.B", + "instruction": "MOV:G.B #H'07, @H'F9C4", + "immediate": 7, + "immediate_hex": "H'07" + }, + { + "instruction_address": 48935, + "instruction_address_hex": "H'BF27", + "access": "read", + "mnemonic": "TST.B", + "instruction": "TST.B @H'F9C4" + }, + { + "instruction_address": 48941, + "instruction_address_hex": "H'BF2D", + "access": "write", + "mnemonic": "ADD:Q.B", + "instruction": "ADD:Q.B #-1, @H'F9C4", + "immediate": 65535, + "immediate_hex": "H'FFFF" + } + ], + "evidence_addresses": [ + 16454, + 16608, + 47665, + 48935, + 48941 + ], + "evidence_addresses_hex": [ + "H'4046", + "H'40E0", + "H'BA31", + "H'BF27", + "H'BF2D" + ], + "confidence": "candidate-medium", + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." + }, + { + "kind": "serial_state_variable_candidate", + "name_candidate": "rx_session_timeout_candidate", + "address": 63941, + "address_hex": "H'F9C5", + "access_count": 4, + "read_count": 1, + "write_count": 3, + "bit_candidates": [], + "immediate_values": [ + 20, + 65535 + ], + "immediate_values_hex": [ + "H'0014", + "H'FFFF" + ], + "accesses": [ + { + "instruction_address": 48030, + "instruction_address_hex": "H'BB9E", + "access": "write", + "mnemonic": "MOV:G.B", + "instruction": "MOV:G.B #H'14, @H'F9C5", + "immediate": 20, + "immediate_hex": "H'14" + }, + { + "instruction_address": 48868, + "instruction_address_hex": "H'BEE4", + "access": "write", + "mnemonic": "CLR.B", + "instruction": "CLR.B @H'F9C5" + }, + { + "instruction_address": 48945, + "instruction_address_hex": "H'BF31", + "access": "read", + "mnemonic": "TST.B", + "instruction": "TST.B @H'F9C5" + }, + { + "instruction_address": 48951, + "instruction_address_hex": "H'BF37", + "access": "write", + "mnemonic": "ADD:Q.B", + "instruction": "ADD:Q.B #-1, @H'F9C5", + "immediate": 65535, + "immediate_hex": "H'FFFF" + } + ], + "evidence_addresses": [ + 48030, + 48868, + 48945, + 48951 + ], + "evidence_addresses_hex": [ + "H'BB9E", + "H'BEE4", + "H'BF31", + "H'BF37" + ], + "confidence": "candidate-medium", + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", "name_candidate": "autonomous_report_period_timer_candidate", "address": 63942, "address_hex": "H'F9C6", - "access_count": 3, - "read_count": 1, - "write_count": 2, + "access_count": 5, + "read_count": 2, + "write_count": 3, "bit_candidates": [], "immediate_values": [ - 500 + 500, + 65535 ], "immediate_values_hex": [ - "H'01F4" + "H'01F4", + "H'FFFF" ], "accesses": [ { @@ -189996,20 +190164,40 @@ "instruction": "MOV:G.W #H'01F4, @H'F9C6", "immediate": 500, "immediate_hex": "H'01F4" + }, + { + "instruction_address": 48898, + "instruction_address_hex": "H'BF02", + "access": "read", + "mnemonic": "TST.W", + "instruction": "TST.W @H'F9C6" + }, + { + "instruction_address": 48904, + "instruction_address_hex": "H'BF08", + "access": "write", + "mnemonic": "ADD:Q.W", + "instruction": "ADD:Q.W #-1, @H'F9C6", + "immediate": 65535, + "immediate_hex": "H'FFFF" } ], "evidence_addresses": [ 47942, 48821, - 48837 + 48837, + 48898, + 48904 ], "evidence_addresses_hex": [ "H'BB46", "H'BEB5", - "H'BEC5" + "H'BEC5", + "H'BF02", + "H'BF08" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -190066,7 +190254,7 @@ "H'BEC1" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." } ], "send_builder": { @@ -196234,6 +196422,38 @@ 16363 ] }, + { + "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": [ + "H'F9C4", + "H'FAA5", + "H'F9C3", + "H'F9B0", + "H'F9B5" + ], + "enqueued_report_candidate_hex": "H'00FF", + "evidence_addresses": [ + 16454, + 16458, + 16460, + 16464, + 16466, + 16470, + 16472, + 16473, + 16477, + 16479, + 16483, + 16485, + 16487, + 16492, + 16496 + ] + }, { "name": "queue_has_pending_report", "entry_label": "loc_BAF2", @@ -196346,6 +196566,28 @@ 16391 ] }, + { + "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": [ + "H'F9C4" + ], + "reload_value_hex": "H'14", + "evidence_addresses": [ + 16608 + ] + }, + { + "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": [ + "H'F9C4" + ], + "reload_value_hex": "H'07", + "evidence_addresses": [ + 47665 + ] + }, { "name": "host_ack_can_advance_queue", "summary": "Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.", @@ -196387,6 +196629,21 @@ 16387, 16389, 16391, + 16454, + 16458, + 16460, + 16464, + 16466, + 16470, + 16472, + 16473, + 16477, + 16479, + 16483, + 16485, + 16487, + 16492, + 16496, 47858, 47862, 47864, @@ -196453,6 +196710,21 @@ "H'4003", "H'4005", "H'4007", + "H'4046", + "H'404A", + "H'404C", + "H'4050", + "H'4052", + "H'4056", + "H'4058", + "H'4059", + "H'405D", + "H'405F", + "H'4063", + "H'4065", + "H'4067", + "H'406C", + "H'4070", "H'BAF2", "H'BAF6", "H'BAF8", @@ -196671,6 +196943,197 @@ "confidence": "candidate-medium", "caveat": "Timer and resend roles are inferred from constants/state references around F9C6, F9C8, FAA3, and loc_BED5; exact scheduling units remain candidate phrasing." }, + "timer_interrupt_model": { + "kind": "timer_interrupt_model_candidate", + "source": "FRT1 OCIA / FRT2 OCIA", + "summary": "FRT compare-match handlers decrement serial gate, timeout, and cadence counters.", + "sources": [ + { + "source": "FRT1 OCIA", + "vector_address_hex": "H'0062", + "handler_address": 48874, + "handler_address_hex": "H'BEEA", + "summary": "Candidate periodic tick ISR for serial busy, interbyte, and resend counters.", + "counters": [ + { + "address": 63936, + "address_hex": "H'F9C0", + "name_candidate": "tx_report_gate_counter_candidate", + "role": "candidate gate counter used before entering the report builder.", + "evidence_address": 48884, + "evidence_address_hex": "H'BEF4" + }, + { + "address": 63937, + "address_hex": "H'F9C1", + "name_candidate": "rx_interbyte_timeout_candidate", + "role": "candidate RX interbyte timeout counter.", + "evidence_address": 48894, + "evidence_address_hex": "H'BEFE" + }, + { + "address": 63942, + "address_hex": "H'F9C6", + "name_candidate": "periodic_resend_cadence_counter_candidate", + "role": "candidate periodic resend/heartbeat cadence counter.", + "evidence_address": 48904, + "evidence_address_hex": "H'BF08" + } + ], + "evidence_addresses": [ + 48874, + 48878, + 48882, + 48884, + 48888, + 48892, + 48894, + 48898, + 48902, + 48904 + ], + "evidence_addresses_hex": [ + "H'BEEA", + "H'BEEE", + "H'BEF2", + "H'BEF4", + "H'BEF8", + "H'BEFC", + "H'BEFE", + "H'BF02", + "H'BF06", + "H'BF08" + ] + }, + { + "source": "FRT2 OCIA", + "vector_address_hex": "H'006A", + "handler_address": 48931, + "handler_address_hex": "H'BF23", + "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": [ + { + "address": 63940, + "address_hex": "H'F9C4", + "name_candidate": "idle_heartbeat_gate_countdown_candidate", + "role": "candidate idle/default report enqueue countdown.", + "evidence_address": 48941, + "evidence_address_hex": "H'BF2D" + }, + { + "address": 63941, + "address_hex": "H'F9C5", + "name_candidate": "rx_session_timeout_candidate", + "role": "candidate RX/session maintenance timeout counter.", + "evidence_address": 48951, + "evidence_address_hex": "H'BF37" + } + ], + "evidence_addresses": [ + 48931, + 48935, + 48939, + 48941, + 48945, + 48949, + 48951 + ], + "evidence_addresses_hex": [ + "H'BF23", + "H'BF27", + "H'BF2B", + "H'BF2D", + "H'BF31", + "H'BF35", + "H'BF37" + ] + } + ], + "counters": [ + { + "address": 63936, + "address_hex": "H'F9C0", + "name_candidate": "tx_report_gate_counter_candidate", + "role": "candidate gate counter used before entering the report builder.", + "evidence_address": 48884, + "evidence_address_hex": "H'BEF4" + }, + { + "address": 63937, + "address_hex": "H'F9C1", + "name_candidate": "rx_interbyte_timeout_candidate", + "role": "candidate RX interbyte timeout counter.", + "evidence_address": 48894, + "evidence_address_hex": "H'BEFE" + }, + { + "address": 63942, + "address_hex": "H'F9C6", + "name_candidate": "periodic_resend_cadence_counter_candidate", + "role": "candidate periodic resend/heartbeat cadence counter.", + "evidence_address": 48904, + "evidence_address_hex": "H'BF08" + }, + { + "address": 63940, + "address_hex": "H'F9C4", + "name_candidate": "idle_heartbeat_gate_countdown_candidate", + "role": "candidate idle/default report enqueue countdown.", + "evidence_address": 48941, + "evidence_address_hex": "H'BF2D" + }, + { + "address": 63941, + "address_hex": "H'F9C5", + "name_candidate": "rx_session_timeout_candidate", + "role": "candidate RX/session maintenance timeout counter.", + "evidence_address": 48951, + "evidence_address_hex": "H'BF37" + } + ], + "evidence_addresses": [ + 48874, + 48878, + 48882, + 48884, + 48888, + 48892, + 48894, + 48898, + 48902, + 48904, + 48931, + 48935, + 48939, + 48941, + 48945, + 48949, + 48951 + ], + "evidence_addresses_hex": [ + "H'BEEA", + "H'BEEE", + "H'BEF2", + "H'BEF4", + "H'BEF8", + "H'BEFC", + "H'BEFE", + "H'BF02", + "H'BF06", + "H'BF08", + "H'BF23", + "H'BF27", + "H'BF2B", + "H'BF2D", + "H'BF31", + "H'BF35", + "H'BF37" + ], + "confidence": "candidate-medium" + }, "evidence": [ { "kind": "rx_frame_reconstruction_present", @@ -205047,7 +205510,7 @@ "H'BEAF" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205170,7 +205633,7 @@ "H'BECB" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205241,15 +205704,15 @@ "H'BE29" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", "name_candidate": "serial_retry_enable_or_mode_flags_candidate", "address": 64165, "address_hex": "H'FAA5", - "access_count": 3, - "read_count": 3, + "access_count": 4, + "read_count": 4, "write_count": 0, "bit_candidates": [ 7 @@ -205261,6 +205724,16 @@ "H'0007" ], "accesses": [ + { + "instruction_address": 16460, + "instruction_address_hex": "H'404C", + "access": "read", + "mnemonic": "BTST.B", + "instruction": "BTST.B #7, @H'FAA5", + "bit": 7, + "immediate": 7, + "immediate_hex": "H'07" + }, { "instruction_address": 47754, "instruction_address_hex": "H'BA8A", @@ -205290,17 +205763,19 @@ } ], "evidence_addresses": [ + 16460, 47754, 48685, 48798 ], "evidence_addresses_hex": [ + "H'404C", "H'BA8A", "H'BE2D", "H'BE9E" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205357,7 +205832,7 @@ "H'BE37" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205417,15 +205892,15 @@ "H'BE99" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", "name_candidate": "event_queue_write_or_pending_cursor_candidate", "address": 63925, "address_hex": "H'F9B5", - "access_count": 7, - "read_count": 1, + "access_count": 8, + "read_count": 2, "write_count": 6, "bit_candidates": [ 7 @@ -205439,6 +205914,13 @@ "H'0007" ], "accesses": [ + { + "instruction_address": 16479, + "instruction_address_hex": "H'405F", + "access": "read", + "mnemonic": "CMP:G.B", + "instruction": "CMP:G.B @H'F9B5, R2" + }, { "instruction_address": 47858, "instruction_address_hex": "H'BAF2", @@ -205505,6 +205987,7 @@ } ], "evidence_addresses": [ + 16479, 47858, 48493, 48497, @@ -205514,6 +205997,7 @@ 48631 ], "evidence_addresses_hex": [ + "H'405F", "H'BAF2", "H'BD6D", "H'BD71", @@ -205523,7 +206007,7 @@ "H'BDF7" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205552,7 +206036,7 @@ "H'BE70" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205690,22 +206174,169 @@ "H'BEF4" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." + }, + { + "kind": "serial_state_variable_candidate", + "name_candidate": "idle_heartbeat_gate_countdown_candidate", + "address": 63940, + "address_hex": "H'F9C4", + "access_count": 5, + "read_count": 2, + "write_count": 3, + "bit_candidates": [], + "immediate_values": [ + 20, + 7, + 65535 + ], + "immediate_values_hex": [ + "H'0014", + "H'0007", + "H'FFFF" + ], + "accesses": [ + { + "instruction_address": 16454, + "instruction_address_hex": "H'4046", + "access": "read", + "mnemonic": "TST.B", + "instruction": "TST.B @H'F9C4" + }, + { + "instruction_address": 16608, + "instruction_address_hex": "H'40E0", + "access": "write", + "mnemonic": "MOV:G.B", + "instruction": "MOV:G.B #H'14, @H'F9C4", + "immediate": 20, + "immediate_hex": "H'14" + }, + { + "instruction_address": 47665, + "instruction_address_hex": "H'BA31", + "access": "write", + "mnemonic": "MOV:G.B", + "instruction": "MOV:G.B #H'07, @H'F9C4", + "immediate": 7, + "immediate_hex": "H'07" + }, + { + "instruction_address": 48935, + "instruction_address_hex": "H'BF27", + "access": "read", + "mnemonic": "TST.B", + "instruction": "TST.B @H'F9C4" + }, + { + "instruction_address": 48941, + "instruction_address_hex": "H'BF2D", + "access": "write", + "mnemonic": "ADD:Q.B", + "instruction": "ADD:Q.B #-1, @H'F9C4", + "immediate": 65535, + "immediate_hex": "H'FFFF" + } + ], + "evidence_addresses": [ + 16454, + 16608, + 47665, + 48935, + 48941 + ], + "evidence_addresses_hex": [ + "H'4046", + "H'40E0", + "H'BA31", + "H'BF27", + "H'BF2D" + ], + "confidence": "candidate-medium", + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." + }, + { + "kind": "serial_state_variable_candidate", + "name_candidate": "rx_session_timeout_candidate", + "address": 63941, + "address_hex": "H'F9C5", + "access_count": 4, + "read_count": 1, + "write_count": 3, + "bit_candidates": [], + "immediate_values": [ + 20, + 65535 + ], + "immediate_values_hex": [ + "H'0014", + "H'FFFF" + ], + "accesses": [ + { + "instruction_address": 48030, + "instruction_address_hex": "H'BB9E", + "access": "write", + "mnemonic": "MOV:G.B", + "instruction": "MOV:G.B #H'14, @H'F9C5", + "immediate": 20, + "immediate_hex": "H'14" + }, + { + "instruction_address": 48868, + "instruction_address_hex": "H'BEE4", + "access": "write", + "mnemonic": "CLR.B", + "instruction": "CLR.B @H'F9C5" + }, + { + "instruction_address": 48945, + "instruction_address_hex": "H'BF31", + "access": "read", + "mnemonic": "TST.B", + "instruction": "TST.B @H'F9C5" + }, + { + "instruction_address": 48951, + "instruction_address_hex": "H'BF37", + "access": "write", + "mnemonic": "ADD:Q.B", + "instruction": "ADD:Q.B #-1, @H'F9C5", + "immediate": 65535, + "immediate_hex": "H'FFFF" + } + ], + "evidence_addresses": [ + 48030, + 48868, + 48945, + 48951 + ], + "evidence_addresses_hex": [ + "H'BB9E", + "H'BEE4", + "H'BF31", + "H'BF37" + ], + "confidence": "candidate-medium", + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", "name_candidate": "autonomous_report_period_timer_candidate", "address": 63942, "address_hex": "H'F9C6", - "access_count": 3, - "read_count": 1, - "write_count": 2, + "access_count": 5, + "read_count": 2, + "write_count": 3, "bit_candidates": [], "immediate_values": [ - 500 + 500, + 65535 ], "immediate_values_hex": [ - "H'01F4" + "H'01F4", + "H'FFFF" ], "accesses": [ { @@ -205732,20 +206363,40 @@ "instruction": "MOV:G.W #H'01F4, @H'F9C6", "immediate": 500, "immediate_hex": "H'01F4" + }, + { + "instruction_address": 48898, + "instruction_address_hex": "H'BF02", + "access": "read", + "mnemonic": "TST.W", + "instruction": "TST.W @H'F9C6" + }, + { + "instruction_address": 48904, + "instruction_address_hex": "H'BF08", + "access": "write", + "mnemonic": "ADD:Q.W", + "instruction": "ADD:Q.W #-1, @H'F9C6", + "immediate": 65535, + "immediate_hex": "H'FFFF" } ], "evidence_addresses": [ 47942, 48821, - 48837 + 48837, + 48898, + 48904 ], "evidence_addresses_hex": [ "H'BB46", "H'BEB5", - "H'BEC5" + "H'BEC5", + "H'BF02", + "H'BF08" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." }, { "kind": "serial_state_variable_candidate", @@ -205802,7 +206453,7 @@ "H'BEC1" ], "confidence": "candidate-medium", - "caveat": "Role is inferred from references in the serial handler region and remains a state-variable candidate." + "caveat": "Role is inferred from references in serial handler, gate, and timer regions and remains a state-variable candidate." } ], "retry_error_model": { @@ -206021,6 +206672,38 @@ 16363 ] }, + { + "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": [ + "H'F9C4", + "H'FAA5", + "H'F9C3", + "H'F9B0", + "H'F9B5" + ], + "enqueued_report_candidate_hex": "H'00FF", + "evidence_addresses": [ + 16454, + 16458, + 16460, + 16464, + 16466, + 16470, + 16472, + 16473, + 16477, + 16479, + 16483, + 16485, + 16487, + 16492, + 16496 + ] + }, { "name": "queue_has_pending_report", "entry_label": "loc_BAF2", @@ -206133,6 +206816,28 @@ 16391 ] }, + { + "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": [ + "H'F9C4" + ], + "reload_value_hex": "H'14", + "evidence_addresses": [ + 16608 + ] + }, + { + "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": [ + "H'F9C4" + ], + "reload_value_hex": "H'07", + "evidence_addresses": [ + 47665 + ] + }, { "name": "host_ack_can_advance_queue", "summary": "Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.", @@ -206174,6 +206879,21 @@ 16387, 16389, 16391, + 16454, + 16458, + 16460, + 16464, + 16466, + 16470, + 16472, + 16473, + 16477, + 16479, + 16483, + 16485, + 16487, + 16492, + 16496, 47858, 47862, 47864, @@ -206240,6 +206960,21 @@ "H'4003", "H'4005", "H'4007", + "H'4046", + "H'404A", + "H'404C", + "H'4050", + "H'4052", + "H'4056", + "H'4058", + "H'4059", + "H'405D", + "H'405F", + "H'4063", + "H'4065", + "H'4067", + "H'406C", + "H'4070", "H'BAF2", "H'BAF6", "H'BAF8", @@ -206458,6 +207193,197 @@ "confidence": "candidate-medium", "caveat": "Timer and resend roles are inferred from constants/state references around F9C6, F9C8, FAA3, and loc_BED5; exact scheduling units remain candidate phrasing." }, + "timer_interrupt_model": { + "kind": "timer_interrupt_model_candidate", + "source": "FRT1 OCIA / FRT2 OCIA", + "summary": "FRT compare-match handlers decrement serial gate, timeout, and cadence counters.", + "sources": [ + { + "source": "FRT1 OCIA", + "vector_address_hex": "H'0062", + "handler_address": 48874, + "handler_address_hex": "H'BEEA", + "summary": "Candidate periodic tick ISR for serial busy, interbyte, and resend counters.", + "counters": [ + { + "address": 63936, + "address_hex": "H'F9C0", + "name_candidate": "tx_report_gate_counter_candidate", + "role": "candidate gate counter used before entering the report builder.", + "evidence_address": 48884, + "evidence_address_hex": "H'BEF4" + }, + { + "address": 63937, + "address_hex": "H'F9C1", + "name_candidate": "rx_interbyte_timeout_candidate", + "role": "candidate RX interbyte timeout counter.", + "evidence_address": 48894, + "evidence_address_hex": "H'BEFE" + }, + { + "address": 63942, + "address_hex": "H'F9C6", + "name_candidate": "periodic_resend_cadence_counter_candidate", + "role": "candidate periodic resend/heartbeat cadence counter.", + "evidence_address": 48904, + "evidence_address_hex": "H'BF08" + } + ], + "evidence_addresses": [ + 48874, + 48878, + 48882, + 48884, + 48888, + 48892, + 48894, + 48898, + 48902, + 48904 + ], + "evidence_addresses_hex": [ + "H'BEEA", + "H'BEEE", + "H'BEF2", + "H'BEF4", + "H'BEF8", + "H'BEFC", + "H'BEFE", + "H'BF02", + "H'BF06", + "H'BF08" + ] + }, + { + "source": "FRT2 OCIA", + "vector_address_hex": "H'006A", + "handler_address": 48931, + "handler_address_hex": "H'BF23", + "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": [ + { + "address": 63940, + "address_hex": "H'F9C4", + "name_candidate": "idle_heartbeat_gate_countdown_candidate", + "role": "candidate idle/default report enqueue countdown.", + "evidence_address": 48941, + "evidence_address_hex": "H'BF2D" + }, + { + "address": 63941, + "address_hex": "H'F9C5", + "name_candidate": "rx_session_timeout_candidate", + "role": "candidate RX/session maintenance timeout counter.", + "evidence_address": 48951, + "evidence_address_hex": "H'BF37" + } + ], + "evidence_addresses": [ + 48931, + 48935, + 48939, + 48941, + 48945, + 48949, + 48951 + ], + "evidence_addresses_hex": [ + "H'BF23", + "H'BF27", + "H'BF2B", + "H'BF2D", + "H'BF31", + "H'BF35", + "H'BF37" + ] + } + ], + "counters": [ + { + "address": 63936, + "address_hex": "H'F9C0", + "name_candidate": "tx_report_gate_counter_candidate", + "role": "candidate gate counter used before entering the report builder.", + "evidence_address": 48884, + "evidence_address_hex": "H'BEF4" + }, + { + "address": 63937, + "address_hex": "H'F9C1", + "name_candidate": "rx_interbyte_timeout_candidate", + "role": "candidate RX interbyte timeout counter.", + "evidence_address": 48894, + "evidence_address_hex": "H'BEFE" + }, + { + "address": 63942, + "address_hex": "H'F9C6", + "name_candidate": "periodic_resend_cadence_counter_candidate", + "role": "candidate periodic resend/heartbeat cadence counter.", + "evidence_address": 48904, + "evidence_address_hex": "H'BF08" + }, + { + "address": 63940, + "address_hex": "H'F9C4", + "name_candidate": "idle_heartbeat_gate_countdown_candidate", + "role": "candidate idle/default report enqueue countdown.", + "evidence_address": 48941, + "evidence_address_hex": "H'BF2D" + }, + { + "address": 63941, + "address_hex": "H'F9C5", + "name_candidate": "rx_session_timeout_candidate", + "role": "candidate RX/session maintenance timeout counter.", + "evidence_address": 48951, + "evidence_address_hex": "H'BF37" + } + ], + "evidence_addresses": [ + 48874, + 48878, + 48882, + 48884, + 48888, + 48892, + 48894, + 48898, + 48902, + 48904, + 48931, + 48935, + 48939, + 48941, + 48945, + 48949, + 48951 + ], + "evidence_addresses_hex": [ + "H'BEEA", + "H'BEEE", + "H'BEF2", + "H'BEF4", + "H'BEF8", + "H'BEFC", + "H'BEFE", + "H'BF02", + "H'BF06", + "H'BF08", + "H'BF23", + "H'BF27", + "H'BF2B", + "H'BF2D", + "H'BF31", + "H'BF35", + "H'BF37" + ], + "confidence": "candidate-medium" + }, "confidence": "medium-high", "confidence_score": 0.9, "caveat": "Semantic names are candidates only. The analyzer reports byte roles, command values, dispatch targets, and response staging patterns observed in code; it does not prove source-level intent or protocol documentation." diff --git a/build/rom_serial_gate.json b/build/rom_serial_gate.json index ae219a5..4567fda 100644 --- a/build/rom_serial_gate.json +++ b/build/rom_serial_gate.json @@ -5,6 +5,185 @@ "Branch predicates are summarized from local instruction order and targets; this is not an emulator trace." ], "evidence": { + "idle_heartbeat_gate_loc_4046": { + "candidate_timer_roles": [ + { + "address": 63940, + "address_hex": "H'F9C4", + "evidence_address_hex": "H'BF2D", + "role": "candidate idle heartbeat/report gate countdown" + } + ], + "decrement_address_hex": "H'BF2D", + "gate_address_hex": "H'4046", + "initial_reload_address_hex": "H'40E0", + "initial_reload_value_hex": "H'14", + "items": [ + { + "address": 16454, + "address_hex": "H'4046", + "present": true, + "targets_hex": [], + "text": "TST.B @H'F9C4" + }, + { + "address": 16458, + "address_hex": "H'404A", + "present": true, + "targets_hex": [ + "H'4058" + ], + "text": "BNE loc_4058" + }, + { + "address": 16460, + "address_hex": "H'404C", + "present": true, + "targets_hex": [], + "text": "BTST.B #7, @H'FAA5" + }, + { + "address": 16464, + "address_hex": "H'4050", + "present": true, + "targets_hex": [ + "H'4059" + ], + "text": "BEQ loc_4059" + }, + { + "address": 16466, + "address_hex": "H'4052", + "present": true, + "targets_hex": [], + "text": "TST.B @H'F9C3" + }, + { + "address": 16470, + "address_hex": "H'4056", + "present": true, + "targets_hex": [ + "H'4059" + ], + "text": "BEQ loc_4059" + }, + { + "address": 16472, + "address_hex": "H'4058", + "present": true, + "targets_hex": [], + "text": "RTS" + }, + { + "address": 16473, + "address_hex": "H'4059", + "present": true, + "targets_hex": [], + "text": "MOV:G.B @H'F9B0, R2" + }, + { + "address": 16479, + "address_hex": "H'405F", + "present": true, + "targets_hex": [], + "text": "CMP:G.B @H'F9B5, R2" + }, + { + "address": 16483, + "address_hex": "H'4063", + "present": true, + "targets_hex": [ + "H'4074" + ], + "text": "BNE loc_4074" + }, + { + "address": 16487, + "address_hex": "H'4067", + "present": true, + "targets_hex": [], + "text": "MOV:G.W #H'00, @(-H'0790,R2)" + }, + { + "address": 16492, + "address_hex": "H'406C", + "present": true, + "targets_hex": [], + "text": "ADD:Q.B #1, @H'F9B0" + }, + { + "address": 16496, + "address_hex": "H'4070", + "present": true, + "targets_hex": [], + "text": "BCLR.B #7, @H'F9B0" + }, + { + "address": 16608, + "address_hex": "H'40E0", + "present": true, + "targets_hex": [], + "text": "MOV:G.B #H'14, @H'F9C4" + }, + { + "address": 47665, + "address_hex": "H'BA31", + "present": true, + "targets_hex": [], + "text": "MOV:G.B #H'07, @H'F9C4" + }, + { + "address": 48931, + "address_hex": "H'BF23", + "present": true, + "targets_hex": [], + "text": "BCLR.B #5, @FRT2_TCSR" + }, + { + "address": 48935, + "address_hex": "H'BF27", + "present": true, + "targets_hex": [], + "text": "TST.B @H'F9C4" + }, + { + "address": 48941, + "address_hex": "H'BF2D", + "present": true, + "targets_hex": [], + "text": "ADD:Q.B #-1, @H'F9C4" + } + ], + "post_tx_reload_address_hex": "H'BA31", + "post_tx_reload_value_hex": "H'07", + "present": true, + "queue_write_address_hex": "H'4067", + "required_addresses_hex": [ + "H'4046", + "H'4050", + "H'4067", + "H'40E0", + "H'BA31", + "H'BF23", + "H'BF2D" + ], + "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.", + "tick_handler_address_hex": "H'BF23", + "timer": { + "clock_select": "CKS1=1 CKS0=0 => phi/32", + "handler_address_hex": "H'BF23", + "manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select", + "observed_period_ms_candidate": 700, + "ocra_address_hex": "H'FEA4", + "ocra_value_hex": "H'7A12", + "source": "FRT2 OCIA", + "tcr_address_hex": "H'FEA0", + "tcsr_address_hex": "H'FEA1", + "vector_address_hex": "H'006A", + "vector_target_label": "vec_frt2_ocia_BF23" + }, + "title": "loc_4046 idle heartbeat/report gate" + }, "queue_send_gate_loc_BAF2": { "items": [ { @@ -1330,6 +1509,87 @@ ], "write_count": 4 }, + { + "accesses": [ + { + "access": "read", + "address": 16454, + "address_hex": "H'4046", + "function": "loc_4046", + "text": "TST.B @H'F9C4" + }, + { + "access": "write", + "address": 16608, + "address_hex": "H'40E0", + "function": "loc_40BB", + "text": "MOV:G.B #H'14, @H'F9C4" + }, + { + "access": "write", + "address": 47665, + "address_hex": "H'BA31", + "function": "loc_BA26", + "text": "MOV:G.B #H'07, @H'F9C4" + }, + { + "access": "read", + "address": 48935, + "address_hex": "H'BF27", + "function": "vec_frt2_ocia_BF23", + "text": "TST.B @H'F9C4" + }, + { + "access": "read_write", + "address": 48941, + "address_hex": "H'BF2D", + "function": "vec_frt2_ocia_BF23", + "text": "ADD:Q.B #-1, @H'F9C4" + } + ], + "address": 63940, + "address_hex": "H'F9C4", + "read_count": 2, + "read_write_count": 1, + "sample_accesses": [ + { + "access": "read", + "address": 16454, + "address_hex": "H'4046", + "function": "loc_4046", + "text": "TST.B @H'F9C4" + }, + { + "access": "write", + "address": 16608, + "address_hex": "H'40E0", + "function": "loc_40BB", + "text": "MOV:G.B #H'14, @H'F9C4" + }, + { + "access": "write", + "address": 47665, + "address_hex": "H'BA31", + "function": "loc_BA26", + "text": "MOV:G.B #H'07, @H'F9C4" + }, + { + "access": "read", + "address": 48935, + "address_hex": "H'BF27", + "function": "vec_frt2_ocia_BF23", + "text": "TST.B @H'F9C4" + }, + { + "access": "read_write", + "address": 48941, + "address_hex": "H'BF2D", + "function": "vec_frt2_ocia_BF23", + "text": "ADD:Q.B #-1, @H'F9C4" + } + ], + "write_count": 2 + }, { "accesses": [ { @@ -1993,6 +2253,11 @@ "address_hex": "H'F9C3", "symbol": "ram_F9C3" }, + { + "address": 63940, + "address_hex": "H'F9C4", + "symbol": "ram_F9C4" + }, { "address": 63941, "address_hex": "H'F9C5", diff --git a/build/rom_serial_gate.txt b/build/rom_serial_gate.txt index 63f5613..89f6873 100644 --- a/build/rom_serial_gate.txt +++ b/build/rom_serial_gate.txt @@ -72,6 +72,29 @@ Evidence: - H'BDF7: BCLR.B #7, @H'F9B5 - H'BDFB: CLR.B @H'FAA3 - H'BDFF: CLR.B @H'FAA2 +- loc_4046 idle heartbeat/report gate: present + 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. + - H'4046: TST.B @H'F9C4 + - H'404A: BNE loc_4058 + - H'404C: BTST.B #7, @H'FAA5 + - H'4050: BEQ loc_4059 + - H'4052: TST.B @H'F9C3 + - H'4056: BEQ loc_4059 + - H'4058: RTS + - H'4059: MOV:G.B @H'F9B0, R2 + - H'405F: CMP:G.B @H'F9B5, R2 + - H'4063: BNE loc_4074 + - H'4067: MOV:G.W #H'00, @(-H'0790,R2) + - H'406C: ADD:Q.B #1, @H'F9B0 + - H'4070: BCLR.B #7, @H'F9B0 + - H'40E0: MOV:G.B #H'14, @H'F9C4 + - H'BA31: MOV:G.B #H'07, @H'F9C4 + - H'BF23: BCLR.B #5, @FRT2_TCSR + - H'BF27: TST.B @H'F9C4 + - H'BF2D: ADD:Q.B #-1, @H'F9C4 + Candidate timer roles: + - H'F9C4: candidate idle heartbeat/report gate countdown + Timer: FRT2 OCIA, H'BF23, OCRA=H'7A12, observed period ~= 700ms - FRT1 OCIA periodic tick countdowns: present Static evidence links vector H'0062 to the FRT1 OCIA handler at H'BEEA; the handler clears FRT1_TCSR.OCFA and conditionally decrements H'F9C0, H'F9C1, and H'F9C6. - H'BEEA: BCLR.B #5, @FRT1_TCSR @@ -101,6 +124,8 @@ State address readers/writers: H'BAED write CLR.B @H'F9C1; H'BB71 read TST.B @H'F9C1; H'BBA3 write MOV:G.B #H'05, @H'F9C1; H'BEF8 read TST.B @H'F9C1; H'BEFE read_write ADD:Q.B #-1, @H'F9C1 - H'F9C3: reads=6 writes=4 read/write=0 H'3FDF read TST.B @H'F9C3; H'4052 read TST.B @H'F9C3; H'BA90 read TST.B @H'F9C3; H'BB77 write CLR.B @H'F9C3; H'BB7D read CMP:G.B #H'05, @H'F9C3; H'BB8A read MOV:G.B @H'F9C3, R1 +- H'F9C4: reads=2 writes=2 read/write=1 + H'4046 read TST.B @H'F9C4; H'40E0 write MOV:G.B #H'14, @H'F9C4; H'BA31 write MOV:G.B #H'07, @H'F9C4; H'BF27 read TST.B @H'F9C4; H'BF2D read_write ADD:Q.B #-1, @H'F9C4 - H'F9C5: reads=2 writes=2 read/write=1 H'3FEF read TST.B @H'F9C5; H'BB9E write MOV:G.B #H'14, @H'F9C5; H'BEE4 write CLR.B @H'F9C5; H'BF31 read TST.B @H'F9C5; H'BF37 read_write ADD:Q.B #-1, @H'F9C5 - H'F9C6: reads=2 writes=2 read/write=1 diff --git a/build/rom_serial_pseudocode.c b/build/rom_serial_pseudocode.c index 76be88a..23fecbd 100644 --- a/build/rom_serial_pseudocode.c +++ b/build/rom_serial_pseudocode.c @@ -173,17 +173,17 @@ extern volatile u8 MEM8[0x10000]; * state variable candidates: * - event_queue_read_cursor_candidate H'F9B4: reads 1, writes 2; bits 5 * evidence: H'BE78, H'BE95, H'BE99 - * - event_queue_write_or_pending_cursor_candidate H'F9B5: reads 1, writes 6; bits 7 - * evidence: H'BAF2, H'BD6D, H'BD71, H'BDC8, H'BDCC, H'BDF3, H'BDF7 + * - event_queue_write_or_pending_cursor_candidate H'F9B5: reads 2, writes 6; bits 7 + * evidence: H'405F, H'BAF2, H'BD6D, H'BD71, H'BDC8, H'BDCC, H'BDF3, H'BDF7 * - event_queue_base_or_current_slot_candidate H'F9B9: reads 1, writes 0 * evidence: H'BE70 * - serial_tx_busy_timer_candidate H'F9C0: reads 2, writes 8 * evidence: H'BA26, H'BA2C, H'BAA2, H'BADA, H'BAE1, H'BAE8, H'BE1D, H'BE3E, H'BEEE, H'BEF4 - * - autonomous_report_period_timer_candidate H'F9C6: reads 1, writes 2 - * evidence: H'BB46, H'BEB5, H'BEC5 - * - autonomous_report_resend_countdown_candidate H'F9C8: reads 1, writes 2 - * evidence: H'BB4C, H'BEBB, H'BEC1 - * - ... 5 more state-variable candidates + * - idle_heartbeat_gate_countdown_candidate H'F9C4: reads 2, writes 3 + * evidence: H'4046, H'40E0, H'BA31, H'BF27, H'BF2D + * - rx_session_timeout_candidate H'F9C5: reads 1, writes 3 + * evidence: H'BB9E, H'BEE4, H'BF31, H'BF37 + * - ... 7 more state-variable candidates * retry/error model candidate: * - checksum path: 0x5A-seeded XOR over RX[0..4] differs from RX[5] -> loc_BE29 * - retry path: counter H'FAA6, threshold 2; Candidate retry path clears/consults serial flags, increments FAA6, compares it with 2, and when still below the apparent limit stages a command 0x07 response. @@ -191,13 +191,16 @@ extern volatile u8 MEM8[0x10000]; * - evidence: H'BBD8, H'BBDC, H'BBE0, H'BBE4, H'BBE8, H'BBEC, H'BBF0, H'BE4D, H'BE56, H'BE5E, H'BE66, H'BE52, H'BE5A, H'BE62, H'BE6A, H'BE29, H'BE2D, H'BE33, H'BE37, H'BE43, H'BE47, H'BE05, H'BE0D, H'BE15, H'BE09, H'BE11, H'BE19, H'BE22 * gate/queue state machine candidate: * - main_loop_may_enter_report_builder: FAA2 == 0 && F9C0 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)); Main-loop report gate; session must be idle, TX busy timer clear, and RX gate open. + * - idle_heartbeat_report_may_enqueue: F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5; 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. * - queue_has_pending_report: F9B5 != F9B0; Queue/pending cursor gate; non-empty state stages through BB43 before loc_BA26. * - periodic_resend_may_fire: (FAA5 & FAA3 & 0x80) != 0 && F9C6 == 0 && F9C8 != 0 after countdown; Resend gate masks pending state with FAA5, checks F9C6/F9C8, then calls BA26 at BED5. * - rx_completion_sets_session_timer: RX completion sets F9C5 (observed reload H'14) after the sixth byte is captured. * - session_timeout_clears_gate_and_queue: When F9C5 is clear, loc_3FEF clears F9B5/F9B0 and clears FAA5.bit7; when nonzero, it sets FAA5.bit7. + * - idle_heartbeat_gate_initial_delay_loaded: Startup/init loads F9C4 with H'14 before the first idle/default report can be queued. + * - idle_heartbeat_gate_post_send_delay_loaded: loc_BA26 reloads F9C4 with H'07 after each send, matching the observed heartbeat spacing. * - host_ack_can_advance_queue: Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.; commands H'05, H'06 * - caveat: Many panel controls may require host/session traffic before reporting. Observed autonomous call/camera-power indexes are runtime/capture overlays, not ROM constants. - * - evidence: H'3FD3, H'3FD7, H'3FD9, H'3FDD, H'3FDF, H'3FE3, H'3FE5, H'3FE9, H'3FEB, H'3FEF, H'3FF3, H'3FF5, H'3FF9, H'3FFD, H'4001, H'4003, H'4005, H'4007, H'BAF2, H'BAF6, H'BAF8, H'BAFC, H'BAFE, H'BB00, H'BB04, H'BB06, H'BB08, H'BB0C, H'BB0E, H'BB11, H'BB13, H'BB15, H'BB17, H'BB19, H'BB1C, H'BB20, H'BB24, H'BB26, H'BB29, H'BB2B, H'BB2F, H'BB33, H'BB35, H'BB39, H'BB3D, H'BB3F, H'BB43, H'BE9E, H'BEA2, H'BEA5, H'BEA9, H'BEAD, H'BEAF, H'BEB3, H'BEB5, H'BEB9, H'BEBB, H'BEBF, H'BEC1, H'BEC5, H'BECB, H'BECF, H'BED1, H'BED5 + * - evidence: H'3FD3, H'3FD7, H'3FD9, H'3FDD, H'3FDF, H'3FE3, H'3FE5, H'3FE9, H'3FEB, H'3FEF, H'3FF3, H'3FF5, H'3FF9, H'3FFD, H'4001, H'4003, H'4005, H'4007, H'4046, H'404A, H'404C, H'4050, H'4052, H'4056, H'4058, H'4059, H'405D, H'405F, H'4063, H'4065, H'4067, H'406C, H'4070, H'BAF2, H'BAF6, H'BAF8, H'BAFC, H'BAFE, H'BB00, H'BB04, H'BB06, H'BB08, H'BB0C, H'BB0E, H'BB11, H'BB13, H'BB15, H'BB17, H'BB19, H'BB1C, H'BB20, H'BB24, H'BB26, H'BB29, H'BB2B, H'BB2F, H'BB33, H'BB35, H'BB39, H'BB3D, H'BB3F, H'BB43, H'BE9E, H'BEA2, H'BEA5, H'BEA9, H'BEAD, H'BEAF, H'BEB3, H'BEB5, H'BEB9, H'BEBB, H'BEBF, H'BEC1, H'BEC5, H'BECB, H'BECF, H'BED1, H'BED5 * TX/autonomous report model candidate: * - loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate; byte5 is 0x5A XOR checksum * - observed overlay candidates: heartbeat_or_idle_report_candidate: 00 00 00 00 80 DA; call_button_report_candidate: 00 00 15 80 00 CF, 00 00 15 00 00 4F; camera_power_report_candidate: 00 00 07 80 00 DD @@ -210,10 +213,14 @@ extern volatile u8 MEM8[0x10000]; * - BED5 resend path: Candidate periodic resend path feeding the TX staging/send-builder flow. * - evidence: H'BB46, H'BEC5, H'BB4C, H'BB51, H'BECB, H'BED5 * interrupt/timer architecture candidate: - * - FRT1 OCIA H'BEEA appears to be a periodic tick ISR for serial gate/cadence counters. + * - FRT1 OCIA H'BEEA: Candidate periodic tick ISR for serial busy, interbyte, and resend counters. + * - FRT2 OCIA H'BF23: Candidate periodic tick ISR for idle heartbeat/report and RX session counters. * - H'F9C0 tx_report_gate_counter_candidate: candidate gate counter used before entering the report builder. * - H'F9C1 rx_interbyte_timeout_candidate: candidate RX interbyte timeout counter. * - H'F9C6 periodic_resend_cadence_counter_candidate: candidate periodic resend/heartbeat cadence counter. + * - H'F9C4 idle_heartbeat_gate_countdown_candidate: candidate idle/default report enqueue countdown. + * - H'F9C5 rx_session_timeout_candidate: candidate RX/session maintenance timeout counter. + * - evidence: H'BEEA, H'BEEE, H'BEF2, H'BEF4, H'BEF8, H'BEFC, H'BEFE, H'BF02, H'BF06, H'BF08, H'BF23, H'BF27, H'BF2B, H'BF2D, H'BF31, H'BF35, H'BF37 */ static u8 sci1_rx_candidate_command(void) @@ -257,6 +264,15 @@ static bool sci1_candidate_report_queue_nonempty(void) 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; @@ -268,7 +284,7 @@ static bool sci1_candidate_periodic_resend_gate_open(void) void frt1_ocia_candidate_tick_isr(void) { - /* Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters. */ + /* Candidate periodic tick at H'BEEA: decrement nonzero serial counters. */ /* TX_REPORT_GATE_COUNTER_CANDIDATE: candidate gate counter used before entering the report builder. */ if (MEM8[0xF9C0u] != 0u) { MEM8[0xF9C0u] = (u8)(MEM8[0xF9C0u] - 1u); @@ -286,6 +302,21 @@ void frt1_ocia_candidate_tick_isr(void) } +void frt2_ocia_candidate_tick_isr(void) +{ + /* Candidate periodic tick at H'BF23: decrement nonzero serial counters. */ + /* IDLE_HEARTBEAT_GATE_COUNTDOWN_CANDIDATE: candidate idle/default report enqueue countdown. */ + if (MEM8[0xF9C4u] != 0u) { + MEM8[0xF9C4u] = (u8)(MEM8[0xF9C4u] - 1u); + } + + /* RX_SESSION_TIMEOUT_CANDIDATE: candidate RX/session maintenance timeout counter. */ + if (MEM8[0xF9C5u] != 0u) { + MEM8[0xF9C5u] = (u8)(MEM8[0xF9C5u] - 1u); + } + +} + void sci1_process_candidate_protocol_command(void) { u8 command = sci1_rx_candidate_command(); diff --git a/h8536/emulator/fast_paths.py b/h8536/emulator/fast_paths.py index 838912d..bcaf974 100644 --- a/h8536/emulator/fast_paths.py +++ b/h8536/emulator/fast_paths.py @@ -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. diff --git a/h8536/emulator/probe.py b/h8536/emulator/probe.py index 20cf76f..c4690b4 100644 --- a/h8536/emulator/probe.py +++ b/h8536/emulator/probe.py @@ -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="", + 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) diff --git a/h8536/emulator/runner.py b/h8536/emulator/runner.py index 61eca40..d40fbf5 100644 --- a/h8536/emulator/runner.py +++ b/h8536/emulator/runner.py @@ -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) diff --git a/h8536/serial_gate.py b/h8536/serial_gate.py index 9248444..5042620 100644 --- a/h8536/serial_gate.py +++ b/h8536/serial_gate.py @@ -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 diff --git a/h8536/serial_pseudocode.py b/h8536/serial_pseudocode.py index fa22d7e..8ec5c39 100644 --- a/h8536/serial_pseudocode.py +++ b/h8536/serial_pseudocode.py @@ -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.", + }, ] diff --git a/h8536/serial_semantics.py b/h8536/serial_semantics.py index 0015b7b..9a88030 100644 --- a/h8536/serial_semantics.py +++ b/h8536/serial_semantics.py @@ -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 diff --git a/tests/test_emulator_addressing.py b/tests/test_emulator_addressing.py new file mode 100644 index 0000000..73672e4 --- /dev/null +++ b/tests/test_emulator_addressing.py @@ -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() diff --git a/tests/test_emulator_fast_paths.py b/tests/test_emulator_fast_paths.py index 521a289..d8463d4 100644 --- a/tests/test_emulator_fast_paths.py +++ b/tests/test_emulator_fast_paths.py @@ -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() diff --git a/tests/test_emulator_probe.py b/tests/test_emulator_probe.py index ad7567a..009678b 100644 --- a/tests/test_emulator_probe.py +++ b/tests/test_emulator_probe.py @@ -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() diff --git a/tests/test_serial_gate.py b/tests/test_serial_gate.py index cef92e3..0a822b0 100644 --- a/tests/test_serial_gate.py +++ b/tests/test_serial_gate.py @@ -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" diff --git a/tests/test_serial_pseudocode.py b/tests/test_serial_pseudocode.py index 42f9aca..e9a1cc8 100644 --- a/tests/test_serial_pseudocode.py +++ b/tests/test_serial_pseudocode.py @@ -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(), diff --git a/tests/test_serial_semantics.py b/tests/test_serial_semantics.py index 02077e6..9614200 100644 --- a/tests/test_serial_semantics.py +++ b/tests/test_serial_semantics.py @@ -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": []},