1
0

emualtor working

This commit is contained in:
Aiden
2026-05-25 21:00:25 +10:00
parent 3ab79648ff
commit 752148c585
22 changed files with 588 additions and 22 deletions

View File

@@ -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_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_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_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 .\.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`. - 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`. - 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. - 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. - 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`. - 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`. - 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`. - Idle frame: `00 00 00 00 80 DA`.
- Capture-side label: `heartbeat_alive_candidate`. - 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. - 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. - 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: The generated listing is written to:
@@ -110,6 +114,7 @@ build/rom_serial_pseudocode.c
build/rom_serial_gate.txt build/rom_serial_gate.txt
build/rom_report_sources.txt build/rom_report_sources.txt
build/rom_table_xrefs.txt build/rom_table_xrefs.txt
build/rom_consistency.txt
build/callgraph.dot build/callgraph.dot
``` ```
@@ -172,11 +177,13 @@ For gate/queue and table reports:
python h8536_serial_gate.py --help python h8536_serial_gate.py --help
python h8536_report_source_trace.py --help python h8536_report_source_trace.py --help
python h8536_table_xrefs.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_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_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_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: For the emulator harness:
@@ -194,7 +201,7 @@ python h8536_emulator_probe.py --help
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration. - `--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. - `--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. - `--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 ## Code Layout
@@ -224,6 +231,7 @@ python h8536_emulator_probe.py --help
- `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction. - `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction.
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer. - `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/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/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/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. - `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_pseudocode.py`: pseudocode CLI wrapper.
- `h8536_serial_pseudocode.py`: focused serial 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_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. - `h8536_emulator.py`, `h8536_emulator_probe.py`: emulator CLI wrappers.

View File

@@ -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.

View File

@@ -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": { "serial_semantics": {
"kind": "serial_semantics", "kind": "serial_semantics",
"protocol_semantics": [ "protocol_semantics": [
@@ -196427,7 +196463,7 @@
"entry_label": "loc_4046", "entry_label": "loc_4046",
"target_label": "loc_4067", "target_label": "loc_4067",
"condition_candidate": "F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5", "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": [ "state_addresses_hex": [
"H'F9C4", "H'F9C4",
"H'FAA5", "H'FAA5",
@@ -196435,7 +196471,26 @@
"H'F9B0", "H'F9B0",
"H'F9B5" "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": [ "evidence_addresses": [
16454, 16454,
16458, 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.", "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", "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.", "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", "entry_label": "loc_4046",
"target_label": "loc_4067", "target_label": "loc_4067",
"condition_candidate": "F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5", "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": [ "state_addresses_hex": [
"H'F9C4", "H'F9C4",
"H'FAA5", "H'FAA5",
@@ -206685,7 +206766,26 @@
"H'F9B0", "H'F9B0",
"H'F9B5" "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": [ "evidence_addresses": [
16454, 16454,
16458, 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.", "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", "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.", "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.",

View File

@@ -22,6 +22,8 @@ u16 SR;
u8 CCR, BR, EP, DP, TP; u8 CCR, BR, EP, DP, TP;
int C, Z, N, V; 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. */ /* H8/536 register field symbols used by this ROM. */
extern volatile u8 P1DDR; /* 0xFE80 */ extern volatile u8 P1DDR; /* 0xFE80 */
extern volatile u8 P1DR; /* 0xFE82 */ 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 */ 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_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_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 */ 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_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_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 */ 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_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 */ 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 */ 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 */ if (Z) { /* 4063; BNE loc_4074; cycles=3/8 nt/t */
R2 <<= 1; /* 4065; SHLL.B R2; cycles=2 */ 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] += (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 */ MEM8[0xF9B0] &= ~BIT(7); /* 4070; BCLR.B #7, @H'F9B0; refs ram_F9B0; cycles=9 */
} }

View File

@@ -167,7 +167,7 @@
"H'BF23", "H'BF23",
"H'BF2D" "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", "tick_handler_address_hex": "H'BF23",
"timer": { "timer": {
"clock_select": "CKS1=1 CKS0=0 => phi/32", "clock_select": "CKS1=1 CKS0=0 => phi/32",

View File

@@ -73,7 +73,7 @@ Evidence:
- H'BDFB: CLR.B @H'FAA3 - H'BDFB: CLR.B @H'FAA3
- H'BDFF: CLR.B @H'FAA2 - H'BDFF: CLR.B @H'FAA2
- loc_4046 idle heartbeat/report gate: present - 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'4046: TST.B @H'F9C4
- H'404A: BNE loc_4058 - H'404A: BNE loc_4058
- H'404C: BTST.B #7, @H'FAA5 - H'404C: BTST.B #7, @H'FAA5

View File

@@ -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 * - 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: * 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. * - 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. * - 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. * - 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. * - 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: * 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 * - 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 * - 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. * - 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 * - evidence: H'BB1C, H'BB20, H'BB2B, H'BB39, H'BB3F, H'BB43
* heartbeat/periodic resend candidate: * 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; 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) static bool sci1_candidate_periodic_resend_gate_open(void)
{ {
bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u; bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;

211
h8536/consistency.py Normal file
View File

@@ -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",
]

View File

@@ -829,7 +829,7 @@ def _report_gate_decision(pc: int, *, f9c4: int, faa5: int, f9c3: int, head: int
if pc == 0x4063: if pc == 0x4063:
return "return_queue_not_empty" if not z else "enqueue_zero_report" return "return_queue_not_empty" if not z else "enqueue_zero_report"
if pc == 0x4067: if pc == 0x4067:
return "write_report_00ff_to_queue_slot" return "write_report_0000_to_queue_slot"
if pc == 0x406C: if pc == 0x406C:
return "advance_report_queue_head" return "advance_report_queue_head"
if pc == 0x4070: if pc == 0x4070:
@@ -1197,7 +1197,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
action="append", action="append",
type=parse_watch_pc, type=parse_watch_pc,
default=[], 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("--report-queue-watch-hit-limit", type=int, default=32)
parser.add_argument( parser.add_argument(

View File

@@ -207,8 +207,9 @@ class H8536Emulator:
if op in (0x06, 0x07): if op in (0x06, 0x07):
value = raw[-1] if op == 0x06 else int.from_bytes(raw[-2:], "big") value = raw[-1] if op == 0x06 else int.from_bytes(raw[-2:], "big")
self._write_ea(ea, value, 1 if op == 0x06 else 2) write_size = size if op == 0x06 else 2
self._set_logic_flags(value, 1 if op == 0x06 else 2) self._write_ea(ea, value, write_size)
self._set_logic_flags(value, write_size)
return next_pc return next_pc
base = op & 0xF8 base = op & 0xF8

View File

@@ -7,6 +7,8 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from .consistency import is_byte_immediate_to_word_destination
JsonObject = dict[str, Any] JsonObject = dict[str, Any]
@@ -195,6 +197,8 @@ def _file_header(source_name: str, payload: JsonObject) -> list[str]:
"u8 CCR, BR, EP, DP, TP;", "u8 CCR, BR, EP, DP, TP;",
"int C, Z, N, V;", "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: 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) source = _format_operand(ops[0], size)
dest = _format_operand(ops[1], size, lvalue=True) 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)};" return f"{dest} = {_cast(source, size)};"
if base in {"MOVFPE"} and len(ops) == 2: 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"): if isinstance(item, dict) and item.get("comment"):
comments.append(str(item["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") board_profile = ins.get("board_profile")
if isinstance(board_profile, dict) and board_profile.get("comment"): if isinstance(board_profile, dict) and board_profile.get("comment"):
comments.append(str(board_profile["comment"])) comments.append(str(board_profile["comment"]))

View File

@@ -4,6 +4,7 @@ import json
from pathlib import Path from pathlib import Path
from .board_profile import board_comment_for_instruction, board_json_payload, board_metadata_for_instruction 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 .cycles import cycle_comment
from .dataflow import state_for_instruction from .dataflow import state_for_instruction
from .dtc import DtcEndpointInfo, DtcRegisterInfo from .dtc import DtcEndpointInfo, DtcRegisterInfo
@@ -491,6 +492,7 @@ def write_json(
for ins in (instructions[addr] for addr in sorted(instructions)) for ins in (instructions[addr] for addr in sorted(instructions))
], ],
} }
payload["decompiler_consistency"] = analyze_decompiler_consistency(payload)
payload["serial_semantics"] = analyze_serial_semantics(payload) payload["serial_semantics"] = analyze_serial_semantics(payload)
path.write_text(json.dumps(payload, indent=2), encoding="utf-8") path.write_text(json.dumps(payload, indent=2), encoding="utf-8")

View File

@@ -316,7 +316,7 @@ def _idle_heartbeat_gate(payload: dict[str, Any], by_address: dict[int, JsonObje
"summary": ( "summary": (
"F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send " "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 " "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 " "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." "the observed heartbeat cadence."
), ),

View File

@@ -635,6 +635,20 @@ def _gate_queue_comment_lines(
condition = _comment_text(str(predicate.get("condition_candidate") or "condition unknown")) condition = _comment_text(str(predicate.get("condition_candidate") or "condition unknown"))
summary = _comment_text(str(predicate.get("summary") or "candidate gate")) summary = _comment_text(str(predicate.get("summary") or "candidate gate"))
lines.append(f"{prefix}- {name}: {condition}; {summary}") 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")): for effect in _object_list(value.get("session_effects")):
name = effect.get("name") or "session_effect_candidate" name = effect.get("name") or "session_effect_candidate"
summary = _comment_text(str(effect.get("summary") or "candidate session effect")) 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;", " 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)", "static bool sci1_candidate_periodic_resend_gate_open(void)",
"{", "{",
" bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;", " bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;",
@@ -711,6 +735,21 @@ def _tx_report_comment_lines(
observed.append(f"{name}: {frames}") observed.append(f"{name}: {frames}")
if observed: if observed:
lines.append(f"{prefix}- observed overlay candidates: {_comment_text('; '.join(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() caveat = str(value.get("observed_autonomous_output_caveat") or value.get("caveat") or "").strip()
if caveat: if caveat:
lines.append(f"{prefix}- caveat: {_comment_text(caveat)}") lines.append(f"{prefix}- caveat: {_comment_text(caveat)}")

View File

@@ -1683,10 +1683,24 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
), ),
"summary": ( "summary": (
"Idle/default report gate; when the FRT2 countdown clears and the queue is " "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)], "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( "evidence_addresses": _addresses_in_ranges(
ordered, ordered,
[(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)], [(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", "value_source_candidate": "current_value_table_candidate",
"checksum_formula": "checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4", "checksum_formula": "checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4",
"observed_capture_overlay_candidates": OBSERVED_TX_REPORT_OVERLAY, "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": ( "observed_autonomous_output_caveat": (
"Real captures supplied so far show only heartbeat/idle, call, and camera-power " "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 " "autonomous TX frames. Other panel controls may require a host/device request or "

5
h8536_consistency.py Normal file
View File

@@ -0,0 +1,5 @@
from h8536.consistency import main
if __name__ == "__main__":
raise SystemExit(main())

36
tests/test_consistency.py Normal file
View File

@@ -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()

View File

@@ -47,6 +47,17 @@ class EmulatorAddressingTest(unittest.TestCase):
self.assertEqual(emulator.cpu.regs[0], 0x00C7) self.assertEqual(emulator.cpu.regs[0], 0x00C7)
self.assertEqual(emulator.cpu.pc, 0x1006) 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): def test_signed_byte_displacement_addresses_below_base(self):
rom = rom_with_reset() rom = rom_with_reset()
rom[0x1000:0x1003] = b"\xE1\xFE\x81" # MOV:G.B @(H'-02,R1), R1 rom[0x1000:0x1003] = b"\xE1\xFE\x81" # MOV:G.B @(H'-02,R1), R1

View File

@@ -352,7 +352,7 @@ class EmulatorProbeTest(unittest.TestCase):
self.assertEqual(decisions[0x4046], "f9c4_zero_continue") self.assertEqual(decisions[0x4046], "f9c4_zero_continue")
self.assertEqual(decisions[0x4050], "enqueue_candidate_faa5_clear") self.assertEqual(decisions[0x4050], "enqueue_candidate_faa5_clear")
self.assertEqual(decisions[0x4063], "enqueue_zero_report") 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())) self.assertTrue(any("recent_report_gates:" == line for line in report.lines()))

View File

@@ -121,6 +121,41 @@ class PseudocodeTest(unittest.TestCase):
self.assertIn("loc_0110:", text) self.assertIn("loc_0110:", text)
self.assertIn("return;", 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -334,6 +334,21 @@ class SerialPseudocodeTest(unittest.TestCase):
"observed_frames_hex": ["00 00 07 80 00 DD"], "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.", "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"], "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_main_report_gate_open(void)", text)
self.assertIn("static bool sci1_candidate_report_queue_nonempty(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_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("static bool sci1_candidate_periodic_resend_gate_open(void)", text)
self.assertIn("TX/autonomous report model candidate:", 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("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("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("heartbeat/periodic resend candidate:", text)
self.assertIn("F9C6 reload H'01F4", text) self.assertIn("F9C6 reload H'01F4", text)
self.assertIn("BED5 resend path", text) self.assertIn("BED5 resend path", text)

View File

@@ -354,6 +354,8 @@ class SerialSemanticsTest(unittest.TestCase):
self.assertIn("bytes 0..2", report_text) self.assertIn("bytes 0..2", report_text)
self.assertIn("current_value_table", report_text) self.assertIn("current_value_table", report_text)
self.assertIn("00 00 15 80 00 cf", 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) self.assertIn("host/device request", report_text)
def test_periodic_resend_model_marks_heartbeat_constants(self): 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("faa2 == 0", gate_text)
self.assertIn("f9c0 == 0", gate_text) self.assertIn("f9c0 == 0", gate_text)
self.assertIn("f9c4 == 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("f9b5 != f9b0", gate_text)
self.assertIn("bb43", gate_text) self.assertIn("bb43", gate_text)
self.assertIn("be9e", gate_text) self.assertIn("be9e", gate_text)