From 752148c585286cc4138640a8869f30ed0b487199 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 25 May 2026 21:00:25 +1000 Subject: [PATCH] emualtor working --- README.md | 14 +- build/rom_consistency.txt | 9 ++ build/rom_decompiled.json | 134 ++++++++++++++++++- build/rom_pseudocode.c | 8 +- build/rom_serial_gate.json | 2 +- build/rom_serial_gate.txt | 2 +- build/rom_serial_pseudocode.c | 17 ++- h8536/consistency.py | 211 ++++++++++++++++++++++++++++++ h8536/emulator/probe.py | 4 +- h8536/emulator/runner.py | 5 +- h8536/pseudocode.py | 9 ++ h8536/render.py | 2 + h8536/serial_gate.py | 2 +- h8536/serial_pseudocode.py | 39 ++++++ h8536/serial_semantics.py | 39 +++++- h8536_consistency.py | 5 + tests/test_consistency.py | 36 +++++ tests/test_emulator_addressing.py | 11 ++ tests/test_emulator_probe.py | 2 +- tests/test_pseudocode.py | 35 +++++ tests/test_serial_pseudocode.py | 18 +++ tests/test_serial_semantics.py | 6 +- 22 files changed, 588 insertions(+), 22 deletions(-) create mode 100644 build/rom_consistency.txt create mode 100644 h8536/consistency.py create mode 100644 h8536_consistency.py create mode 100644 tests/test_consistency.py diff --git a/README.md b/README.md index 2e5a41a..703c3f9 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ To run the newer sidecar protocol and gate/queue analysis tools: .\.venv\Scripts\python.exe h8536_serial_gate.py build\rom_decompiled.json --out build\rom_serial_gate.txt .\.venv\Scripts\python.exe h8536_report_source_trace.py build\rom_decompiled.json --out build\rom_report_sources.txt .\.venv\Scripts\python.exe h8536_table_xrefs.py --out build\rom_table_xrefs.txt +.\.venv\Scripts\python.exe h8536_consistency.py build\rom_decompiled.json --out build\rom_consistency.txt .\.venv\Scripts\python.exe h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt ``` @@ -60,6 +61,8 @@ To start the current emulator harness: - Reconstructs evidence-supported SCI1 serial frame candidates, including the apparent six-byte TX/RX units and XOR checksum seeded by `0x5A`. - Infers candidate serial protocol semantics from validated frames, including `RX[0] & 0x07` command dispatch, likely index/value byte roles, and response staging through `F850-F854`. - Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates. +- Marks H8 word-destination writes fed by byte immediates as explicit zero-extension in pseudocode, including the heartbeat queue write at `loc_4067`. +- Emits a decompiler/pseudocode consistency report for width semantics that are easy to misread. - 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`, the `F9C4`/FRT2 idle heartbeat gate at `loc_4046`, and the resend path through `BE9E/BED5`. @@ -87,7 +90,8 @@ 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. +- 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'0000` when it reaches zero and the queue is empty. +- Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`. - 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: @@ -110,6 +114,7 @@ build/rom_serial_pseudocode.c build/rom_serial_gate.txt build/rom_report_sources.txt build/rom_table_xrefs.txt +build/rom_consistency.txt build/callgraph.dot ``` @@ -172,11 +177,13 @@ For gate/queue and table reports: python h8536_serial_gate.py --help python h8536_report_source_trace.py --help python h8536_table_xrefs.py --help +python h8536_consistency.py --help ``` - `h8536_serial_gate.py`: reports the autonomous TX gate and report queue evidence. - `h8536_report_source_trace.py`: traces direct `loc_3E54` report enqueue sources. Current finding: no direct static `R3 = 0x0007` enqueue in the JSON, so CAM power `0x0007` remains runtime/capture-observed unless a later indirect/table path proves it. - `h8536_table_xrefs.py`: emits candidate table/index xrefs and LCD text correlation hints. +- `h8536_consistency.py`: flags JSON-to-pseudocode semantic hazards such as byte immediates written to word destinations. For the emulator harness: @@ -194,7 +201,7 @@ python h8536_emulator_probe.py --help - `--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. +- 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. With the P9 fast path and current timer cadence, the emulator reaches the SCI1 transmit path and emits the observed heartbeat frame `00 00 00 00 80 DA`. ## Code Layout @@ -224,6 +231,7 @@ python h8536_emulator_probe.py --help - `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction. - `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer. - `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation. +- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks. - `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, P9 bus model, runner, probe, CLI, and peripheral scaffolding. - `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path. - `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis. @@ -233,5 +241,5 @@ python h8536_emulator_probe.py --help - `h8536_pseudocode.py`: pseudocode CLI wrapper. - `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper. - `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers. -- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`: sidecar analysis CLI wrappers. +- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers. - `h8536_emulator.py`, `h8536_emulator_probe.py`: emulator CLI wrappers. diff --git a/build/rom_consistency.txt b/build/rom_consistency.txt new file mode 100644 index 0000000..7c73a97 --- /dev/null +++ b/build/rom_consistency.txt @@ -0,0 +1,9 @@ +Decompiler/Pseudocode Consistency +3 byte-immediate-to-word destination case(s) require explicit zero-extension in pseudocode. + +- H'1043: MOV:G.W #H'00, @FRT1_FRC_H [requires_zero_extend8_to16_pseudocode] + Word-sized MOV with an 8-bit immediate writes a zero-extended word. Pseudocode should not model this as a one-byte write or preserve the old low byte. +- H'1058: MOV:G.W #H'00, @FRT2_FRC_H [requires_zero_extend8_to16_pseudocode] + Word-sized MOV with an 8-bit immediate writes a zero-extended word. Pseudocode should not model this as a one-byte write or preserve the old low byte. +- H'4067: MOV:G.W #H'00, @(-H'0790,R2) [requires_zero_extend8_to16_pseudocode] + Word-sized MOV with an 8-bit immediate writes a zero-extended word. Pseudocode should not model this as a one-byte write or preserve the old low byte. diff --git a/build/rom_decompiled.json b/build/rom_decompiled.json index df5cbfa..fb9c4c1 100644 --- a/build/rom_decompiled.json +++ b/build/rom_decompiled.json @@ -186062,6 +186062,42 @@ } } ], + "decompiler_consistency": { + "kind": "decompiler_pseudocode_consistency", + "summary": "3 byte-immediate-to-word destination case(s) require explicit zero-extension in pseudocode.", + "checks": [ + { + "kind": "byte_immediate_to_word_destination", + "status": "requires_zero_extend8_to16_pseudocode", + "address": 4163, + "address_hex": "H'1043", + "instruction": "MOV:G.W #H'00, @FRT1_FRC_H", + "expected_pseudocode_hint": "zero_extend8_to16", + "zero_extended_value_hex": "0x0000", + "summary": "Word-sized MOV with an 8-bit immediate writes a zero-extended word. Pseudocode should not model this as a one-byte write or preserve the old low byte." + }, + { + "kind": "byte_immediate_to_word_destination", + "status": "requires_zero_extend8_to16_pseudocode", + "address": 4184, + "address_hex": "H'1058", + "instruction": "MOV:G.W #H'00, @FRT2_FRC_H", + "expected_pseudocode_hint": "zero_extend8_to16", + "zero_extended_value_hex": "0x0000", + "summary": "Word-sized MOV with an 8-bit immediate writes a zero-extended word. Pseudocode should not model this as a one-byte write or preserve the old low byte." + }, + { + "kind": "byte_immediate_to_word_destination", + "status": "requires_zero_extend8_to16_pseudocode", + "address": 16487, + "address_hex": "H'4067", + "instruction": "MOV:G.W #H'00, @(-H'0790,R2)", + "expected_pseudocode_hint": "zero_extend8_to16", + "zero_extended_value_hex": "0x0000", + "summary": "Word-sized MOV with an 8-bit immediate writes a zero-extended word. Pseudocode should not model this as a one-byte write or preserve the old low byte." + } + ] + }, "serial_semantics": { "kind": "serial_semantics", "protocol_semantics": [ @@ -196427,7 +196463,7 @@ "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.", + "summary": "Idle/default report gate; when the FRT2 countdown clears and the queue is empty, loc_4046 can enqueue H'0000 for the later loc_BAF2 -> loc_BA26 send path.", "state_addresses_hex": [ "H'F9C4", "H'FAA5", @@ -196435,7 +196471,26 @@ "H'F9B0", "H'F9B5" ], - "enqueued_report_candidate_hex": "H'00FF", + "enqueued_report_candidate_hex": "H'0000", + "write_semantics_candidate": "loc_4067 is MOV:G.W #H'00, @(-H'0790,R2): the byte immediate is zero-extended by the word destination, so the queue slot becomes H'0000.", + "runtime_trace_confirmation": { + "source": "h8536_emulator_probe target-frame run", + "report_id_hex": "H'0000", + "queue_write_address_hex": "H'4067", + "queue_write_semantics": "H'FFFF -> H'0000, not H'00FF", + "dequeue_path": [ + "loc_4046", + "loc_BAF2", + "loc_BB08", + "loc_BB1C", + "loc_BB20", + "loc_BB2B", + "loc_BA26" + ], + "emitted_frame_hex": "00 00 00 00 80 DA", + "checksum_seed_hex": "H'5A", + "checksum_hex": "H'DA" + }, "evidence_addresses": [ 16454, 16458, @@ -196847,6 +196902,32 @@ ] } ], + "runtime_confirmed_paths": [ + { + "name": "idle_heartbeat_report_runtime_confirmation", + "report_id_hex": "H'0000", + "queue_write_address_hex": "H'4067", + "queue_write_semantics": "MOV:G.W #H'00 writes H'0000 to the queue slot", + "staging_path": [ + "loc_4046", + "loc_BAF2", + "loc_BB08", + "loc_BB1C", + "loc_BB20", + "loc_BB2B", + "loc_BA26" + ], + "emitted_frame_hex": "00 00 00 00 80 DA", + "checksum_hex": "H'DA" + } + ], + "consistency_checks": [ + { + "name": "idle_heartbeat_report_id_width", + "status": "pass", + "summary": "Decompiler mnemonic MOV:G.W and emulator execution now agree that the H'00 immediate at loc_4067 is zero-extended to report H'0000." + } + ], "observed_autonomous_output_caveat": "Real captures supplied so far show only heartbeat/idle, call, and camera-power autonomous TX frames. Other panel controls may require a host/device request or state transition before the firmware reports them.", "confidence": "candidate-medium", "caveat": "This is a TX/report model for the BB43 -> BA26 path, separate from RX command dispatch. Observed report names are a capture overlay candidate only, not hard-coded source truth.", @@ -206677,7 +206758,7 @@ "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.", + "summary": "Idle/default report gate; when the FRT2 countdown clears and the queue is empty, loc_4046 can enqueue H'0000 for the later loc_BAF2 -> loc_BA26 send path.", "state_addresses_hex": [ "H'F9C4", "H'FAA5", @@ -206685,7 +206766,26 @@ "H'F9B0", "H'F9B5" ], - "enqueued_report_candidate_hex": "H'00FF", + "enqueued_report_candidate_hex": "H'0000", + "write_semantics_candidate": "loc_4067 is MOV:G.W #H'00, @(-H'0790,R2): the byte immediate is zero-extended by the word destination, so the queue slot becomes H'0000.", + "runtime_trace_confirmation": { + "source": "h8536_emulator_probe target-frame run", + "report_id_hex": "H'0000", + "queue_write_address_hex": "H'4067", + "queue_write_semantics": "H'FFFF -> H'0000, not H'00FF", + "dequeue_path": [ + "loc_4046", + "loc_BAF2", + "loc_BB08", + "loc_BB1C", + "loc_BB20", + "loc_BB2B", + "loc_BA26" + ], + "emitted_frame_hex": "00 00 00 00 80 DA", + "checksum_seed_hex": "H'5A", + "checksum_hex": "H'DA" + }, "evidence_addresses": [ 16454, 16458, @@ -207097,6 +207197,32 @@ ] } ], + "runtime_confirmed_paths": [ + { + "name": "idle_heartbeat_report_runtime_confirmation", + "report_id_hex": "H'0000", + "queue_write_address_hex": "H'4067", + "queue_write_semantics": "MOV:G.W #H'00 writes H'0000 to the queue slot", + "staging_path": [ + "loc_4046", + "loc_BAF2", + "loc_BB08", + "loc_BB1C", + "loc_BB20", + "loc_BB2B", + "loc_BA26" + ], + "emitted_frame_hex": "00 00 00 00 80 DA", + "checksum_hex": "H'DA" + } + ], + "consistency_checks": [ + { + "name": "idle_heartbeat_report_id_width", + "status": "pass", + "summary": "Decompiler mnemonic MOV:G.W and emulator execution now agree that the H'00 immediate at loc_4067 is zero-extended to report H'0000." + } + ], "observed_autonomous_output_caveat": "Real captures supplied so far show only heartbeat/idle, call, and camera-power autonomous TX frames. Other panel controls may require a host/device request or state transition before the firmware reports them.", "confidence": "candidate-medium", "caveat": "This is a TX/report model for the BB43 -> BA26 path, separate from RX command dispatch. Observed report names are a capture overlay candidate only, not hard-coded source truth.", diff --git a/build/rom_pseudocode.c b/build/rom_pseudocode.c index 5c728c1..ce2c1d6 100644 --- a/build/rom_pseudocode.c +++ b/build/rom_pseudocode.c @@ -22,6 +22,8 @@ u16 SR; u8 CCR, BR, EP, DP, TP; int C, Z, N, V; +static inline u16 zero_extend8_to16(u8 value) { return (u16)value; } + /* H8/536 register field symbols used by this ROM. */ extern volatile u8 P1DDR; /* 0xFE80 */ extern volatile u8 P1DR; /* 0xFE82 */ @@ -468,11 +470,11 @@ void vec_reset_1000(void) SYSCR2 = (uint8_t)(0x84); /* 1034; MOV:G.B #H'84, @SYSCR2; SYSCR2 = H'84 (IRQ5E=0 IRQ4E=0 IRQ3E=0 IRQ2E=0 P6PWME=1 P9PWME=0 P9SCI2E=0; enabled P6 PWM); SYSCR2 write leaves P9SCI2E=0; SCI2 pins are disabled, so SCI2 is not the traced MAX202 path; traced RS232/MAX202 remains SCI1 P95/P96; refs SYSCR2; cycles=9 */ FRT1_TCR = (uint8_t)(0x02); /* 1039; MOV:G.B #H'02, @FRT1_TCR; FRT1_TCR = H'02 (ICIE=0 OCIEB=0 OCIEA=0 OVIE=0 OEB=0 OEA=0 CKS1=1 CKS0=0); refs FRT1_TCR; cycles=9 */ FRT1_TCSR = (uint8_t)(0x01); /* 103E; MOV:G.B #H'01, @FRT1_TCSR; FRT1_TCSR = H'01 (ICF=0 OCFB=0 OCFA=0 OVF=0 OLVLB=0 OLVLA=0 IEDG=0 CCLRA=1); refs FRT1_TCSR; cycles=9 */ - FRT1_FRC_H = (uint16_t)(0x00); /* 1043; MOV:G.W #H'00, @FRT1_FRC_H; FRT1_FRC_H = H'00; refs FRT1_FRC_H; FRT1_FRC W write high TEMP access; cycles=9 */ + FRT1_FRC_H = zero_extend8_to16(0x00); /* 1043; MOV:G.W #H'00, @FRT1_FRC_H; FRT1_FRC_H = H'00; byte immediate zero-extended into word destination; refs FRT1_FRC_H; FRT1_FRC W write high TEMP access; cycles=9 */ FRT1_OCRA_H = (uint16_t)(0x009C); /* 1048; MOV:G.W #H'009C, @FRT1_OCRA_H; FRT1_OCRA_H = H'9C; refs FRT1_OCRA_H; FRT1_OCRA W write high TEMP access; cycles=11 */ FRT2_TCR = (uint8_t)(0x02); /* 104E; MOV:G.B #H'02, @FRT2_TCR; FRT2_TCR = H'02 (ICIE=0 OCIEB=0 OCIEA=0 OVIE=0 OEB=0 OEA=0 CKS1=1 CKS0=0); refs FRT2_TCR; cycles=9 */ FRT2_TCSR = (uint8_t)(0x01); /* 1053; MOV:G.B #H'01, @FRT2_TCSR; FRT2_TCSR = H'01 (ICF=0 OCFB=0 OCFA=0 OVF=0 OLVLB=0 OLVLA=0 IEDG=0 CCLRA=1); refs FRT2_TCSR; cycles=9 */ - FRT2_FRC_H = (uint16_t)(0x00); /* 1058; MOV:G.W #H'00, @FRT2_FRC_H; FRT2_FRC_H = H'00; refs FRT2_FRC_H; FRT2_FRC W write high TEMP access; cycles=11 */ + FRT2_FRC_H = zero_extend8_to16(0x00); /* 1058; MOV:G.W #H'00, @FRT2_FRC_H; FRT2_FRC_H = H'00; byte immediate zero-extended into word destination; refs FRT2_FRC_H; FRT2_FRC W write high TEMP access; cycles=11 */ FRT2_OCRA_H = (uint16_t)(0x7A12); /* 105D; MOV:G.W #H'7A12, @FRT2_OCRA_H; FRT2_OCRA_H = H'7A12; refs FRT2_OCRA_H; FRT2_OCRA W write high TEMP access; cycles=9 */ FRT3_TCR = (uint8_t)(0x00); /* 1063; MOV:G.B #H'00, @FRT3_TCR; FRT3_TCR = H'00 (ICIE=0 OCIEB=0 OCIEA=0 OVIE=0 OEB=0 OEA=0 CKS1=0 CKS0=0); refs FRT3_TCR; cycles=9 */ FRT3_TCSR = (uint8_t)(0x00); /* 1068; MOV:G.B #H'00, @FRT3_TCSR; FRT3_TCSR = H'00 (ICF=0 OCFB=0 OCFA=0 OVF=0 OLVLB=0 OLVLA=0 IEDG=0 CCLRA=0); refs FRT3_TCSR; cycles=9 */ @@ -3701,7 +3703,7 @@ loc_4059: set_flags_cmp8(R2, MEM8[0xF9B5]); /* 405F; CMP:G.B @H'F9B5, R2; refs ram_F9B5; cycles=6 */ if (Z) { /* 4063; BNE loc_4074; cycles=3/8 nt/t */ R2 <<= 1; /* 4065; SHLL.B R2; cycles=2 */ - MEM16[R2 - 0x0790] = (uint16_t)(0x00); /* 4067; MOV:G.W #H'00, @(-H'0790,R2); cycles=11 */ + MEM16[R2 - 0x0790] = zero_extend8_to16(0x00); /* 4067; MOV:G.W #H'00, @(-H'0790,R2); byte immediate zero-extended into word destination; cycles=11 */ MEM8[0xF9B0] += (uint8_t)(1); /* 406C; ADD:Q.B #1, @H'F9B0; refs ram_F9B0; cycles=9 */ MEM8[0xF9B0] &= ~BIT(7); /* 4070; BCLR.B #7, @H'F9B0; refs ram_F9B0; cycles=9 */ } diff --git a/build/rom_serial_gate.json b/build/rom_serial_gate.json index 4567fda..4ef9ef9 100644 --- a/build/rom_serial_gate.json +++ b/build/rom_serial_gate.json @@ -167,7 +167,7 @@ "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.", + "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'0000 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", diff --git a/build/rom_serial_gate.txt b/build/rom_serial_gate.txt index 89f6873..26aff48 100644 --- a/build/rom_serial_gate.txt +++ b/build/rom_serial_gate.txt @@ -73,7 +73,7 @@ Evidence: - 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. + 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'0000 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 diff --git a/build/rom_serial_pseudocode.c b/build/rom_serial_pseudocode.c index 23fecbd..fe07477 100644 --- a/build/rom_serial_pseudocode.c +++ b/build/rom_serial_pseudocode.c @@ -191,7 +191,10 @@ 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. + * - 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'0000 for the later loc_BAF2 -> loc_BA26 send path. + * enqueues report H'0000 + * write semantics: loc_4067 is MOV:G.W #H'00, @(-H'0790,R2): the byte immediate is zero-extended by the word destination, so the queue slot becomes H'0000. + * runtime-confirmed frame 00 00 00 00 80 DA via loc_4046 -> loc_BAF2 -> loc_BB08 -> loc_BB1C -> loc_BB20 -> loc_BB2B -> loc_BA26 * - 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. @@ -204,6 +207,8 @@ extern volatile u8 MEM8[0x10000]; * 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 + * - runtime confirmation: idle_heartbeat_report_runtime_confirmation: report H'0000 emits 00 00 00 00 80 DA; MOV:G.W #H'00 writes H'0000 to the queue slot + * - consistency idle_heartbeat_report_id_width: pass; Decompiler mnemonic MOV:G.W and emulator execution now agree that the H'00 immediate at loc_4067 is zero-extended to report H'0000. * - caveat: Real captures supplied so far show only heartbeat/idle, call, and camera-power autonomous TX frames. Other panel controls may require a host/device request or state transition before the firmware reports them. * - evidence: H'BB1C, H'BB20, H'BB2B, H'BB39, H'BB3F, H'BB43 * heartbeat/periodic resend candidate: @@ -273,6 +278,16 @@ static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void) return idle_timer_clear && rx_gate_open && queue_empty; } +static void sci1_candidate_enqueue_idle_heartbeat_report(void) +{ + if (!sci1_candidate_idle_heartbeat_enqueue_gate_open()) { + return; + } + + /* loc_4067 writes MOV:G.W #H'00, so the queue report id is 0x0000. */ + candidate_enqueue_report(0x0000u); +} + static bool sci1_candidate_periodic_resend_gate_open(void) { bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u; diff --git a/h8536/consistency.py b/h8536/consistency.py new file mode 100644 index 0000000..38d26a0 --- /dev/null +++ b/h8536/consistency.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any, Mapping + + +JsonObject = dict[str, Any] + + +def analyze_decompiler_consistency(payload: Mapping[str, Any]) -> JsonObject: + """Flag decompiler/pseudocode semantic cases that are easy to misread.""" + width_checks = [ + _byte_immediate_word_write_check(ins) + for ins in _instruction_sequence(payload.get("instructions")) + if is_byte_immediate_to_word_destination(ins) + ] + width_checks = [check for check in width_checks if check] + return { + "kind": "decompiler_pseudocode_consistency", + "summary": _summary(width_checks), + "checks": width_checks, + } + + +def format_consistency_report(analysis: Mapping[str, Any]) -> str: + lines = [ + "Decompiler/Pseudocode Consistency", + str(analysis.get("summary") or "No checks emitted."), + "", + ] + checks = analysis.get("checks") + if not isinstance(checks, list) or not checks: + return "\n".join(lines).rstrip() + "\n" + for check in checks: + if not isinstance(check, Mapping): + continue + lines.append( + f"- {check.get('address_hex', '?')}: {check.get('instruction', '')} " + f"[{check.get('status', 'info')}]", + ) + summary = check.get("summary") + if summary: + lines.append(f" {summary}") + return "\n".join(lines).rstrip() + "\n" + + +def is_byte_immediate_to_word_destination(instruction: Mapping[str, Any]) -> bool: + mnemonic = str(instruction.get("mnemonic") or "") + if _mnemonic_base(mnemonic) not in {"MOV:G", "MOV"} or _mnemonic_size(mnemonic) != "W": + return False + operands = _split_operands(str(instruction.get("operands") or "")) + if len(operands) != 2: + return False + source = operands[0].strip() + if not source.startswith("#"): + return False + literal = _immediate_literal_text(source[1:]) + return literal is not None and len(literal) <= 2 + + +def _byte_immediate_word_write_check(instruction: Mapping[str, Any]) -> JsonObject: + address = int(instruction.get("address") or 0) + immediate = _immediate_value(_split_operands(str(instruction.get("operands") or ""))[0]) + value_text = f"0x{immediate:04X}" if immediate is not None else "zero-extended byte" + return { + "kind": "byte_immediate_to_word_destination", + "status": "requires_zero_extend8_to16_pseudocode", + "address": address, + "address_hex": _h16(address), + "instruction": str(instruction.get("text") or _instruction_text(instruction)), + "expected_pseudocode_hint": "zero_extend8_to16", + "zero_extended_value_hex": value_text, + "summary": ( + "Word-sized MOV with an 8-bit immediate writes a zero-extended word. " + "Pseudocode should not model this as a one-byte write or preserve the old low byte." + ), + } + + +def _summary(width_checks: list[JsonObject]) -> str: + if not width_checks: + return "No byte-immediate-to-word destination cases found." + return ( + f"{len(width_checks)} byte-immediate-to-word destination case(s) require " + "explicit zero-extension in pseudocode." + ) + + +def _instruction_sequence(value: object) -> list[Mapping[str, Any]]: + if not isinstance(value, list): + return [] + instructions = [item for item in value if isinstance(item, Mapping)] + return sorted(instructions, key=lambda item: int(item.get("address") or 0)) + + +def _split_operands(operands: str) -> list[str]: + if not operands: + return [] + parts: list[str] = [] + start = 0 + depth = 0 + for idx, char in enumerate(operands): + if char in "({": + depth += 1 + elif char in ")}" and depth: + depth -= 1 + elif char == "," and depth == 0: + parts.append(operands[start:idx].strip()) + start = idx + 1 + parts.append(operands[start:].strip()) + return [part for part in parts if part] + + +def _immediate_literal_text(text: str) -> str | None: + stripped = text.strip() + h_match = re.fullmatch(r"H'([0-9A-Fa-f]+)", stripped) + if h_match: + return h_match.group(1) + x_match = re.fullmatch(r"0x([0-9A-Fa-f]+)", stripped) + if x_match: + return x_match.group(1) + decimal_match = re.fullmatch(r"\d+", stripped) + if decimal_match: + value = int(stripped, 10) + if 0 <= value <= 0xFF: + return f"{value:02X}" + return None + + +def _immediate_value(operand: str) -> int | None: + stripped = operand.strip() + if stripped.startswith("#"): + stripped = stripped[1:].strip() + literal = _immediate_literal_text(stripped) + if literal is None: + return None + return int(literal, 16) + + +def _instruction_text(instruction: Mapping[str, Any]) -> str: + mnemonic = str(instruction.get("mnemonic") or "") + operands = str(instruction.get("operands") or "") + return f"{mnemonic} {operands}".strip() + + +def _mnemonic_base(mnemonic: str) -> str: + return mnemonic.rsplit(".", 1)[0] if "." in mnemonic else mnemonic + + +def _mnemonic_size(mnemonic: str) -> str: + suffix = mnemonic.rsplit(".", 1)[-1] if "." in mnemonic else "" + return suffix if suffix in {"B", "W"} else "" + + +def _h16(value: int) -> str: + return f"H'{value & 0xFFFF:04X}" + + +def load_consistency_input(path: Path) -> JsonObject: + with path.open("r", encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict) or "instructions" not in payload: + raise ValueError(f"{path} does not look like h8536_decompiler JSON output") + return payload + + +def write_consistency_report(input_path: Path, output_path: Path, *, json_output: bool = False) -> None: + analysis = analyze_decompiler_consistency(load_consistency_input(input_path)) + output_path.parent.mkdir(parents=True, exist_ok=True) + if json_output: + output_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8") + else: + output_path.write_text(format_consistency_report(analysis), encoding="utf-8") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Report decompiler/pseudocode semantic consistency checks.", + ) + parser.add_argument( + "input", + nargs="?", + type=Path, + default=Path("build/rom_decompiled.json"), + help="structured JSON emitted by h8536_decompiler.py", + ) + parser.add_argument( + "--out", + type=Path, + default=Path("build/rom_consistency.txt"), + help="consistency report output path", + ) + parser.add_argument("--json", action="store_true", help="write JSON instead of text") + args = parser.parse_args(argv) + + write_consistency_report(args.input, args.out, json_output=args.json) + print(f"wrote {args.out}") + return 0 + + +__all__ = [ + "analyze_decompiler_consistency", + "format_consistency_report", + "is_byte_immediate_to_word_destination", + "load_consistency_input", + "main", + "write_consistency_report", +] diff --git a/h8536/emulator/probe.py b/h8536/emulator/probe.py index c4690b4..796cf2e 100644 --- a/h8536/emulator/probe.py +++ b/h8536/emulator/probe.py @@ -829,7 +829,7 @@ def _report_gate_decision(pc: int, *, f9c4: int, faa5: int, f9c3: int, head: int 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" + return "write_report_0000_to_queue_slot" if pc == 0x406C: return "advance_report_queue_head" if pc == 0x4070: @@ -1197,7 +1197,7 @@ def build_arg_parser() -> argparse.ArgumentParser: action="append", type=parse_watch_pc, default=[], - help="highlight queue traces involving a logical report id, e.g. 00FF or 0x00FF", + help="highlight queue traces involving a logical report id, e.g. 0015 or 0x0015", ) parser.add_argument("--report-queue-watch-hit-limit", type=int, default=32) parser.add_argument( diff --git a/h8536/emulator/runner.py b/h8536/emulator/runner.py index d40fbf5..8e4ca46 100644 --- a/h8536/emulator/runner.py +++ b/h8536/emulator/runner.py @@ -207,8 +207,9 @@ class H8536Emulator: if op in (0x06, 0x07): value = raw[-1] if op == 0x06 else int.from_bytes(raw[-2:], "big") - self._write_ea(ea, value, 1 if op == 0x06 else 2) - self._set_logic_flags(value, 1 if op == 0x06 else 2) + write_size = size if op == 0x06 else 2 + self._write_ea(ea, value, write_size) + self._set_logic_flags(value, write_size) return next_pc base = op & 0xF8 diff --git a/h8536/pseudocode.py b/h8536/pseudocode.py index 27705f6..140242a 100644 --- a/h8536/pseudocode.py +++ b/h8536/pseudocode.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from pathlib import Path from typing import Any +from .consistency import is_byte_immediate_to_word_destination + JsonObject = dict[str, Any] @@ -195,6 +197,8 @@ def _file_header(source_name: str, payload: JsonObject) -> list[str]: "u8 CCR, BR, EP, DP, TP;", "int C, Z, N, V;", "", + "static inline u16 zero_extend8_to16(u8 value) { return (u16)value; }", + "", ] @@ -648,6 +652,8 @@ def _translate_instruction(ins: JsonObject, labels: dict[int, str]) -> str: if base in {"MOV", "MOV:G", "MOV:I", "MOV:E", "MOV:L", "MOV:S", "MOV:F"} and len(ops) == 2: source = _format_operand(ops[0], size) dest = _format_operand(ops[1], size, lvalue=True) + if is_byte_immediate_to_word_destination(ins): + return f"{dest} = zero_extend8_to16({source});" return f"{dest} = {_cast(source, size)};" if base in {"MOVFPE"} and len(ops) == 2: @@ -908,6 +914,9 @@ def _metadata_comments(ins: JsonObject) -> list[str]: if isinstance(item, dict) and item.get("comment"): comments.append(str(item["comment"])) + if is_byte_immediate_to_word_destination(ins): + comments.append("byte immediate zero-extended into word destination") + board_profile = ins.get("board_profile") if isinstance(board_profile, dict) and board_profile.get("comment"): comments.append(str(board_profile["comment"])) diff --git a/h8536/render.py b/h8536/render.py index b34a44f..21a35d5 100644 --- a/h8536/render.py +++ b/h8536/render.py @@ -4,6 +4,7 @@ import json from pathlib import Path from .board_profile import board_comment_for_instruction, board_json_payload, board_metadata_for_instruction +from .consistency import analyze_decompiler_consistency from .cycles import cycle_comment from .dataflow import state_for_instruction from .dtc import DtcEndpointInfo, DtcRegisterInfo @@ -491,6 +492,7 @@ def write_json( for ins in (instructions[addr] for addr in sorted(instructions)) ], } + payload["decompiler_consistency"] = analyze_decompiler_consistency(payload) payload["serial_semantics"] = analyze_serial_semantics(payload) path.write_text(json.dumps(payload, indent=2), encoding="utf-8") diff --git a/h8536/serial_gate.py b/h8536/serial_gate.py index 5042620..a38a611 100644 --- a/h8536/serial_gate.py +++ b/h8536/serial_gate.py @@ -316,7 +316,7 @@ def _idle_heartbeat_gate(payload: dict[str, Any], by_address: dict[int, JsonObje "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 " + "can enqueue H'0000 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." ), diff --git a/h8536/serial_pseudocode.py b/h8536/serial_pseudocode.py index 8ec5c39..1d5bf2e 100644 --- a/h8536/serial_pseudocode.py +++ b/h8536/serial_pseudocode.py @@ -635,6 +635,20 @@ def _gate_queue_comment_lines( condition = _comment_text(str(predicate.get("condition_candidate") or "condition unknown")) summary = _comment_text(str(predicate.get("summary") or "candidate gate")) lines.append(f"{prefix}- {name}: {condition}; {summary}") + enqueued = predicate.get("enqueued_report_candidate_hex") + if enqueued: + lines.append(f"{prefix} enqueues report {enqueued}") + write_semantics = str(predicate.get("write_semantics_candidate") or "").strip() + if write_semantics: + lines.append(f"{prefix} write semantics: {_comment_text(write_semantics)}") + runtime = predicate.get("runtime_trace_confirmation") + if isinstance(runtime, dict): + frame = runtime.get("emitted_frame_hex") + path = " -> ".join(str(item) for item in runtime.get("dequeue_path", []) if item) + detail = f"runtime-confirmed frame {frame}" if frame else "runtime-confirmed path" + if path: + detail += f" via {path}" + lines.append(f"{prefix} {detail}") for effect in _object_list(value.get("session_effects")): name = effect.get("name") or "session_effect_candidate" summary = _comment_text(str(effect.get("summary") or "candidate session effect")) @@ -677,6 +691,16 @@ def _gate_queue_predicate_function_lines(value: object) -> list[str]: " return idle_timer_clear && rx_gate_open && queue_empty;", "}", "", + "static void sci1_candidate_enqueue_idle_heartbeat_report(void)", + "{", + " if (!sci1_candidate_idle_heartbeat_enqueue_gate_open()) {", + " return;", + " }", + "", + " /* loc_4067 writes MOV:G.W #H'00, so the queue report id is 0x0000. */", + " candidate_enqueue_report(0x0000u);", + "}", + "", "static bool sci1_candidate_periodic_resend_gate_open(void)", "{", " bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;", @@ -711,6 +735,21 @@ def _tx_report_comment_lines( observed.append(f"{name}: {frames}") if observed: lines.append(f"{prefix}- observed overlay candidates: {_comment_text('; '.join(observed))}") + for runtime in _object_list(value.get("runtime_confirmed_paths")): + name = runtime.get("name") or "runtime_confirmation" + frame = runtime.get("emitted_frame_hex") or "frame?" + report = runtime.get("report_id_hex") or "report?" + summary = f"{name}: report {report} emits {frame}" + semantics = runtime.get("queue_write_semantics") + if semantics: + summary += f"; {semantics}" + lines.append(f"{prefix}- runtime confirmation: {_comment_text(summary)}") + checks = _object_list(value.get("consistency_checks")) + for check in checks: + name = check.get("name") or "consistency_check" + status = check.get("status") or "info" + summary = _comment_text(str(check.get("summary") or "")) + lines.append(f"{prefix}- consistency {name}: {status}; {summary}") caveat = str(value.get("observed_autonomous_output_caveat") or value.get("caveat") or "").strip() if caveat: lines.append(f"{prefix}- caveat: {_comment_text(caveat)}") diff --git a/h8536/serial_semantics.py b/h8536/serial_semantics.py index 9a88030..83148e0 100644 --- a/h8536/serial_semantics.py +++ b/h8536/serial_semantics.py @@ -1683,10 +1683,24 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) -> ), "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." + "empty, loc_4046 can enqueue H'0000 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), + "enqueued_report_candidate_hex": _h16(0x0000), + "write_semantics_candidate": ( + "loc_4067 is MOV:G.W #H'00, @(-H'0790,R2): the byte immediate is " + "zero-extended by the word destination, so the queue slot becomes H'0000." + ), + "runtime_trace_confirmation": { + "source": "h8536_emulator_probe target-frame run", + "report_id_hex": _h16(0x0000), + "queue_write_address_hex": _h16(IDLE_REPORT_QUEUE_WRITE), + "queue_write_semantics": "H'FFFF -> H'0000, not H'00FF", + "dequeue_path": ["loc_4046", "loc_BAF2", "loc_BB08", "loc_BB1C", "loc_BB20", "loc_BB2B", "loc_BA26"], + "emitted_frame_hex": "00 00 00 00 80 DA", + "checksum_seed_hex": _h16(CHECKSUM_SEED, width=2), + "checksum_hex": "H'DA", + }, "evidence_addresses": _addresses_in_ranges( ordered, [(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)], @@ -1965,6 +1979,27 @@ def _tx_report_model(ordered: list[JsonObject], responses: list[JsonObject]) -> "value_source_candidate": "current_value_table_candidate", "checksum_formula": "checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4", "observed_capture_overlay_candidates": OBSERVED_TX_REPORT_OVERLAY, + "runtime_confirmed_paths": [ + { + "name": "idle_heartbeat_report_runtime_confirmation", + "report_id_hex": _h16(0x0000), + "queue_write_address_hex": _h16(IDLE_REPORT_QUEUE_WRITE), + "queue_write_semantics": "MOV:G.W #H'00 writes H'0000 to the queue slot", + "staging_path": ["loc_4046", "loc_BAF2", "loc_BB08", "loc_BB1C", "loc_BB20", "loc_BB2B", "loc_BA26"], + "emitted_frame_hex": "00 00 00 00 80 DA", + "checksum_hex": "H'DA", + } + ], + "consistency_checks": [ + { + "name": "idle_heartbeat_report_id_width", + "status": "pass", + "summary": ( + "Decompiler mnemonic MOV:G.W and emulator execution now agree that the " + "H'00 immediate at loc_4067 is zero-extended to report H'0000." + ), + } + ], "observed_autonomous_output_caveat": ( "Real captures supplied so far show only heartbeat/idle, call, and camera-power " "autonomous TX frames. Other panel controls may require a host/device request or " diff --git a/h8536_consistency.py b/h8536_consistency.py new file mode 100644 index 0000000..2a7fe38 --- /dev/null +++ b/h8536_consistency.py @@ -0,0 +1,5 @@ +from h8536.consistency import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_consistency.py b/tests/test_consistency.py new file mode 100644 index 0000000..57ee8d5 --- /dev/null +++ b/tests/test_consistency.py @@ -0,0 +1,36 @@ +import unittest + +from h8536.consistency import analyze_decompiler_consistency, format_consistency_report + + +class ConsistencyTest(unittest.TestCase): + def test_flags_byte_immediate_word_destination_cases(self): + payload = { + "instructions": [ + { + "address": 0x4067, + "text": "MOV:G.W #H'00, @(-H'0790,R2)", + "mnemonic": "MOV:G.W", + "operands": "#H'00, @(-H'0790,R2)", + }, + { + "address": 0x5000, + "text": "MOV:I.W #H'1234, R0", + "mnemonic": "MOV:I.W", + "operands": "#H'1234, R0", + }, + ], + } + + analysis = analyze_decompiler_consistency(payload) + + self.assertEqual(len(analysis["checks"]), 1) + check = analysis["checks"][0] + self.assertEqual(check["address"], 0x4067) + self.assertEqual(check["zero_extended_value_hex"], "0x0000") + self.assertIn("zero-extended word", check["summary"]) + self.assertIn("H'4067", format_consistency_report(analysis)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_emulator_addressing.py b/tests/test_emulator_addressing.py index 73672e4..8b480b6 100644 --- a/tests/test_emulator_addressing.py +++ b/tests/test_emulator_addressing.py @@ -47,6 +47,17 @@ class EmulatorAddressingTest(unittest.TestCase): self.assertEqual(emulator.cpu.regs[0], 0x00C7) self.assertEqual(emulator.cpu.pc, 0x1006) + def test_byte_immediate_to_word_destination_writes_zero_extended_word(self): + rom = rom_with_reset() + rom[0x1000:0x1005] = b"\x1D\xF8\x70\x06\xAB" # MOV:G.W #H'AB, @H'F870 + + emulator = H8536Emulator(bytes(rom)) + emulator.memory.write16(0xF870, 0xFFFF) + emulator.step() + + self.assertEqual(emulator.memory.read16(0xF870), 0x00AB) + self.assertEqual(emulator.cpu.pc, 0x1005) + 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 diff --git a/tests/test_emulator_probe.py b/tests/test_emulator_probe.py index 009678b..2302e1c 100644 --- a/tests/test_emulator_probe.py +++ b/tests/test_emulator_probe.py @@ -352,7 +352,7 @@ class EmulatorProbeTest(unittest.TestCase): 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.assertEqual(decisions[0x4067], "write_report_0000_to_queue_slot") self.assertTrue(any("recent_report_gates:" == line for line in report.lines())) diff --git a/tests/test_pseudocode.py b/tests/test_pseudocode.py index 3a19fc1..71cb1e3 100644 --- a/tests/test_pseudocode.py +++ b/tests/test_pseudocode.py @@ -121,6 +121,41 @@ class PseudocodeTest(unittest.TestCase): self.assertIn("loc_0110:", text) self.assertIn("return;", text) + def test_zero_extends_byte_immediate_to_word_destination(self): + payload = { + "vectors": [{"address": 0, "name": "reset", "target": 0x4067, "target_label": "loc_4067"}], + "call_graph": { + "nodes": [ + { + "start": 0x4067, + "end": 0x4067, + "label": "loc_4067", + "sources": ["reset"], + "instruction_count": 1, + "calls": [], + }, + ], + "edges": [], + }, + "instructions": [ + { + "address": 0x4067, + "text": "MOV:G.W #H'00, @(-H'0790,R2)", + "mnemonic": "MOV:G.W", + "operands": "#H'00, @(-H'0790,R2)", + "kind": "normal", + "targets": [], + "references": [], + "comment": "", + }, + ], + } + + text = generate_pseudocode(payload, options=PseudocodeOptions(structured=False)) + + self.assertIn("zero_extend8_to16(0x00)", text) + self.assertIn("byte immediate zero-extended into word destination", text) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_serial_pseudocode.py b/tests/test_serial_pseudocode.py index e9a1cc8..93e9212 100644 --- a/tests/test_serial_pseudocode.py +++ b/tests/test_serial_pseudocode.py @@ -334,6 +334,21 @@ class SerialPseudocodeTest(unittest.TestCase): "observed_frames_hex": ["00 00 07 80 00 DD"], }, ], + "runtime_confirmed_paths": [ + { + "name": "idle_heartbeat_report_runtime_confirmation", + "report_id_hex": "H'0000", + "queue_write_semantics": "MOV:G.W #H'00 writes H'0000 to the queue slot", + "emitted_frame_hex": "00 00 00 00 80 DA", + } + ], + "consistency_checks": [ + { + "name": "idle_heartbeat_report_id_width", + "status": "pass", + "summary": "MOV:G.W zero-extends the H'00 immediate.", + } + ], "observed_autonomous_output_caveat": "Observed autonomous output is limited to heartbeat/call/cam-power; other controls may require host/device requests first.", "evidence_addresses_hex": ["H'BB20", "H'BB43"], }, @@ -404,10 +419,13 @@ class SerialPseudocodeTest(unittest.TestCase): 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("candidate_enqueue_report(0x0000u);", 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) self.assertIn("heartbeat_or_idle_report_candidate: 00 00 00 00 80 DA", text) + self.assertIn("runtime confirmation: idle_heartbeat_report_runtime_confirmation: report H'0000 emits 00 00 00 00 80 DA", text) + self.assertIn("consistency idle_heartbeat_report_id_width: pass", text) self.assertIn("heartbeat/periodic resend candidate:", text) self.assertIn("F9C6 reload H'01F4", text) self.assertIn("BED5 resend path", text) diff --git a/tests/test_serial_semantics.py b/tests/test_serial_semantics.py index 9614200..0c5b8f0 100644 --- a/tests/test_serial_semantics.py +++ b/tests/test_serial_semantics.py @@ -354,6 +354,8 @@ class SerialSemanticsTest(unittest.TestCase): self.assertIn("bytes 0..2", report_text) self.assertIn("current_value_table", report_text) self.assertIn("00 00 15 80 00 cf", report_text) + self.assertIn("idle_heartbeat_report_runtime_confirmation", report_text) + self.assertIn("idle_heartbeat_report_id_width", report_text) self.assertIn("host/device request", report_text) def test_periodic_resend_model_marks_heartbeat_constants(self): @@ -381,7 +383,9 @@ class SerialSemanticsTest(unittest.TestCase): 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("h'0000", gate_text) + self.assertIn("zero-extended", gate_text) + self.assertIn("00 00 00 00 80 da", gate_text) self.assertIn("f9b5 != f9b0", gate_text) self.assertIn("bb43", gate_text) self.assertIn("be9e", gate_text)