1
0

Compare commits

..

12 Commits

Author SHA1 Message Date
Aiden
6d4d9f0027 Emula;tor bench mimicing 2026-05-25 22:00:25 +10:00
Aiden
191b72d418 LCD emulation 2026-05-25 21:33:19 +10:00
Aiden
e141f3b30d Emualtor RX side 2026-05-25 21:25:10 +10:00
Aiden
752148c585 emualtor working 2026-05-25 21:00:25 +10:00
Aiden
3ab79648ff EMualtor adjustments 2026-05-25 20:42:45 +10:00
Aiden
d2e7609bbf Emulator learnings folded back into decompiler 2026-05-25 19:11:22 +10:00
Aiden
1fabf6587d Emualtor improements 2026-05-25 18:55:50 +10:00
Aiden
05e1237acc EMualtor im 2026-05-25 18:43:36 +10:00
Aiden
81f5d7a150 emulator improvements 2026-05-25 18:07:55 +10:00
Aiden
9d93d88840 Emulator improvements 2026-05-25 17:55:07 +10:00
Aiden
b264037e82 further digging and basic emulator 2026-05-25 17:42:58 +10:00
Aiden
07f48c76e0 More decompiling work 2026-05-25 17:32:00 +10:00
80 changed files with 20235 additions and 126 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
*.pyc
captures/

120
README.md
View File

@@ -26,6 +26,29 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction
.\.venv\Scripts\python.exe h8536_serial_pseudocode.py build\rom_decompiled.json --out build\rom_serial_pseudocode.c
```
To run the newer sidecar protocol and gate/queue analysis tools:
```powershell
.\.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
```
To start the current emulator harness:
```powershell
.\.venv\Scripts\python.exe h8536_emulator.py --max-steps 1000000 --stop-on-heartbeat --interval-steps 512
.\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 4000000 --stop-on-tx
.\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 1000000 --stop-on-tx --p9-fast-path
.\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --preset connect-lcd
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen
.\.venv\Scripts\python.exe h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity
```
The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed.
## What It Does
- Decodes the H8/500 instruction set used by the H8/536.
@@ -43,6 +66,13 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction
- 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`.
- Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations.
- Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations.
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
- Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers.
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
@@ -56,6 +86,24 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path.
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects.
- Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs.
- Includes a bench-log replay harness that feeds recorded host TX frames back into the ROM emulator and asserts parity against the real device's observed response/LCD state.
Current serial observations:
- Idle capture reference: `ROM/rcp-txd-idle-only.txt`.
- 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'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`.
- Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue.
- RX probe finding: the `--preset connect-lcd` sequence reaches the command-`0x04` handler; `04 00 00 80 00 DE` writes table index zero, fills the LCD line buffer with `CONNECT: OK`, and emits `02 00 02 00 00 5A` in the current emulator model.
- Bench follow-up: replaying the emulator CONNECT sequence on the real device did not switch the LCD to OK. The real device answered the `04 00 00 80 00 DE` step with `07 80 C0 60 20 5D` in the captured run and remained at `CONNECT NOT ACT`, which points to a missing gate/session precondition in the emulator.
- 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:
@@ -69,6 +117,18 @@ The optional JSON output is useful for scripts or later analysis:
build/rom_decompiled.json
```
Common derived outputs:
```text
build/rom_pseudocode.c
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
```
## Useful Options
```powershell
@@ -111,6 +171,54 @@ python h8536_serial_pseudocode.py --help
- `--no-board`: omit board/MAX202 comments.
- `--no-semantics`: omit candidate command/field semantics.
For protocol trace and capture logs:
```powershell
python h8536_protocol_trace.py --help
python h8536_protocol_capture.py --help
```
- `h8536_protocol_trace.py --direction tx 00 00 15 80 00 CF`: decode raw bytes as protocol frames.
- `h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt`: parse timestamped captures, recombine split chunks, validate checksums, and summarize cadence/gate hints.
- `--json` on the capture tool emits machine-readable frame and cadence data.
For gate/queue and table reports:
```powershell
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:
```powershell
python h8536_emulator.py --help
python h8536_emulator_probe.py --help
python h8536_emulator_rx_probe.py --help
```
- `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`.
- `--max-steps N`: bound execution.
- `--trace`: print executed instructions.
- `--stop-on-heartbeat`: stop only if `00 00 00 00 80 DA` is emitted through SCI1 TDR.
- `--interval-steps N`: tune the scaffolded interval timer cadence.
- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: tune rough FRT compare-interrupt cadence.
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration.
- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history.
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
- `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text.
- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator and intentionally fail while the emulator still emits `02 00 02 00 00 5A` instead of the bench-observed `07 80 C0 60 20 5D`.
- 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, schedules FRT1/FRT2 OCIA, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`.
## Code Layout
- `h8536_decompiler.py`: compatibility wrapper for the CLI.
@@ -134,6 +242,14 @@ python h8536_serial_pseudocode.py --help
- `h8536/serial_reconstruction.py`: cautious higher-level SCI frame reconstruction from decompiled evidence.
- `h8536/serial_semantics.py`: candidate command/field semantics inferred from serial frame use.
- `h8536/serial_pseudocode.py`: focused RX/TX protocol pseudocode generation from reconstruction metadata.
- `h8536/protocol_trace.py`: raw six-byte protocol frame decoder/checksum validator.
- `h8536/protocol_capture.py`: timestamped serial capture parser, frame recombiner, and cadence/gate-session analyzer.
- `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, LCD model, runner, probe, CLI, and peripheral scaffolding.
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
- `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/pseudocode.py`: JSON-to-C-like pseudocode generation.
@@ -141,3 +257,7 @@ python h8536_serial_pseudocode.py --help
- `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers.
- `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`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers.
- `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence.

54
ROM/rcp-txd-idle-only.txt Normal file
View File

@@ -0,0 +1,54 @@
11:54:40.567 frame 006 00 00 00 00 80 DA
11:54:41.368 frame 006 00 00 00 00 80 DA
11:54:41.970 frame 006 00 00 00 00 80 DA
11:54:42.772 frame 006 00 00 00 00 80 DA
11:54:43.373 frame 006 00 00 00 00 80 DA
11:54:44.176 frame 006 00 00 00 00 80 DA
11:54:44.778 frame 006 00 00 00 00 80 DA
11:54:45.580 frame 006 00 00 00 00 80 DA
11:54:46.183 frame 006 00 00 00 00 80 DA
11:54:46.984 frame 006 00 00 00 00 80 DA
11:54:47.586 frame 006 00 00 00 00 80 DA
11:54:48.387 frame 006 00 00 00 00 80 DA
11:54:48.988 frame 006 00 00 00 00 80 DA
11:54:49.790 frame 006 00 00 00 00 80 DA
11:54:50.393 frame 006 00 00 00 00 80 DA
11:54:51.196 frame 006 00 00 00 00 80 DA
11:54:51.797 frame 006 00 00 00 00 80 DA
11:54:52.599 frame 006 00 00 00 00 80 DA
11:54:53.201 frame 006 00 00 00 00 80 DA
11:54:54.003 frame 006 00 00 00 00 80 DA
11:54:54.604 frame 006 00 00 00 00 80 DA
11:54:55.406 frame 006 00 00 00 00 80 DA
11:54:56.009 frame 006 00 00 00 00 80 DA
11:54:56.812 frame 006 00 00 00 00 80 DA
11:54:57.413 frame 006 00 00 00 00 80 DA
11:54:58.215 frame 006 00 00 00 00 80 DA
11:54:58.816 frame 006 00 00 00 00 80 DA
11:54:59.619 frame 006 00 00 00 00 80 DA
11:55:00.220 frame 006 00 00 00 00 80 DA
11:55:01.023 frame 006 00 00 00 00 80 DA
11:55:01.625 frame 006 00 00 00 00 80 DA
11:55:02.426 frame 006 00 00 00 00 80 DA
11:55:03.027 frame 006 00 00 00 00 80 DA
11:55:03.829 frame 006 00 00 00 00 80 DA
11:55:04.430 frame 006 00 00 00 00 80 DA
11:55:05.231 frame 006 00 00 00 00 80 DA
11:55:05.832 frame 006 00 00 00 00 80 DA
11:55:06.634 frame 006 00 00 00 00 80 DA
11:55:07.235 frame 006 00 00 00 00 80 DA
11:55:07.837 frame 006 00 00 00 00 80 DA
11:55:08.638 frame 006 00 00 00 00 80 DA
11:55:09.239 frame 006 00 00 00 00 80 DA
11:55:10.040 frame 006 00 00 00 00 80 DA
11:55:10.642 frame 006 00 00 00 00 80 DA
11:55:11.443 frame 006 00 00 00 00 80 DA
11:55:12.045 frame 006 00 00 00 00 80 DA
11:55:12.848 frame 006 00 00 00 00 80 DA
11:55:13.450 frame 006 00 00 00 00 80 DA
11:55:14.253 frame 006 00 00 00 00 80 DA
11:55:14.854 frame 006 00 00 00 00 80 DA
11:55:15.657 frame 006 00 00 00 00 80 DA
11:55:16.259 frame 006 00 00 00 00 80 DA
11:55:17.061 frame 006 00 00 00 00 80 DA
11:55:17.663 frame 006 00 00 00 00 80 DA

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

@@ -232,8 +232,13 @@
; Serial Protocol Reconstruction
; TX candidate: 6 bytes H'F858-H'F85D, checksum H'F85D seeded by H'005A (confidence high 0.95)
; TX path: initial byte is written from the TX frame buffer, then subsequent bytes are sent by the TXI path when TDRE is reasserted
; RX candidate: 6 bytes capture H'F868-H'F86D, validate H'F860-H'F865 checksum H'F865 seeded by H'005A (confidence high 0.9)
; caveat: candidate frame means six consecutive bytes within the observed RX timing/state machine, not a proven delimited packet
; Serial RAM role candidates
; H'F9C0: post_tx_report_delay - post_tx_report_delay at H'F9C0 is a candidate/evidence-supported RAM timer role; FRT1 OCIA tick ISR H'BEEA decrements it
; H'F9C1: secondary_tx_report_delay - secondary_tx_report_delay at H'F9C1 is a candidate/evidence-supported RAM timer role; FRT1 OCIA tick ISR H'BEEA decrements it
; H'F9C6: periodic_report_countdown - periodic_report_countdown at H'F9C6 is a candidate/evidence-supported RAM timer role; FRT1 OCIA tick ISR H'BEEA decrements it
; LCD/Text Scan
; search 'CONNECT': not literal, hits=0
@@ -430,10 +435,10 @@ vec_reset_1000:
108B: 15 FE C8 06 3B MOV:G.B #H'3B, @PWM3_TCR ; PWM3_TCR = H'3B (OE=0 OS=0 CKS2=0 CKS1=1 CKS0=1); cycles=9
1090: 15 FE C9 06 7D MOV:G.B #H'7D, @PWM3_DTR ; PWM3_DTR = H'7D; cycles=9
1095: 15 FE D8 06 24 MOV:G.B #H'24, @SCI1_SMR ; SCI1_SMR = H'24 (C/A=0 CHR=0 PE=1 O/E=0 STOP=0 CKS1=0 CKS0=0; SCI async, 8-bit, even parity, 1 stop, clock phi); SCI1 SMR serial init for traced RS232/MAX202 path (H8 pin 66 P95/TXD to MAX202 pin 11; MAX202 pin 12 to H8 pin 67 P96/RXD); cycles=9
109A: 15 FE DA 06 3C MOV:G.B #H'3C, @SCI1_SCR ; SCI1_SCR = H'3C (TIE=0 RIE=0 TE=1 RE=1 CKE1=0 CKE0=0; SCI enables TX,RX, internal clock); disable SCI1 TX interrupt (TIE); disable SCI1 receive and receive-error interrupts (RIE); enable SCI1 transmitter (TE); enable SCI1 receiver (RE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=9
109A: 15 FE DA 06 3C MOV:G.B #H'3C, @SCI1_SCR ; SCI1_SCR = H'3C (TIE=0 RIE=0 TE=1 RE=1 CKE1=0 CKE0=0; SCI enables TX,RX, internal clock); disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; disable SCI1 receive and receive-error interrupts (RIE); enable SCI1 transmitter (TE); enable SCI1 receiver (RE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=9
109F: 15 FE D9 06 07 MOV:G.B #H'07, @SCI1_BRR ; SCI1_BRR = H'07; SCI1 async 8-bit even parity 1 stop BRR N=7 CKS n=0; baud needs --clock-hz; SCI1 BRR serial init for traced RS232/MAX202 path (H8 pin 66 P95/TXD to MAX202 pin 11; MAX202 pin 12 to H8 pin 67 P96/RXD); cycles=9
10A4: 15 FE F0 06 24 MOV:G.B #H'24, @SCI2_SMR ; SCI2_SMR = H'24 (C/A=0 CHR=0 PE=1 O/E=0 STOP=0 CKS1=0 CKS0=0; SCI async, 8-bit, even parity, 1 stop, clock phi); SCI2 SMR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; cycles=9
10A9: 15 FE F2 06 0C MOV:G.B #H'0C, @SCI2_SCR ; SCI2_SCR = H'0C (TIE=0 RIE=0 TE=0 RE=0 CKE1=0 CKE0=0; SCI enables none, internal clock); disable SCI2 TX interrupt (TIE); disable SCI2 receive and receive-error interrupts (RIE); disable SCI2 transmitter (TE); disable SCI2 receiver (RE); SCI2 SCR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; cycles=9
10A9: 15 FE F2 06 0C MOV:G.B #H'0C, @SCI2_SCR ; SCI2_SCR = H'0C (TIE=0 RIE=0 TE=0 RE=0 CKE1=0 CKE0=0; SCI enables none, internal clock); disable SCI2 TX interrupt (TIE); gates TXI when hardware sets TDRE; disable SCI2 receive and receive-error interrupts (RIE); disable SCI2 transmitter (TE); disable SCI2 receiver (RE); SCI2 SCR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; cycles=9
10AE: 15 FE F1 06 07 MOV:G.B #H'07, @SCI2_BRR ; SCI2_BRR = H'07; SCI2 async 8-bit even parity 1 stop BRR N=7 CKS n=0; baud needs --clock-hz; SCI2 BRR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; cycles=9
10B3: 15 FE E8 06 19 MOV:G.B #H'19, @ADCSR ; ADCSR = H'19 (ADF=0 ADIE=0 ADST=0 SCAN=1 CKS=1 CH2=0 CH1=0 CH0=1; A/D halt, scan AN0-AN1, 138-state max, ADI disabled); cycles=9
10B8: 15 FE E9 06 7F MOV:G.B #H'7F, @H'FEE9 ; refs H'FEE9 in register_field; cycles=9
@@ -3041,8 +3046,8 @@ BA6C: 27 FA BEQ loc_BA68 ; repeat SCI1 transmit-empty wait while TDRE=0
BA6E: 15 F8 58 80 MOV:G.B @H'F858, R0 ; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: TX buffer-region references cluster around H'F858-H'F85D; confidence high; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: initial SCI1 TDR send is supported by a read from H'F858; confidence high; refs ram_F858 in on_chip_ram; cycles=7
BA72: 15 FE DB 90 MOV:G.B R0, @SCI1_TDR ; SCI1_TDR; write RS232/SCI byte to SCI1 TDR for transmission; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: initial SCI1 TDR send is supported by a read from H'F858; confidence high; SCI1 TDR write transmits on traced RS232/MAX202 path: H8 pin 66 P95/TXD -> MAX202 pin 11; cycles=7
BA76: 15 F9 C2 06 01 MOV:G.B #H'01, @H'F9C2 ; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: write evidence supports TX index H'F9C2 being initialized to 1; confidence high; refs ram_F9C2 in on_chip_ram; cycles=9
BA7B: 15 FE DC D7 BCLR.B #7, @SCI1_SSR ; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 transmit data register empty flag (TDRE); SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BA7F: 15 FE DA C7 BSET.B #7, @SCI1_SCR ; set TIE (bit 7) of SCI1_SCR; enable SCI1 TX interrupt (TIE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=8
BA7B: 15 FE DC D7 BCLR.B #7, @SCI1_SSR ; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 TDRE after TDR write; TXI can fire again when hardware reasserts TDRE; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BA7F: 15 FE DA C7 BSET.B #7, @SCI1_SCR ; set TIE (bit 7) of SCI1_SCR; enable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=8
BA83: 19 RTS ; cycles=13
vec_sci1_txi_BA84:
@@ -3054,7 +3059,7 @@ BA90: 15 F9 C3 16 TST.B @H'F9C3 ; refs ram_F9C3 in on_chip_ram; cycles=7
BA94: 27 13 BEQ loc_BAA9 ; cycles=3/7 nt/t
BA96: 15 FA A2 D3 BCLR.B #3, @H'FAA2 ; refs ram_FAA2 in on_chip_ram; cycles=9
BA9A: 15 FA A3 13 CLR.B @H'FAA3 ; refs ram_FAA3 in on_chip_ram; cycles=9
BA9E: 15 FE DA D7 BCLR.B #7, @SCI1_SCR ; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=9
BA9E: 15 FE DA D7 BCLR.B #7, @SCI1_SCR ; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=9
BAA2: 15 F9 C0 06 1F MOV:G.B #H'1F, @H'F9C0 ; refs ram_F9C0 in on_chip_ram; cycles=9
BAA7: 20 48 BRA loc_BAF1 ; cycles=8
@@ -3065,11 +3070,11 @@ BAAF: A0 12 EXTU.B R0 ; cycles=3
BAB1: F0 F8 58 80 MOV:G.B @(-H'07A8,R0), R0 ; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR sends SCI1 TDR from indexed H'F858 buffer; confidence high; cycles=6
BAB5: 15 FE DB 90 MOV:G.B R0, @SCI1_TDR ; SCI1_TDR; write RS232/SCI byte to SCI1 TDR for transmission; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR sends SCI1 TDR from indexed H'F858 buffer; confidence high; SCI1 TDR write transmits on traced RS232/MAX202 path: H8 pin 66 P95/TXD -> MAX202 pin 11; cycles=6
BAB9: CF 80 MOV:G.W @R7+, R0 ; cycles=6
BABB: 15 FE DC D7 BCLR.B #7, @SCI1_SSR ; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 transmit data register empty flag (TDRE); SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BABB: 15 FE DC D7 BCLR.B #7, @SCI1_SSR ; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 TDRE after TDR write; TXI can fire again when hardware reasserts TDRE; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BABF: 15 F9 C2 08 ADD:Q.B #1, @H'F9C2 ; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR increments TX index H'F9C2; confidence high; refs ram_F9C2 in on_chip_ram; cycles=8
BAC3: 15 F9 C2 04 06 CMP:G.B #H'06, @H'F9C2 ; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR compares TX index to frame length 6; confidence high; refs ram_F9C2 in on_chip_ram; cycles=6
BAC8: 26 27 BNE loc_BAF1 ; cycles=3/7 nt/t
BACA: 15 FE DA D7 BCLR.B #7, @SCI1_SCR ; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=9
BACA: 15 FE DA D7 BCLR.B #7, @SCI1_SCR ; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); cycles=9
BACE: 15 F7 95 F6 BTST.B #6, @H'F795 ; refs ram_F795 in on_chip_ram; cycles=7
BAD2: 26 14 BNE loc_BAE8 ; cycles=3/7 nt/t
BAD4: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
@@ -3131,13 +3136,13 @@ BB56: 19 RTS ; cycles=12
vec_sci1_eri_BB57:
BB57: 15 FA A4 C7 BSET.B #7, @H'FAA4 ; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; refs ram_FAA4 in on_chip_ram; cycles=8
BB5B: 15 FE DC D5 BCLR.B #5, @SCI1_SSR ; clear ORER (bit 5) of SCI1_SSR; clear SCI1 overrun error flag (ORER); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB5F: 15 FE DC D4 BCLR.B #4, @SCI1_SSR ; clear FER (bit 4) of SCI1_SSR; clear SCI1 framing error flag (FER); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB63: 15 FE DC D3 BCLR.B #3, @SCI1_SSR ; clear PER (bit 3) of SCI1_SSR; clear SCI1 parity error flag (PER); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB5B: 15 FE DC D5 BCLR.B #5, @SCI1_SSR ; clear ORER (bit 5) of SCI1_SSR; clear SCI1 ORER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB5F: 15 FE DC D4 BCLR.B #4, @SCI1_SSR ; clear FER (bit 4) of SCI1_SSR; clear SCI1 FER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB63: 15 FE DC D3 BCLR.B #3, @SCI1_SSR ; clear PER (bit 3) of SCI1_SSR; clear SCI1 PER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
vec_sci1_rxi_BB67:
BB67: 12 03 STM.W {R0,R1}, @-SP ; cycles=12
BB69: 15 FE DC D6 BCLR.B #6, @SCI1_SSR ; clear RDRF (bit 6) of SCI1_SSR; clear SCI1 receive-data-full flag (RDRF); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: ROM clears SCI1 SSR.RDRF before reading SCI1_RDR; preserve this observed ordering even though the manual describes the canonical RDR-read then RDRF-clear sequence; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB69: 15 FE DC D6 BCLR.B #6, @SCI1_SSR ; clear RDRF (bit 6) of SCI1_SSR; clear SCI1 RDRF with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: ROM clears SCI1 SSR.RDRF before reading SCI1_RDR; preserve this observed ordering even though the manual describes the canonical RDR-read then RDRF-clear sequence; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; cycles=8
BB6D: 15 FE DD 80 MOV:G.B @SCI1_RDR, R0 ; read SCI1 received byte from RDR; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 RX ISR reads a byte from SCI1_RDR; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: ROM clears SCI1 SSR.RDRF before reading SCI1_RDR; preserve this observed ordering even though the manual describes the canonical RDR-read then RDRF-clear sequence; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 RDR read receives from traced RS232/MAX202 path: MAX202 pin 12 -> H8 pin 67 P96/RXD; refs SCI1_RDR in register_field; cycles=6
BB71: 15 F9 C1 16 TST.B @H'F9C1 ; refs ram_F9C1 in on_chip_ram; cycles=6
BB75: 26 06 BNE loc_BB7D ; cycles=3/8 nt/t
@@ -3495,20 +3500,20 @@ loc_BEE8:
BEE8: 19 RTS ; cycles=12
vec_frt1_ocia_BEEA:
BEEA: 15 FE 91 D5 BCLR.B #5, @FRT1_TCSR ; clear OCFA (bit 5) of FRT1_TCSR; cycles=9
BEEE: 15 F9 C0 16 TST.B @H'F9C0 ; refs ram_F9C0 in on_chip_ram; cycles=7
BEEA: 15 FE 91 D5 BCLR.B #5, @FRT1_TCSR ; clear OCFA (bit 5) of FRT1_TCSR; candidate/evidence-supported RAM role post_tx_report_delay at H'F9C0; evidence: candidate periodic tick ISR at H'BEEA for FRT1 OCIA vector H'0062 clears OCFA; confidence candidate/evidence-supported; candidate/evidence-supported RAM role secondary_tx_report_delay at H'F9C1; evidence: candidate periodic tick ISR at H'BEEA for FRT1 OCIA vector H'0062 clears OCFA; confidence candidate/evidence-supported; candidate/evidence-supported RAM role periodic_report_countdown at H'F9C6; evidence: candidate periodic tick ISR at H'BEEA for FRT1 OCIA vector H'0062 clears OCFA; confidence candidate/evidence-supported; cycles=9
BEEE: 15 F9 C0 16 TST.B @H'F9C0 ; candidate/evidence-supported RAM role post_tx_report_delay at H'F9C0; evidence: candidate post-TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C0 in on_chip_ram; cycles=7
BEF2: 27 04 BEQ loc_BEF8 ; cycles=3/7 nt/t
BEF4: 15 F9 C0 0C ADD:Q.B #-1, @H'F9C0 ; refs ram_F9C0 in on_chip_ram; cycles=9
BEF4: 15 F9 C0 0C ADD:Q.B #-1, @H'F9C0 ; candidate/evidence-supported RAM role post_tx_report_delay at H'F9C0; evidence: candidate post-TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C0 in on_chip_ram; cycles=9
loc_BEF8:
BEF8: 15 F9 C1 16 TST.B @H'F9C1 ; refs ram_F9C1 in on_chip_ram; cycles=7
BEF8: 15 F9 C1 16 TST.B @H'F9C1 ; candidate/evidence-supported RAM role secondary_tx_report_delay at H'F9C1; evidence: candidate secondary TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C1 in on_chip_ram; cycles=7
BEFC: 27 04 BEQ loc_BF02 ; cycles=3/7 nt/t
BEFE: 15 F9 C1 0C ADD:Q.B #-1, @H'F9C1 ; refs ram_F9C1 in on_chip_ram; cycles=9
BEFE: 15 F9 C1 0C ADD:Q.B #-1, @H'F9C1 ; candidate/evidence-supported RAM role secondary_tx_report_delay at H'F9C1; evidence: candidate secondary TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C1 in on_chip_ram; cycles=9
loc_BF02:
BF02: 1D F9 C6 16 TST.W @H'F9C6 ; refs ram_F9C6 in on_chip_ram; cycles=7
BF02: 1D F9 C6 16 TST.W @H'F9C6 ; candidate/evidence-supported RAM role periodic_report_countdown at H'F9C6; evidence: candidate periodic report countdown is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C6 in on_chip_ram; cycles=7
BF06: 27 04 BEQ loc_BF0C ; cycles=3/7 nt/t
BF08: 1D F9 C6 0C ADD:Q.W #-1, @H'F9C6 ; refs ram_F9C6 in on_chip_ram; cycles=9
BF08: 1D F9 C6 0C ADD:Q.W #-1, @H'F9C6 ; candidate/evidence-supported RAM role periodic_report_countdown at H'F9C6; evidence: candidate periodic report countdown is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C6 in on_chip_ram; cycles=9
loc_BF0C:
BF0C: 15 F6 F6 F7 BTST.B #7, @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=7

File diff suppressed because it is too large Load Diff

View File

@@ -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 */
@@ -485,10 +487,10 @@ void vec_reset_1000(void)
PWM3_TCR = (uint8_t)(0x3B); /* 108B; MOV:G.B #H'3B, @PWM3_TCR; PWM3_TCR = H'3B (OE=0 OS=0 CKS2=0 CKS1=1 CKS0=1); refs PWM3_TCR; cycles=9 */
PWM3_DTR = (uint8_t)(0x7D); /* 1090; MOV:G.B #H'7D, @PWM3_DTR; PWM3_DTR = H'7D; refs PWM3_DTR; cycles=9 */
SCI1_SMR = (uint8_t)(0x24); /* 1095; MOV:G.B #H'24, @SCI1_SMR; SCI1_SMR = H'24 (C/A=0 CHR=0 PE=1 O/E=0 STOP=0 CKS1=0 CKS0=0; SCI async, 8-bit, even parity, 1 stop, clock phi); SCI1 SMR serial init for traced RS232/MAX202 path (H8 pin 66 P95/TXD to MAX202 pin 11; MAX202 pin 12 to H8 pin 67 P96/RXD); refs SCI1_SMR; cycles=9 */
SCI1_SCR = (uint8_t)(0x3C); /* 109A; MOV:G.B #H'3C, @SCI1_SCR; SCI1_SCR = H'3C (TIE=0 RIE=0 TE=1 RE=1 CKE1=0 CKE0=0; SCI enables TX,RX, internal clock); disable SCI1 TX interrupt (TIE); disable SCI1 receive and receive-error interrupts (RIE); enable SCI1 transmitter (TE); enable SCI1 receiver (RE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=9 */
SCI1_SCR = (uint8_t)(0x3C); /* 109A; MOV:G.B #H'3C, @SCI1_SCR; SCI1_SCR = H'3C (TIE=0 RIE=0 TE=1 RE=1 CKE1=0 CKE0=0; SCI enables TX,RX, internal clock); disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; disable SCI1 receive and receive-error interrupts (RIE); enable SCI1 transmitter (TE); enable SCI1 receiver (RE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=9 */
SCI1_BRR = (uint8_t)(0x07); /* 109F; MOV:G.B #H'07, @SCI1_BRR; SCI1_BRR = H'07; SCI1 async 8-bit even parity 1 stop BRR N=7 CKS n=0; baud needs --clock-hz; SCI1 BRR serial init for traced RS232/MAX202 path (H8 pin 66 P95/TXD to MAX202 pin 11; MAX202 pin 12 to H8 pin 67 P96/RXD); refs SCI1_BRR; cycles=9 */
SCI2_SMR = (uint8_t)(0x24); /* 10A4; MOV:G.B #H'24, @SCI2_SMR; SCI2_SMR = H'24 (C/A=0 CHR=0 PE=1 O/E=0 STOP=0 CKS1=0 CKS0=0; SCI async, 8-bit, even parity, 1 stop, clock phi); SCI2 SMR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; refs SCI2_SMR; cycles=9 */
SCI2_SCR = (uint8_t)(0x0C); /* 10A9; MOV:G.B #H'0C, @SCI2_SCR; SCI2_SCR = H'0C (TIE=0 RIE=0 TE=0 RE=0 CKE1=0 CKE0=0; SCI enables none, internal clock); disable SCI2 TX interrupt (TIE); disable SCI2 receive and receive-error interrupts (RIE); disable SCI2 transmitter (TE); disable SCI2 receiver (RE); SCI2 SCR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; refs SCI2_SCR; cycles=9 */
SCI2_SCR = (uint8_t)(0x0C); /* 10A9; MOV:G.B #H'0C, @SCI2_SCR; SCI2_SCR = H'0C (TIE=0 RIE=0 TE=0 RE=0 CKE1=0 CKE0=0; SCI enables none, internal clock); disable SCI2 TX interrupt (TIE); gates TXI when hardware sets TDRE; disable SCI2 receive and receive-error interrupts (RIE); disable SCI2 transmitter (TE); disable SCI2 receiver (RE); SCI2 SCR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; refs SCI2_SCR; cycles=9 */
SCI2_BRR = (uint8_t)(0x07); /* 10AE; MOV:G.B #H'07, @SCI2_BRR; SCI2_BRR = H'07; SCI2 async 8-bit even parity 1 stop BRR N=7 CKS n=0; baud needs --clock-hz; SCI2 BRR write; not the traced MAX202 path; P9SCI2E=0 disables SCI2 pins P92/P93/P94, while the board trace is SCI1 P95/P96; refs SCI2_BRR; cycles=9 */
ADCSR = (uint8_t)(0x19); /* 10B3; MOV:G.B #H'19, @ADCSR; ADCSR = H'19 (ADF=0 ADIE=0 ADST=0 SCAN=1 CKS=1 CH2=0 CH1=0 CH0=1; A/D halt, scan AN0-AN1, 138-state max, ADI disabled); refs ADCSR; cycles=9 */
MEM8[0xFEE9] = (uint8_t)(0x7F); /* 10B8; MOV:G.B #H'7F, @H'FEE9; cycles=9 */
@@ -2854,8 +2856,8 @@ void loc_BA26(void)
R0 = (uint8_t)(MEM8[0xF858]); /* BA6E; MOV:G.B @H'F858, R0; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: TX buffer-region references cluster around H'F858-H'F85D; confidence high; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: initial SCI1 TDR send is supported by a read from H'F858; confidence high; refs ram_F858; cycles=7 */
SCI1_TDR = (uint8_t)(R0); /* BA72; MOV:G.B R0, @SCI1_TDR; SCI1_TDR; write RS232/SCI byte to SCI1 TDR for transmission; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: initial SCI1 TDR send is supported by a read from H'F858; confidence high; SCI1 TDR write transmits on traced RS232/MAX202 path: H8 pin 66 P95/TXD -> MAX202 pin 11; refs SCI1_TDR; cycles=7 */
MEM8[0xF9C2] = (uint8_t)(0x01); /* BA76; MOV:G.B #H'01, @H'F9C2; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: write evidence supports TX index H'F9C2 being initialized to 1; confidence high; refs ram_F9C2; cycles=9 */
SCI1_SSR &= ~BIT(7); /* BA7B; BCLR.B #7, @SCI1_SSR; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 transmit data register empty flag (TDRE); SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SCR |= BIT(7); /* BA7F; BSET.B #7, @SCI1_SCR; set TIE (bit 7) of SCI1_SCR; enable SCI1 TX interrupt (TIE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=8 */
SCI1_SSR &= ~BIT(7); /* BA7B; BCLR.B #7, @SCI1_SSR; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 TDRE after TDR write; TXI can fire again when hardware reasserts TDRE; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SCR |= BIT(7); /* BA7F; BSET.B #7, @SCI1_SCR; set TIE (bit 7) of SCI1_SCR; enable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=8 */
return; /* BA83; RTS; cycles=13 */
}
@@ -2870,7 +2872,7 @@ void vec_sci1_txi_BA84(void)
if (Z) goto loc_BAA9; /* BA94; BEQ loc_BAA9; cycles=3/7 nt/t */
MEM8[0xFAA2] &= ~BIT(3); /* BA96; BCLR.B #3, @H'FAA2; refs ram_FAA2; cycles=9 */
MEM8[0xFAA3] = 0; /* BA9A; CLR.B @H'FAA3; refs ram_FAA3; cycles=9 */
SCI1_SCR &= ~BIT(7); /* BA9E; BCLR.B #7, @SCI1_SCR; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=9 */
SCI1_SCR &= ~BIT(7); /* BA9E; BCLR.B #7, @SCI1_SCR; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=9 */
MEM8[0xF9C0] = (uint8_t)(0x1F); /* BAA2; MOV:G.B #H'1F, @H'F9C0; refs ram_F9C0; cycles=9 */
goto loc_BAF1; /* BAA7; BRA loc_BAF1; cycles=8 */
loc_BAA9:
@@ -2880,11 +2882,11 @@ loc_BAA9:
R0 = (uint8_t)(MEM8[R0 - 0x07A8]); /* BAB1; MOV:G.B @(-H'07A8,R0), R0; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR sends SCI1 TDR from indexed H'F858 buffer; confidence high; cycles=6 */
SCI1_TDR = (uint8_t)(R0); /* BAB5; MOV:G.B R0, @SCI1_TDR; SCI1_TDR; write RS232/SCI byte to SCI1 TDR for transmission; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR sends SCI1 TDR from indexed H'F858 buffer; confidence high; SCI1 TDR write transmits on traced RS232/MAX202 path: H8 pin 66 P95/TXD -> MAX202 pin 11; refs SCI1_TDR; cycles=6 */
R0 = (uint16_t)(MEM16[R7++]); /* BAB9; MOV:G.W @R7+, R0; cycles=6 */
SCI1_SSR &= ~BIT(7); /* BABB; BCLR.B #7, @SCI1_SSR; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 transmit data register empty flag (TDRE); SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(7); /* BABB; BCLR.B #7, @SCI1_SSR; clear TDRE (bit 7) of SCI1_SSR; clear SCI1 TDRE after TDR write; TXI can fire again when hardware reasserts TDRE; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
MEM8[0xF9C2] += (uint8_t)(1); /* BABF; ADD:Q.B #1, @H'F9C2; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR increments TX index H'F9C2; confidence high; refs ram_F9C2; cycles=8 */
set_flags_cmp8(MEM8[0xF9C2], 0x06); /* BAC3; CMP:G.B #H'06, @H'F9C2; candidate/evidence-supported SCI1 6-byte TX frame; H'F858-H'F85D, checksum H'F85D seeded by H'005A; evidence: candidate TX ISR compares TX index to frame length 6; confidence high; refs ram_F9C2; cycles=6 */
if (!Z) goto loc_BAF1; /* BAC8; BNE loc_BAF1; cycles=3/7 nt/t */
SCI1_SCR &= ~BIT(7); /* BACA; BCLR.B #7, @SCI1_SCR; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=9 */
SCI1_SCR &= ~BIT(7); /* BACA; BCLR.B #7, @SCI1_SCR; clear TIE (bit 7) of SCI1_SCR; disable SCI1 TX interrupt (TIE); gates TXI when hardware sets TDRE; SCI1 SCR write TE=1 RE=1; TE/RE select the traced RS232/MAX202 pins (P95/TXD pin 66 to MAX202 pin 11, P96/RXD pin 67 to MAX202 pin 12); refs SCI1_SCR; cycles=9 */
set_flags_btst(MEM8[0xF795], 6); /* BACE; BTST.B #6, @H'F795; refs ram_F795; cycles=7 */
if (!Z) goto loc_BAE8; /* BAD2; BNE loc_BAE8; cycles=3/7 nt/t */
set_flags_btst(MEM8[0xF791], 7); /* BAD4; BTST.B #7, @H'F791; refs ram_F791; cycles=7 */
@@ -2945,16 +2947,16 @@ void vec_sci1_eri_BB57(void)
{
/* vector sources: sci1_eri */
MEM8[0xFAA4] |= BIT(7); /* BB57; BSET.B #7, @H'FAA4; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; refs ram_FAA4; cycles=8 */
SCI1_SSR &= ~BIT(5); /* BB5B; BCLR.B #5, @SCI1_SSR; clear ORER (bit 5) of SCI1_SSR; clear SCI1 overrun error flag (ORER); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(4); /* BB5F; BCLR.B #4, @SCI1_SSR; clear FER (bit 4) of SCI1_SSR; clear SCI1 framing error flag (FER); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(3); /* BB63; BCLR.B #3, @SCI1_SSR; clear PER (bit 3) of SCI1_SSR; clear SCI1 parity error flag (PER); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(5); /* BB5B; BCLR.B #5, @SCI1_SSR; clear ORER (bit 5) of SCI1_SSR; clear SCI1 ORER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(4); /* BB5F; BCLR.B #4, @SCI1_SSR; clear FER (bit 4) of SCI1_SSR; clear SCI1 FER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(3); /* BB63; BCLR.B #3, @SCI1_SSR; clear PER (bit 3) of SCI1_SSR; clear SCI1 PER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
}
void vec_sci1_rxi_BB67(void)
{
/* vector sources: sci1_rxi */
push_registers(R0, R1); /* BB67; STM.W {R0,R1}, @-SP; cycles=12 */
SCI1_SSR &= ~BIT(6); /* BB69; BCLR.B #6, @SCI1_SSR; clear RDRF (bit 6) of SCI1_SSR; clear SCI1 receive-data-full flag (RDRF); candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: ROM clears SCI1 SSR.RDRF before reading SCI1_RDR; preserve this observed ordering even though the manual describes the canonical RDR-read then RDRF-clear sequence; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
SCI1_SSR &= ~BIT(6); /* BB69; BCLR.B #6, @SCI1_SSR; clear RDRF (bit 6) of SCI1_SSR; clear SCI1 RDRF with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: ROM clears SCI1 SSR.RDRF before reading SCI1_RDR; preserve this observed ordering even though the manual describes the canonical RDR-read then RDRF-clear sequence; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 SSR status for traced RS232/MAX202 path; TDRE/RDRF/error flags gate TDR/RDR use; refs SCI1_SSR; cycles=8 */
R0 = (uint8_t)(SCI1_RDR); /* BB6D; MOV:G.B @SCI1_RDR, R0; read SCI1 received byte from RDR; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 RX ISR reads a byte from SCI1_RDR; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: ROM clears SCI1 SSR.RDRF before reading SCI1_RDR; preserve this observed ordering even though the manual describes the canonical RDR-read then RDRF-clear sequence; confidence high; candidate/evidence-supported SCI1 6-byte RX frame; capture H'F868-H'F86D, validate H'F860-H'F865, checksum H'F865 seeded by H'005A; evidence: SCI1 ERI latches FAA4.bit7, clears ORER/FER/PER, then falls through into the same RXI byte-capture path; confidence high; SCI1 RDR read receives from traced RS232/MAX202 path: MAX202 pin 12 -> H8 pin 67 P96/RXD; refs SCI1_RDR; cycles=6 */
set_flags_tst8(MEM8[0xF9C1]); /* BB71; TST.B @H'F9C1; refs ram_F9C1; cycles=6 */
if (!Z) goto loc_BB7D; /* BB75; BNE loc_BB7D; cycles=3/8 nt/t */
@@ -3284,18 +3286,18 @@ loc_BEE8:
void vec_frt1_ocia_BEEA(void)
{
/* vector sources: frt1_ocia */
FRT1_TCSR &= ~BIT(5); /* BEEA; BCLR.B #5, @FRT1_TCSR; clear OCFA (bit 5) of FRT1_TCSR; refs FRT1_TCSR; cycles=9 */
set_flags_tst8(MEM8[0xF9C0]); /* BEEE; TST.B @H'F9C0; refs ram_F9C0; cycles=7 */
FRT1_TCSR &= ~BIT(5); /* BEEA; BCLR.B #5, @FRT1_TCSR; clear OCFA (bit 5) of FRT1_TCSR; candidate/evidence-supported RAM role post_tx_report_delay at H'F9C0; evidence: candidate periodic tick ISR at H'BEEA for FRT1 OCIA vector H'0062 clears OCFA; confidence candidate/evidence-supported; candidate/evidence-supported RAM role secondary_tx_report_delay at H'F9C1; evidence: candidate periodic tick ISR at H'BEEA for FRT1 OCIA vector H'0062 clears OCFA; confidence candidate/evidence-supported; candidate/evidence-supported RAM role periodic_report_countdown at H'F9C6; evidence: candidate periodic tick ISR at H'BEEA for FRT1 OCIA vector H'0062 clears OCFA; confidence candidate/evidence-supported; refs FRT1_TCSR; cycles=9 */
set_flags_tst8(MEM8[0xF9C0]); /* BEEE; TST.B @H'F9C0; candidate/evidence-supported RAM role post_tx_report_delay at H'F9C0; evidence: candidate post-TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C0; cycles=7 */
if (!Z) { /* BEF2; BEQ loc_BEF8; cycles=3/7 nt/t */
MEM8[0xF9C0] += (uint8_t)(-1); /* BEF4; ADD:Q.B #-1, @H'F9C0; refs ram_F9C0; cycles=9 */
MEM8[0xF9C0] += (uint8_t)(-1); /* BEF4; ADD:Q.B #-1, @H'F9C0; candidate/evidence-supported RAM role post_tx_report_delay at H'F9C0; evidence: candidate post-TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C0; cycles=9 */
}
set_flags_tst8(MEM8[0xF9C1]); /* BEF8; TST.B @H'F9C1; refs ram_F9C1; cycles=7 */
set_flags_tst8(MEM8[0xF9C1]); /* BEF8; TST.B @H'F9C1; candidate/evidence-supported RAM role secondary_tx_report_delay at H'F9C1; evidence: candidate secondary TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C1; cycles=7 */
if (!Z) { /* BEFC; BEQ loc_BF02; cycles=3/7 nt/t */
MEM8[0xF9C1] += (uint8_t)(-1); /* BEFE; ADD:Q.B #-1, @H'F9C1; refs ram_F9C1; cycles=9 */
MEM8[0xF9C1] += (uint8_t)(-1); /* BEFE; ADD:Q.B #-1, @H'F9C1; candidate/evidence-supported RAM role secondary_tx_report_delay at H'F9C1; evidence: candidate secondary TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C1; cycles=9 */
}
set_flags_tst16(MEM16[0xF9C6]); /* BF02; TST.W @H'F9C6; refs ram_F9C6; cycles=7 */
set_flags_tst16(MEM16[0xF9C6]); /* BF02; TST.W @H'F9C6; candidate/evidence-supported RAM role periodic_report_countdown at H'F9C6; evidence: candidate periodic report countdown is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C6; cycles=7 */
if (!Z) { /* BF06; BEQ loc_BF0C; cycles=3/7 nt/t */
MEM16[0xF9C6] += (uint16_t)(-1); /* BF08; ADD:Q.W #-1, @H'F9C6; refs ram_F9C6; cycles=9 */
MEM16[0xF9C6] += (uint16_t)(-1); /* BF08; ADD:Q.W #-1, @H'F9C6; candidate/evidence-supported RAM role periodic_report_countdown at H'F9C6; evidence: candidate periodic report countdown is decremented by the FRT1 OCIA periodic tick ISR; confidence candidate/evidence-supported; refs ram_F9C6; cycles=9 */
}
set_flags_btst(MEM8[0xF6F6], 7); /* BF0C; BTST.B #7, @H'F6F6; refs ram_F6F6; cycles=7 */
if (Z) goto loc_BF22; /* BF10; BEQ loc_BF22; cycles=3/7 nt/t */
@@ -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 */
}

View File

@@ -0,0 +1,256 @@
{
"calls": [
{
"address": 5622,
"address_hex": "H'15F6",
"assessment": "Direct static enqueue source for 0x0081, not 0x0007.",
"can_directly_enqueue_report_index": false,
"dataflow_block": 5609,
"function_label": "loc_15E0",
"function_start": 5600,
"function_start_hex": "H'15E0",
"instruction": "BSR loc_3E54",
"r2": {
"bit7": true,
"classification": "constant",
"evidence": {
"address": 5617,
"address_hex": "H'15F1",
"instruction": "MOV:E.B #H'80, R2",
"mnemonic": "MOV:E.B",
"operands": "#H'80, R2"
},
"reason": "immediate load",
"register": "R2",
"value": 128,
"value_hex": "0x80"
},
"r3": {
"classification": "constant",
"evidence": {
"address": 5619,
"address_hex": "H'15F3",
"instruction": "MOV:I.W #H'0081, R3",
"mnemonic": "MOV:I.W",
"operands": "#H'0081, R3"
},
"reason": "immediate load",
"register": "R3",
"value": 129,
"value_hex": "0x0081"
},
"table_hints": [],
"target": 15956,
"target_hex": "H'3E54",
"window_instruction_count": 4,
"window_start": 5609,
"window_start_hex": "H'15E9"
},
{
"address": 6673,
"address_hex": "H'1A11",
"assessment": "No static 0x0007 constant here; R3 is dynamic/table-derived.",
"can_directly_enqueue_report_index": false,
"dataflow_block": 6665,
"function_label": "loc_19DB",
"function_start": 6619,
"function_start_hex": "H'19DB",
"instruction": "BSR loc_3E54",
"r2": {
"bit7": true,
"classification": "constant",
"evidence": {
"address": 6669,
"address_hex": "H'1A0D",
"instruction": "MOV:E.B #H'80, R2",
"mnemonic": "MOV:E.B",
"operands": "#H'80, R2"
},
"reason": "immediate load",
"register": "R2",
"value": 128,
"value_hex": "0x80"
},
"r3": {
"classification": "dynamic/table-derived",
"evidence": {
"address": 6671,
"address_hex": "H'1A0F",
"instruction": "MOV:G.W R5, R3",
"mnemonic": "MOV:G.W",
"operands": "R5, R3"
},
"reason": "copied from unresolved R5",
"register": "R3",
"value": null,
"value_hex": null
},
"table_hints": [
{
"address": 6665,
"address_hex": "H'1A09",
"instruction": "MOV:G.W R1, @(-H'1800,R3)",
"operand": "@(-H'1800,R3)",
"table": "current_value_table_candidate"
}
],
"target": 15956,
"target_hex": "H'3E54",
"window_instruction_count": 3,
"window_start": 6665,
"window_start_hex": "H'1A09"
},
{
"address": 6777,
"address_hex": "H'1A79",
"assessment": "No static 0x0007 constant here; R3 is dynamic/table-derived.",
"can_directly_enqueue_report_index": false,
"dataflow_block": 6769,
"function_label": "loc_1A35",
"function_start": 6709,
"function_start_hex": "H'1A35",
"instruction": "BSR loc_3E54",
"r2": {
"bit7": true,
"classification": "constant",
"evidence": {
"address": 6773,
"address_hex": "H'1A75",
"instruction": "MOV:E.B #H'80, R2",
"mnemonic": "MOV:E.B",
"operands": "#H'80, R2"
},
"reason": "immediate load",
"register": "R2",
"value": 128,
"value_hex": "0x80"
},
"r3": {
"classification": "dynamic/table-derived",
"evidence": {
"address": 6775,
"address_hex": "H'1A77",
"instruction": "MOV:G.W R5, R3",
"mnemonic": "MOV:G.W",
"operands": "R5, R3"
},
"reason": "copied from unresolved R5",
"register": "R3",
"value": null,
"value_hex": null
},
"table_hints": [
{
"address": 6769,
"address_hex": "H'1A71",
"instruction": "MOV:G.W R0, @(-H'1800,R3)",
"operand": "@(-H'1800,R3)",
"table": "current_value_table_candidate"
}
],
"target": 15956,
"target_hex": "H'3E54",
"window_instruction_count": 3,
"window_start": 6769,
"window_start_hex": "H'1A71"
},
{
"address": 9896,
"address_hex": "H'26A8",
"assessment": "No static 0x0007 constant here; R3 is unknown.",
"can_directly_enqueue_report_index": false,
"dataflow_block": 9896,
"function_label": "loc_2650",
"function_start": 9808,
"function_start_hex": "H'2650",
"instruction": "BSR loc_3E54",
"r2": {
"bit7": null,
"classification": "unknown",
"evidence": null,
"reason": "decompiler dataflow: block_entry",
"register": "R2",
"value": null,
"value_hex": null
},
"r3": {
"classification": "unknown",
"evidence": null,
"reason": "decompiler dataflow: block_entry",
"register": "R3",
"value": null,
"value_hex": null
},
"table_hints": [],
"target": 15956,
"target_hex": "H'3E54",
"window_instruction_count": 0,
"window_start": 9896,
"window_start_hex": "H'26A8"
},
{
"address": 18726,
"address_hex": "H'4926",
"assessment": "Direct static enqueue source for 0x00f6, not 0x0007.",
"can_directly_enqueue_report_index": false,
"dataflow_block": 18709,
"function_label": "loc_48FA",
"function_start": 18682,
"function_start_hex": "H'48FA",
"instruction": "BSR loc_3E54",
"r2": {
"bit7": true,
"classification": "constant",
"evidence": {
"address": 18721,
"address_hex": "H'4921",
"instruction": "MOV:E.B #H'80, R2",
"mnemonic": "MOV:E.B",
"operands": "#H'80, R2"
},
"reason": "immediate load",
"register": "R2",
"value": 128,
"value_hex": "0x80"
},
"r3": {
"classification": "constant",
"evidence": {
"address": 18723,
"address_hex": "H'4923",
"instruction": "MOV:I.W #H'00F6, R3",
"mnemonic": "MOV:I.W",
"operands": "#H'00F6, R3"
},
"reason": "immediate load",
"register": "R3",
"value": 246,
"value_hex": "0x00F6"
},
"table_hints": [],
"target": 15956,
"target_hex": "H'3E54",
"window_instruction_count": 5,
"window_start": 18709,
"window_start_hex": "H'4915"
}
],
"caveats": [
"This is a bounded local static trace, not an emulator run.",
"R3 values classified as dynamic/table-derived may still become 0x0007 at runtime.",
"Indirect dispatch, table handlers, interrupt interleavings, or callers absent from the JSON may still enqueue 0x0007.",
"The generic queue-to-TX path only emits queued entries; this tracer looks for direct report-index sources at loc_3E54 callers."
],
"kind": "report_source_trace",
"queue_function": 15956,
"queue_function_hex": "H'3E54",
"report_index_of_interest": 7,
"report_index_of_interest_hex": "0x0007",
"summary": {
"conclusion": "No direct loc_3E54 caller in this JSON statically loads report index 0x0007. 0x0007 remains an observed runtime/capture value unless another indirect or table-dispatch path is proven.",
"direct_call_count": 5,
"direct_static_hit_count": 0,
"dynamic_or_unknown_candidate_count": 3,
"status": "not_statically_proven"
}
}

View File

@@ -0,0 +1,32 @@
H8/536 loc_3E54 Report Source Trace
Queue function: H'3E54
Report index of interest: 0x0007
Direct callers: 5
Direct static 0x0007 hits: 0
Dynamic/unknown candidates: 3
Conclusion: No direct loc_3E54 caller in this JSON statically loads report index 0x0007. 0x0007 remains an observed runtime/capture value unless another indirect or table-dispatch path is proven.
Call sites:
- H'15F6 in loc_15E0: R2.bit7=set, R3=0x0081 (constant); direct_0x0007=False
R2 evidence: H'15F1 MOV:E.B #H'80, R2
R3 evidence: H'15F3 MOV:I.W #H'0081, R3
- H'1A11 in loc_19DB: R2.bit7=set, R3=<dynamic> (dynamic/table-derived); direct_0x0007=False
R2 evidence: H'1A0D MOV:E.B #H'80, R2
R3 evidence: H'1A0F MOV:G.W R5, R3
table/context hints: H'1A09 current_value_table_candidate via @(-H'1800,R3)
- H'1A79 in loc_1A35: R2.bit7=set, R3=<dynamic> (dynamic/table-derived); direct_0x0007=False
R2 evidence: H'1A75 MOV:E.B #H'80, R2
R3 evidence: H'1A77 MOV:G.W R5, R3
table/context hints: H'1A71 current_value_table_candidate via @(-H'1800,R3)
- H'26A8 in loc_2650: R2.bit7=unknown, R3=<dynamic> (unknown); direct_0x0007=False
- H'4926 in loc_48FA: R2.bit7=set, R3=0x00F6 (constant); direct_0x0007=False
R2 evidence: H'4921 MOV:E.B #H'80, R2
R3 evidence: H'4923 MOV:I.W #H'00F6, R3
Caveats:
- This is a bounded local static trace, not an emulator run.
- R3 values classified as dynamic/table-derived may still become 0x0007 at runtime.
- Indirect dispatch, table handlers, interrupt interleavings, or callers absent from the JSON may still enqueue 0x0007.
- The generic queue-to-TX path only emits queued entries; this tracer looks for direct report-index sources at loc_3E54 callers.

2297
build/rom_serial_gate.json Normal file

File diff suppressed because it is too large Load Diff

145
build/rom_serial_gate.txt Normal file
View File

@@ -0,0 +1,145 @@
H8/536 Serial Gate/Queue State-Machine Reconstruction
Summary: autonomous serial TX/report queue gate
Confidence: high
Evidence:
- loc_3FD3 gate into loc_BAF2: present
Requires FAA2 == 0, allows the FAA5.bit7 path only when F9C3 == 0, then requires F9C0 == 0 before BSR loc_BAF2.
- H'3FD3: TST.B @H'FAA2
- H'3FD7: BNE loc_3FEE
- H'3FD9: BTST.B #7, @H'FAA5
- H'3FDD: BEQ loc_3FE5
- H'3FDF: TST.B @H'F9C3
- H'3FE3: BNE loc_3FEE
- H'3FE5: TST.B @H'F9C0
- H'3FE9: BNE loc_3FEE
- H'3FEB: BSR loc_BAF2
- loc_BAF2 queue send gate: present
F9B5 is compared against F9B0; inequality enters the send path, reads a queued word via the F9B5-derived index around F870, stages F850-F854, and calls BA26 at BB43.
- H'BAF2: MOV:G.B @H'F9B5, R1
- H'BAF8: CMP:G.B @H'F9B0, R1
- H'BAFC: BNE loc_BB00
- H'BAFE: BRA loc_BB56
- H'BB00: BSET.B #3, @H'FAA2
- H'BB08: MOV:G.W @(-H'0790,R0), R0
- H'BB1C: MOV:G.B R1, @H'F850
- H'BB20: MOV:G.B R5, @H'F852
- H'BB2B: MOV:G.B R5, @H'F851
- H'BB39: MOV:G.B R4, @H'F854
- H'BB3F: MOV:G.B R4, @H'F853
- H'BB43: BSR loc_BA26
- H'BB46: MOV:G.W #H'01F4, @H'F9C6
- H'BB4C: MOV:G.B #H'14, @H'F9C8
- H'BB51: MOV:G.B #H'80, @H'FAA3
- resend gate/path: present
BE9E masks FAA5 with FAA3, waits for F9C6/F9C8 timeout gates, then if FAA3.bit7 remains set clears F9C3 and calls BA26 from BED5.
- H'BE9E: MOV:G.B @H'FAA5, R0
- H'BEA5: AND.B @H'FAA3, R0
- H'BEA9: MOV:G.B R0, @H'FAA3
- H'BEAF: CLR.B @H'FAA2
- H'BEB5: TST.W @H'F9C6
- H'BEBB: TST.B @H'F9C8
- H'BEC5: MOV:G.W #H'01F4, @H'F9C6
- H'BECB: BTST.B #7, @H'FAA3
- H'BED1: CLR.B @H'F9C3
- H'BED5: BSR loc_BA26
- RX/session maintenance: present
F9C5 timeout maintenance clears F9B5/F9B0 and FAA5.bit7; RX command processing uses FAA2 as an in-session latch and paths advance F9B5/F9B0 or clear FAA3/FAA2.
- H'3FEF: TST.B @H'F9C5
- H'3FF5: CLR.B @H'F9B5
- H'3FF9: CLR.B @H'F9B0
- H'3FFD: BCLR.B #7, @H'FAA5
- H'4007: BSET.B #7, @H'FAA5
- H'BBCB: CLR.B @H'F9C3
- H'BC0F: TST.B @H'FAA2
- H'BC15: BSET.B #7, @H'FAA2
- H'BC33: CLR.B @H'FAA2
- H'BC5C: BCLR.B #3, @H'FAA2
- H'BC63: CLR.B @H'FAA3
- H'BCD0: BCLR.B #7, @H'FAA2
- H'BCFD: BCLR.B #7, @H'FAA2
- H'BD04: BCLR.B #7, @H'FAA2
- H'BD6D: ADD:Q.B #1, @H'F9B5
- H'BD71: BCLR.B #7, @H'F9B5
- H'BD75: CLR.B @H'FAA3
- H'BD79: CLR.B @H'FAA2
- H'BDC8: ADD:Q.B #1, @H'F9B5
- H'BDCC: BCLR.B #7, @H'F9B5
- H'BDD0: CLR.B @H'FAA3
- H'BDD4: CLR.B @H'FAA2
- H'BDF3: ADD:Q.B #1, @H'F9B5
- H'BDF7: BCLR.B #7, @H'F9B5
- H'BDFB: CLR.B @H'FAA3
- H'BDFF: CLR.B @H'FAA2
- loc_4046 idle heartbeat/report gate: present
F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 can enqueue H'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
- H'4050: BEQ loc_4059
- H'4052: TST.B @H'F9C3
- H'4056: BEQ loc_4059
- H'4058: RTS
- H'4059: MOV:G.B @H'F9B0, R2
- H'405F: CMP:G.B @H'F9B5, R2
- H'4063: BNE loc_4074
- H'4067: MOV:G.W #H'00, @(-H'0790,R2)
- H'406C: ADD:Q.B #1, @H'F9B0
- H'4070: BCLR.B #7, @H'F9B0
- H'40E0: MOV:G.B #H'14, @H'F9C4
- H'BA31: MOV:G.B #H'07, @H'F9C4
- H'BF23: BCLR.B #5, @FRT2_TCSR
- H'BF27: TST.B @H'F9C4
- H'BF2D: ADD:Q.B #-1, @H'F9C4
Candidate timer roles:
- H'F9C4: candidate idle heartbeat/report gate countdown
Timer: FRT2 OCIA, H'BF23, OCRA=H'7A12, observed period ~= 700ms
- FRT1 OCIA periodic tick countdowns: present
Static evidence links vector H'0062 to the FRT1 OCIA handler at H'BEEA; the handler clears FRT1_TCSR.OCFA and conditionally decrements H'F9C0, H'F9C1, and H'F9C6.
- H'BEEA: BCLR.B #5, @FRT1_TCSR
- H'BEEE: TST.B @H'F9C0
- H'BEF4: ADD:Q.B #-1, @H'F9C0
- H'BEF8: TST.B @H'F9C1
- H'BEFE: ADD:Q.B #-1, @H'F9C1
- H'BF02: TST.W @H'F9C6
- H'BF08: ADD:Q.W #-1, @H'F9C6
Candidate timer roles:
- H'F9C0: candidate post-TX/report delay countdown
- H'F9C1: candidate secondary delay countdown
- H'F9C6: candidate periodic report/heartbeat countdown
State address readers/writers:
- H'F9B0: reads=4 writes=1 read/write=4
H'3E60 read MOV:G.B @H'F9B0, R1; H'3E7A read_write ADD:Q.B #1, @H'F9B0; H'3E7E read_write BCLR.B #7, @H'F9B0; H'3E82 read MOV:G.B @H'F9B0, R0; H'3FF9 write CLR.B @H'F9B0; H'4059 read MOV:G.B @H'F9B0, R2
- H'F9B4: reads=3 writes=0 read/write=4
H'280C read CMP:G.B @H'F9B4, R1; H'3EA6 read MOV:G.B @H'F9B4, R1; H'3EC3 read_write ADD:Q.B #1, @H'F9B4; H'3EC7 read_write BCLR.B #5, @H'F9B4; H'BE78 read MOV:G.B @H'F9B4, R1; H'BE95 read_write ADD:Q.B #1, @H'F9B4
- H'F9B5: reads=4 writes=1 read/write=6
H'3E58 read MOV:G.B @H'F9B5, R0; H'3E8B read CMP:G.B @H'F9B5, R0; H'3FF5 write CLR.B @H'F9B5; H'405F read CMP:G.B @H'F9B5, R2; H'BAF2 read MOV:G.B @H'F9B5, R1; H'BD6D read_write ADD:Q.B #1, @H'F9B5
- H'F9B9: reads=3 writes=1 read/write=0
H'2806 read MOV:G.B @H'F9B9, R1; H'2822 write MOV:G.B R1, @H'F9B9; H'3E9E read MOV:G.B @H'F9B9, R0; H'BE70 read MOV:G.B @H'F9B9, R3
- H'F9C0: reads=3 writes=7 read/write=1
H'3FE5 read TST.B @H'F9C0; H'BA26 read TST.B @H'F9C0; H'BA2C write MOV:G.B #H'64, @H'F9C0; H'BAA2 write MOV:G.B #H'1F, @H'F9C0; H'BADA write MOV:G.B #H'09, @H'F9C0; H'BAE1 write MOV:G.B #H'09, @H'F9C0
- H'F9C1: reads=2 writes=2 read/write=1
H'BAED write CLR.B @H'F9C1; H'BB71 read TST.B @H'F9C1; H'BBA3 write MOV:G.B #H'05, @H'F9C1; H'BEF8 read TST.B @H'F9C1; H'BEFE read_write ADD:Q.B #-1, @H'F9C1
- H'F9C3: reads=6 writes=4 read/write=0
H'3FDF read TST.B @H'F9C3; H'4052 read TST.B @H'F9C3; H'BA90 read TST.B @H'F9C3; H'BB77 write CLR.B @H'F9C3; H'BB7D read CMP:G.B #H'05, @H'F9C3; H'BB8A read MOV:G.B @H'F9C3, R1
- H'F9C4: reads=2 writes=2 read/write=1
H'4046 read TST.B @H'F9C4; H'40E0 write MOV:G.B #H'14, @H'F9C4; H'BA31 write MOV:G.B #H'07, @H'F9C4; H'BF27 read TST.B @H'F9C4; H'BF2D read_write ADD:Q.B #-1, @H'F9C4
- H'F9C5: reads=2 writes=2 read/write=1
H'3FEF read TST.B @H'F9C5; H'BB9E write MOV:G.B #H'14, @H'F9C5; H'BEE4 write CLR.B @H'F9C5; H'BF31 read TST.B @H'F9C5; H'BF37 read_write ADD:Q.B #-1, @H'F9C5
- H'F9C6: reads=2 writes=2 read/write=1
H'BB46 write MOV:G.W #H'01F4, @H'F9C6; H'BEB5 read TST.W @H'F9C6; H'BEC5 write MOV:G.W #H'01F4, @H'F9C6; H'BF02 read TST.W @H'F9C6; H'BF08 read_write ADD:Q.W #-1, @H'F9C6
- H'F9C8: reads=1 writes=1 read/write=1
H'BB4C write MOV:G.B #H'14, @H'F9C8; H'BEBB read TST.B @H'F9C8; H'BEC1 read_write ADD:Q.B #-1, @H'F9C8
- H'FAA2: reads=6 writes=6 read/write=7
H'3FD3 read TST.B @H'FAA2; H'BA84 read BTST.B #3, @H'FAA2; H'BA96 read_write BCLR.B #3, @H'FAA2; H'BB00 read_write BSET.B #3, @H'FAA2; H'BC0F read TST.B @H'FAA2; H'BC15 read_write BSET.B #7, @H'FAA2
- H'FAA3: reads=2 writes=8 read/write=0
H'BA9A write CLR.B @H'FAA3; H'BB51 write MOV:G.B #H'80, @H'FAA3; H'BC63 write CLR.B @H'FAA3; H'BD75 write CLR.B @H'FAA3; H'BDD0 write CLR.B @H'FAA3; H'BDFB write CLR.B @H'FAA3
- H'FAA5: reads=5 writes=0 read/write=2
H'3FD9 read BTST.B #7, @H'FAA5; H'3FFD read_write BCLR.B #7, @H'FAA5; H'4007 read_write BSET.B #7, @H'FAA5; H'404C read BTST.B #7, @H'FAA5; H'BA8A read BTST.B #7, @H'FAA5; H'BE2D read BTST.B #7, @H'FAA5
Caveats:
- Observed report indexes 0x0007 and 0x0015 are capture overlays/runtime queue entries; this analyzer does not treat them as statically proven ROM constants.
- Queue entries near F870 are reached through RAM-indexed addressing; static JSON proves the access pattern, not the runtime queue contents.
- Branch predicates are summarized from local instruction order and targets; this is not an emulator trace.

View File

@@ -84,9 +84,14 @@ extern volatile u8 MEM8[0x10000];
#define SCI_SSR_FER 0x10u
#define SCI_SSR_PER 0x08u
#define TX_FRAME_LENGTH 6u
#define SCI1_TX_FRAME_LENGTH 6u
#define SCI1_TX_FRAME_BASE 0xF858u
#define SCI1_TX_FRAME_BYTE(n) MEM8[(u16)(SCI1_TX_FRAME_BASE + (n))]
#define SCI1_TX_FRAME_CHECKSUM SCI1_TX_FRAME_BYTE(5u)
#define SCI1_TX_INDEX MEM8[0xF9C2u]
#define TX_FRAME_LENGTH SCI1_TX_FRAME_LENGTH
#define TX_FRAME(n) MEM8[(u16)(0xF858u + (n))]
#define TX_INDEX MEM8[0xF9C2u]
#define TX_INDEX SCI1_TX_INDEX
#define RX_FRAME_LENGTH 6u
#define RX_CAPTURE(n) MEM8[(u16)(0xF868u + (n))]
@@ -168,22 +173,59 @@ extern volatile u8 MEM8[0x10000];
* state variable candidates:
* - event_queue_read_cursor_candidate H'F9B4: reads 1, writes 2; bits 5
* evidence: H'BE78, H'BE95, H'BE99
* - event_queue_write_or_pending_cursor_candidate H'F9B5: reads 1, writes 6; bits 7
* evidence: H'BAF2, H'BD6D, H'BD71, H'BDC8, H'BDCC, H'BDF3, H'BDF7
* - event_queue_write_or_pending_cursor_candidate H'F9B5: reads 2, writes 6; bits 7
* evidence: H'405F, H'BAF2, H'BD6D, H'BD71, H'BDC8, H'BDCC, H'BDF3, H'BDF7
* - event_queue_base_or_current_slot_candidate H'F9B9: reads 1, writes 0
* evidence: H'BE70
* - serial_tx_busy_timer_candidate H'F9C0: reads 2, writes 8
* evidence: H'BA26, H'BA2C, H'BAA2, H'BADA, H'BAE1, H'BAE8, H'BE1D, H'BE3E, H'BEEE, H'BEF4
* - serial_session_flags_candidate H'FAA2: reads 5, writes 13; bits 3, 7
* evidence: H'BA84, H'BA96, H'BB00, H'BC0F, H'BC15, H'BC33, H'BC5C, H'BCD0, H'BCFD, H'BD04, H'BD67, H'BD79, H'BDC2, H'BDD4, H'BDED, H'BDFF, H'BE47, H'BEAF
* - serial_pending_mask_candidate H'FAA3: reads 1, writes 9; bits 7
* evidence: H'BA9A, H'BB51, H'BC63, H'BD75, H'BDD0, H'BDFB, H'BE43, H'BEA5, H'BEA9, H'BECB
* - ... 3 more state-variable candidates
* - idle_heartbeat_gate_countdown_candidate H'F9C4: reads 2, writes 3
* evidence: H'4046, H'40E0, H'BA31, H'BF27, H'BF2D
* - rx_session_timeout_candidate H'F9C5: reads 1, writes 3
* evidence: H'BB9E, H'BEE4, H'BF31, H'BF37
* - ... 7 more state-variable candidates
* retry/error model candidate:
* - checksum path: 0x5A-seeded XOR over RX[0..4] differs from RX[5] -> loc_BE29
* - retry path: counter H'FAA6, threshold 2; Candidate retry path clears/consults serial flags, increments FAA6, compares it with 2, and when still below the apparent limit stages a command 0x07 response.
* - command 0x07 path: Candidate retransmit/explicit command 0x07 path either copies previous TX frame bytes back to F850-F854 or stages an observed 0x07 response before loc_BA26.
* - 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'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.
* - session_timeout_clears_gate_and_queue: When F9C5 is clear, loc_3FEF clears F9B5/F9B0 and clears FAA5.bit7; when nonzero, it sets FAA5.bit7.
* - idle_heartbeat_gate_initial_delay_loaded: Startup/init loads F9C4 with H'14 before the first idle/default report can be queued.
* - idle_heartbeat_gate_post_send_delay_loaded: loc_BA26 reloads F9C4 with H'07 after each send, matching the observed heartbeat spacing.
* - host_ack_can_advance_queue: Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.; commands H'05, H'06
* - caveat: Many panel controls may require host/session traffic before reporting. Observed autonomous call/camera-power indexes are runtime/capture overlays, not ROM constants.
* - evidence: H'3FD3, H'3FD7, H'3FD9, H'3FDD, H'3FDF, H'3FE3, H'3FE5, H'3FE9, H'3FEB, H'3FEF, H'3FF3, H'3FF5, H'3FF9, H'3FFD, H'4001, H'4003, H'4005, H'4007, H'4046, H'404A, H'404C, H'4050, H'4052, H'4056, H'4058, H'4059, H'405D, H'405F, H'4063, H'4065, H'4067, H'406C, H'4070, H'BAF2, H'BAF6, H'BAF8, H'BAFC, H'BAFE, H'BB00, H'BB04, H'BB06, H'BB08, H'BB0C, H'BB0E, H'BB11, H'BB13, H'BB15, H'BB17, H'BB19, H'BB1C, H'BB20, H'BB24, H'BB26, H'BB29, H'BB2B, H'BB2F, H'BB33, H'BB35, H'BB39, H'BB3D, H'BB3F, H'BB43, H'BE9E, H'BEA2, H'BEA5, H'BEA9, H'BEAD, H'BEAF, H'BEB3, H'BEB5, H'BEB9, H'BEBB, H'BEBF, H'BEC1, H'BEC5, H'BECB, H'BECF, H'BED1, H'BED5
* TX/autonomous report model candidate:
* - loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate; byte5 is 0x5A XOR checksum
* - observed overlay candidates: heartbeat_or_idle_report_candidate: 00 00 00 00 80 DA; call_button_report_candidate: 00 00 15 80 00 CF, 00 00 15 00 00 4F; camera_power_report_candidate: 00 00 07 80 00 DD
* - 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:
* - F9C6 reload H'01F4: Candidate periodic report/heartbeat timer reload.
* - F9C8 reload H'14: Candidate periodic resend countdown/retry spacing value.
* - FAA3 mask H'80: Candidate bit/mask that marks an autonomous report pending.
* - BED5 resend path: Candidate periodic resend path feeding the TX staging/send-builder flow.
* - evidence: H'BB46, H'BEC5, H'BB4C, H'BB51, H'BECB, H'BED5
* interrupt/timer architecture candidate:
* - FRT1 OCIA H'BEEA: Candidate periodic tick ISR for serial busy, interbyte, and resend counters.
* - FRT2 OCIA H'BF23: Candidate periodic tick ISR for idle heartbeat/report and RX session counters.
* - H'F9C0 tx_report_gate_counter_candidate: candidate gate counter used before entering the report builder.
* - H'F9C1 rx_interbyte_timeout_candidate: candidate RX interbyte timeout counter.
* - H'F9C6 periodic_resend_cadence_counter_candidate: candidate periodic resend/heartbeat cadence counter.
* - H'F9C4 idle_heartbeat_gate_countdown_candidate: candidate idle/default report enqueue countdown.
* - H'F9C5 rx_session_timeout_candidate: candidate RX/session maintenance timeout counter.
* - evidence: H'BEEA, H'BEEE, H'BEF2, H'BEF4, H'BEF8, H'BEFC, H'BEFE, H'BF02, H'BF06, H'BF08, H'BF23, H'BF27, H'BF2B, H'BF2D, H'BF31, H'BF35, H'BF37
*/
static u8 sci1_rx_candidate_command(void)
@@ -213,6 +255,83 @@ static u16 sci1_rx_candidate_logical_index(void)
return 0x01FFu;
}
static bool sci1_candidate_main_report_gate_open(void)
{
bool session_idle = MEM8[0xFAA2u] == 0u;
bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;
bool tx_timer_clear = MEM8[0xF9C0u] == 0u;
return session_idle && rx_gate_open && tx_timer_clear;
}
static bool sci1_candidate_report_queue_nonempty(void)
{
return MEM8[0xF9B5u] != MEM8[0xF9B0u];
}
static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)
{
bool idle_timer_clear = MEM8[0xF9C4u] == 0u;
bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;
bool queue_empty = MEM8[0xF9B0u] == MEM8[0xF9B5u];
return idle_timer_clear && rx_gate_open && queue_empty;
}
static 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;
bool period_elapsed = MEM8[0xF9C6u] == 0u && MEM8[0xF9C7u] == 0u;
bool resend_countdown_active = MEM8[0xF9C8u] != 0u;
return pending && period_elapsed && resend_countdown_active;
}
void frt1_ocia_candidate_tick_isr(void)
{
/* Candidate periodic tick at H'BEEA: decrement nonzero serial counters. */
/* TX_REPORT_GATE_COUNTER_CANDIDATE: candidate gate counter used before entering the report builder. */
if (MEM8[0xF9C0u] != 0u) {
MEM8[0xF9C0u] = (u8)(MEM8[0xF9C0u] - 1u);
}
/* RX_INTERBYTE_TIMEOUT_CANDIDATE: candidate RX interbyte timeout counter. */
if (MEM8[0xF9C1u] != 0u) {
MEM8[0xF9C1u] = (u8)(MEM8[0xF9C1u] - 1u);
}
/* PERIODIC_RESEND_CADENCE_COUNTER_CANDIDATE: candidate periodic resend/heartbeat cadence counter. */
if (MEM8[0xF9C6u] != 0u) {
MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);
}
}
void frt2_ocia_candidate_tick_isr(void)
{
/* Candidate periodic tick at H'BF23: decrement nonzero serial counters. */
/* IDLE_HEARTBEAT_GATE_COUNTDOWN_CANDIDATE: candidate idle/default report enqueue countdown. */
if (MEM8[0xF9C4u] != 0u) {
MEM8[0xF9C4u] = (u8)(MEM8[0xF9C4u] - 1u);
}
/* RX_SESSION_TIMEOUT_CANDIDATE: candidate RX/session maintenance timeout counter. */
if (MEM8[0xF9C5u] != 0u) {
MEM8[0xF9C5u] = (u8)(MEM8[0xF9C5u] - 1u);
}
}
void sci1_process_candidate_protocol_command(void)
{
u8 command = sci1_rx_candidate_command();
@@ -316,6 +435,7 @@ void sci1_tx_start_candidate_frame(void)
/* wait for transmit data register empty */
}
/* First byte is sent synchronously; TIE enables TXI for the remaining bytes. */
SCI1_TDR = TX_FRAME(0);
TX_INDEX = 1u;
SCI1_SSR &= (u8)~SCI_SSR_TDRE;
@@ -324,6 +444,11 @@ void sci1_tx_start_candidate_frame(void)
void sci1_txi_candidate_isr(void)
{
/* TXI runs after hardware reasserts SSR.TDRE for the next transmit byte. */
if ((SCI1_SSR & SCI_SSR_TDRE) == 0u) {
return;
}
if (TX_INDEX < TX_FRAME_LENGTH) {
SCI1_TDR = TX_FRAME(TX_INDEX);
TX_INDEX = (u8)(TX_INDEX + 1u);

2991
build/rom_table_xrefs.json Normal file

File diff suppressed because it is too large Load Diff

93
build/rom_table_xrefs.txt Normal file
View File

@@ -0,0 +1,93 @@
Table/Index Cross-Reference Report for build\rom_decompiled.json
================================================================
Static offsets are emitted only when an index register value can be derived from nearby immediate loads in the current JSON. Other indexed accesses are dynamic.
LCD correlation hints
term 'CONNECT': no LCD/text candidate hits in current decompile
term 'CONNECT: OK': no LCD/text candidate hits in current decompile
term 'CONNECT: NOT ACT': no LCD/text candidate hits in current decompile
term 'NOT ACT': no LCD/text candidate hits in current decompile
term 'COMM LINK': 2 candidate hit(s): H'77F4 'COMM LINK ITEM-1Xw', H'78F4 'COMM LINK ITEM-2Xx'
term 'COMPLETED': 1 candidate hit(s): H'A025 'COMPLETED'
display builder xrefs: H'5A91:165, H'5EED:15, H'5E24:14, H'5B88:13, H'5C91:10, H'5D9A:5
LCD driver routines: H'3F40 lcd_wait_and_transfer
caveat: LCD strings can be builder/script output; absence of a literal term does not disprove runtime composition.
primary_value_table_candidate H'E000-H'E3FF (negative H'2000; direct H'F900-H'F91F)
accesses=31 reads=21 writes=10 dynamic=11
static offsets: H'0000, H'0004, H'0006, H'0046, H'0080, H'0102, H'0124, H'0126, H'014E, H'016E, H'0172, H'01EC, H'0220
functions: loc_BBAB:5, loc_2650:3, loc_4096:3, loc_1795:2, loc_19DB:2, loc_1A35:2, loc_48FA:2, vec_ad_adi_3D99:2, <no function>:1, loc_1705:1, loc_174D:1, loc_17C9:1
- H'170C read offset H'014E -> H'E14E; loc_1705; BTST.W #15, @H'E14E
- H'175A read offset H'016E -> H'E16E; loc_174D; BTST.W #13, @H'E16E
- H'179C read offset H'0172 -> H'E172; loc_1795; BTST.W #13, @H'E172
- H'17A7 read offset H'0220 -> H'E220; loc_1795; BTST.W #15, @H'E220
- H'17D0 read offset H'0126 -> H'E126; loc_17C9; BTST.W #12, @H'E126
- H'1802 read offset H'0126 -> H'E126; loc_17FB; BTST.W #12, @H'E126
- H'183A read offset H'0126 -> H'E126; loc_182D; BTST.W #5, @H'E126
- H'189E read offset H'0126 -> H'E126; loc_1891; BTST.W #5, @H'E126
- H'18F4 read offset H'0126 -> H'E126; loc_18E7; BTST.W #5, @H'E126
- H'19E3 read index dynamic via R3 operand @(-H'2000,R3); loc_19DB; MOV:G.W @(-H'2000,R3), R0
- H'1A03 read index dynamic via R3 operand @(-H'2000,R3); loc_19DB; CMP:G.W @(-H'2000,R3), R1
- H'1A3D read index dynamic via R3 operand @(-H'2000,R3); loc_1A35; MOV:G.W @(-H'2000,R3), R0
- H'1A6B read index dynamic via R3 operand @(-H'2000,R3); loc_1A35; CMP:G.W @(-H'2000,R3), R0
- H'2657 read offset H'0124 -> H'E124; loc_2650; MOV:G.W @H'E124, R0
- H'266F read offset H'0004 -> H'E004; loc_2650; BTST.W #13, @H'E004
- H'268B read offset H'0124 -> H'E124; loc_2650; CMP:G.W @H'E124, R0
- H'3DDA read offset H'0102 -> H'E102; vec_ad_adi_3D99; MOV:G.W @H'E102, R0
- H'3DFA read offset H'0102 -> H'E102; vec_ad_adi_3D99; CMP:G.W @H'E102, R1
- H'3F8C write index dynamic via R0 operand @(-H'2000,R0); <no function>; CLR.W @(-H'2000,R0)
- H'402C write offset H'0046 -> H'E046; loc_400C; CLR.W @H'E046
- H'4077 write index dynamic via R0 operand @(-H'2000,R0); loc_4075; CLR.W @(-H'2000,R0)
- H'4096 write offset H'0000 -> H'E000; loc_4096; MOV:G.W #H'0080, @H'E000
- H'409C write offset H'0006 -> H'E006; loc_4096; MOV:G.W #H'8000, @H'E006
- H'40A2 write offset H'0080 -> H'E080; loc_4096; MOV:G.W #H'FFFF, @H'E080
- H'490F read offset H'01EC -> H'E1EC; loc_48FA; BTST.W #13, @H'E1EC
- H'4915 read offset H'01EC -> H'E1EC; loc_48FA; MOV:G.W @H'E1EC, R0
- H'BC75 write index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W R0, @(-H'2000,R4)
- H'BC95 write index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W R0, @(-H'2000,R4)
- H'BCEC read index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W @(-H'2000,R4), R0
- H'BD1A write index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W R0, @(-H'2000,R4)
- H'BD35 write index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W R0, @(-H'2000,R4)
secondary_value_table_candidate H'E400-H'E7FF (negative H'1C00; direct H'F940-H'F95F)
accesses=8 reads=6 writes=2 dynamic=8
functions: loc_1A35:2, loc_1A9C:2, <no function>:1, loc_19A2:1, loc_4075:1, loc_BBAB:1
- H'19AA read index dynamic via R3 operand @(-H'1C00,R3); loc_19A2; MOV:G.W @(-H'1C00,R3), R0
- H'1A4B read index dynamic via R3 operand @(-H'1C00,R3); loc_1A35; MOV:G.W @(-H'1C00,R3), R1
- H'1A5B read index dynamic via R3 operand @(-H'1C00,R3); loc_1A35; MOV:G.W @(-H'1C00,R3), R1
- H'1A81 read index dynamic via R3 operand @(-H'1C00,R3); <no function>; AND.W @(-H'1C00,R3), R1
- H'1AB4 read index dynamic via R3 operand @(-H'1C00,R3); loc_1A9C; BTST.W R0, @(-H'1C00,R3)
- H'1AC1 read index dynamic via R3 operand @(-H'1C00,R3); loc_1A9C; BTST.W R0, @(-H'1C00,R3)
- H'407B write index dynamic via R0 operand @(-H'1C00,R0); loc_4075; CLR.W @(-H'1C00,R0)
- H'BDE5 write index dynamic via R4 operand @(-H'1C00,R4); loc_BBAB; MOV:G.W R0, @(-H'1C00,R4)
current_value_table_candidate H'E800-H'EBFF (negative H'1800; direct H'F920-H'F93F)
accesses=14 reads=1 writes=13 dynamic=8
static offsets: H'0000, H'0006, H'0080, H'0102, H'0124, H'01EC
functions: loc_4096:3, loc_BBAB:3, <no function>:1, loc_15E0:1, loc_19DB:1, loc_1A35:1, loc_2650:1, loc_4075:1, loc_48FA:1, loc_BAF2:1
- H'15ED write offset H'0102 -> H'E902; loc_15E0; MOV:G.W R1, @H'E902
- H'1A09 write index dynamic via R3 operand @(-H'1800,R3); loc_19DB; MOV:G.W R1, @(-H'1800,R3)
- H'1A71 write index dynamic via R3 operand @(-H'1800,R3); loc_1A35; MOV:G.W R0, @(-H'1800,R3)
- H'2691 write offset H'0124 -> H'E924; loc_2650; MOV:G.W R0, @H'E924
- H'3F90 write index dynamic via R0 operand @(-H'1800,R0); <no function>; CLR.W @(-H'1800,R0)
- H'407F write index dynamic via R0 operand @(-H'1800,R0); loc_4075; CLR.W @(-H'1800,R0)
- H'40A8 write offset H'0000 -> H'E800; loc_4096; MOV:G.W #H'0080, @H'E800
- H'40AE write offset H'0006 -> H'E806; loc_4096; MOV:G.W #H'8000, @H'E806
- H'40B4 write offset H'0080 -> H'E880; loc_4096; MOV:G.W #H'FFFF, @H'E880
- H'491D write offset H'01EC -> H'E9EC; loc_48FA; MOV:G.W R0, @H'E9EC
- H'BB35 read index dynamic via R0 operand @(-H'1800,R0); loc_BAF2; MOV:G.W @(-H'1800,R0), R4
- H'BC79 write index dynamic via R4 operand @(-H'1800,R4); loc_BBAB; MOV:G.W R0, @(-H'1800,R4)
- H'BC99 write index dynamic via R4 operand @(-H'1800,R4); loc_BBAB; MOV:G.W R0, @(-H'1800,R4)
- H'BD1E write index dynamic via R4 operand @(-H'1800,R4); loc_BBAB; MOV:G.W R0, @(-H'1800,R4)
flag_table_candidate H'EC00-H'EFFF (negative H'1400; direct H'F980-H'F99F)
accesses=6 reads=0 writes=6 dynamic=5
static offsets: H'0200
functions: loc_BBAB:5, loc_4075:1
- H'4088 write offset H'0200 -> H'EE00; loc_4075; CLR.W @(-H'1400,R0)
- H'BC82 write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)
- H'BC9D write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)
- H'BD22 write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)
- H'BD39 write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)
- H'BDE9 write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #6, @(-H'1400,R5)

348
h8536/bench_connect_lcd.py Normal file
View File

@@ -0,0 +1,348 @@
from __future__ import annotations
import argparse
import sys
import time
from collections import Counter
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Iterable, TextIO
CHECKSUM_SEED = 0x5A
FRAME_LENGTH = 6
CONNECT_LCD_SEQUENCE = (
bytes.fromhex("04000040001E"),
bytes.fromhex("0400008000DE"),
bytes.fromhex("040000C0009E"),
)
COMMAND7_REPEAT_FRAME = bytes.fromhex("07000000005D")
@dataclass
class FrameDetector:
buffer: bytearray = field(default_factory=bytearray)
frames: list[bytes] = field(default_factory=list)
labels: Counter[str] = field(default_factory=Counter)
def feed(self, data: bytes) -> list[tuple[bytes, str]]:
self.buffer.extend(data)
detected = []
while len(self.buffer) >= FRAME_LENGTH:
frame = bytes(self.buffer[:FRAME_LENGTH])
del self.buffer[:FRAME_LENGTH]
label = label_frame(frame)
self.frames.append(frame)
if label:
self.labels[label] += 1
detected.append((frame, label))
return detected
class BenchLogger:
def __init__(self, path: Path, stdout: TextIO = sys.stdout) -> None:
self.path = path
self.stdout = stdout
self.path.parent.mkdir(parents=True, exist_ok=True)
self.file = self.path.open("w", encoding="utf-8", newline="\n")
def close(self) -> None:
self.file.close()
def emit(self, line: str = "") -> None:
print(line, file=self.stdout)
print(line, file=self.file)
self.file.flush()
def chunk(self, direction: str, data: bytes) -> None:
self.emit(f"{timestamp()} {direction:<2} {len(data):03d} bytes {format_frame(data)}")
def event(self, text: str) -> None:
self.emit(f"{timestamp()} {text}")
def frame_checksum(data: bytes) -> int:
checksum = CHECKSUM_SEED
for value in data[: FRAME_LENGTH - 1]:
checksum ^= value
return checksum & 0xFF
def frame_checksum_ok(frame: bytes) -> bool:
return len(frame) == FRAME_LENGTH and frame_checksum(frame) == frame[-1]
def parse_frame(text: str) -> bytes:
normalized = text.strip().replace(",", " ").replace(":", " ").replace("-", " ").replace("_", " ")
parts = normalized.split()
if len(parts) == 1:
compact = parts[0]
if compact.lower().startswith("0x"):
compact = compact[2:]
if compact.upper().startswith("H'"):
compact = compact[2:]
if len(compact) % 2:
raise argparse.ArgumentTypeError("compact frame hex must contain an even number of digits")
parts = [compact[index : index + 2] for index in range(0, len(compact), 2)]
values = [_parse_byte(part) for part in parts]
if len(values) == FRAME_LENGTH - 1:
values.append(frame_checksum(bytes(values)))
if len(values) != FRAME_LENGTH:
raise argparse.ArgumentTypeError("frame must contain five bytes plus computed checksum, or exactly six bytes")
return bytes(values)
def format_frame(data: bytes) -> str:
return data.hex(" ").upper()
def label_frame(frame: bytes) -> str:
labels = {
bytes.fromhex("0000000080DA"): "heartbeat",
bytes.fromhex("02000200005A"): "connect_ok_path_response_candidate",
bytes.fromhex("010002000059"): "connect_c0_path_response_candidate",
bytes.fromhex("07804040A07D"): "visible_40A0_family_40",
bytes.fromhex("07808040A0BD"): "visible_40A0_family_80",
bytes.fromhex("0780C040A0FD"): "visible_40A0_family_C0",
bytes.fromhex("0780C060205D"): "visible_C0_6020_family_candidate",
}
label = labels.get(frame, "")
if label:
return label
if frame_checksum_ok(frame):
return "checksum_ok_unlabeled"
return "checksum_bad_or_unaligned"
def default_log_path() -> Path:
return Path("captures") / f"bench-connect-lcd-sequence-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Bench-test the emulator CONNECT LCD sequence against the real RCP over RS232."
)
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP")
parser.add_argument("--baud", type=int, default=38400, help="RCP serial baud rate")
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port")
parser.add_argument("--relay-baud", type=int, default=115200, help="Pico relay serial baud rate")
parser.add_argument("--no-power-cycle", action="store_true", help="do not send relay off/on before the test")
parser.add_argument("--power-off-command", default="off", help="relay command used to remove DUT power")
parser.add_argument("--power-on-command", default="on", help="relay command used to apply DUT power")
parser.add_argument("--off-seconds", type=float, default=1.5, help="seconds to hold the DUT powered off")
parser.add_argument("--relay-settle", type=float, default=2.0, help="seconds to wait after opening the Pico relay port")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="seconds to wait for heartbeat before sending")
parser.add_argument("--ready-heartbeats", type=int, default=2, help="heartbeat frames to observe before sending")
parser.add_argument("--require-ready", action="store_true", help="abort if ready heartbeat count is not observed")
parser.add_argument("--frame-gap", type=float, default=0.150, help="seconds to listen between sent frames")
parser.add_argument("--post-sequence-read", type=float, default=3.0, help="seconds to listen after the sequence")
parser.add_argument("--repeat", type=int, default=1, help="times to send the frame sequence in the same power session")
parser.add_argument("--frame", action="append", type=parse_frame, help="override preset with a custom frame; repeatable")
parser.add_argument("--two-frame", action="store_true", help="send only the first two CONNECT candidate frames")
parser.add_argument("--command7-after", action="store_true", help="send command-7 repeat probe after the sequence")
parser.add_argument("--pre-sequence-drain", type=float, default=0.250, help="seconds to drain/log RX immediately before sending")
parser.add_argument("--prompt-screen", action="store_true", help="prompt for observed LCD text after the sequence")
parser.add_argument("--prompt-before-send", action="store_true", help="also prompt for LCD text before sending the sequence")
parser.add_argument("--log", type=Path, help="capture log path")
parser.add_argument("--dry-run", action="store_true", help="print the planned sequence without opening serial ports")
return parser
def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
args = build_arg_parser().parse_args(argv)
frames = _planned_frames(args)
log_path = args.log or default_log_path()
if args.dry_run:
print(f"device={args.port} {args.baud} 8N1", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle={int(not args.no_power_cycle)} off={args.power_off_command!r} on={args.power_on_command!r}", file=stdout)
for index, frame in enumerate(frames, start=1):
print(f"frame[{index}]={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}", file=stdout)
if args.command7_after:
print(f"command7_after={format_frame(COMMAND7_REPEAT_FRAME)} checksum_ok=1", file=stdout)
print(f"log={log_path}", file=stdout)
return 0
serial = _import_serial()
logger = BenchLogger(log_path, stdout=stdout)
detector = FrameDetector()
try:
logger.emit("CONNECT LCD bench sequence")
logger.emit(f"device={args.port} {args.baud} 8N1 relay={args.relay_port} {args.relay_baud}")
logger.emit(f"log={log_path}")
for index, frame in enumerate(frames, start=1):
logger.emit(f"plan frame[{index}]={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}")
with serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0.05) as device:
relay = None
try:
if not args.no_power_cycle:
relay = serial.Serial(args.relay_port, args.relay_baud, timeout=0.25)
_relay_settle(relay, args.relay_settle, logger)
_relay_command(relay, args.power_off_command, logger)
time.sleep(args.off_seconds)
device.reset_input_buffer()
detector = FrameDetector()
_relay_command(relay, args.power_on_command, logger)
else:
device.reset_input_buffer()
ready = _wait_for_ready(device, detector, logger, args.ready_timeout, args.ready_heartbeats)
if args.require_ready and not ready:
logger.event("ABORT ready heartbeat threshold was not observed")
return 2
if args.prompt_before_send:
_prompt_screen("LCD after boot/ready", logger)
if args.pre_sequence_drain > 0:
logger.event(f"DRAIN before sequence {args.pre_sequence_drain:.3f}s")
_read_for(device, detector, logger, args.pre_sequence_drain)
for repeat_index in range(args.repeat):
if args.repeat > 1:
logger.event(f"BEGIN repeat {repeat_index + 1}/{args.repeat}")
for frame_index, frame in enumerate(frames, start=1):
_send_frame(device, frame, logger, f"seq{repeat_index + 1}.frame{frame_index}")
_read_for(device, detector, logger, args.frame_gap)
if args.command7_after:
_send_frame(device, COMMAND7_REPEAT_FRAME, logger, "command7_after")
_read_for(device, detector, logger, args.frame_gap)
_read_for(device, detector, logger, args.post_sequence_read)
if args.prompt_screen:
_prompt_screen("LCD after CONNECT sequence", logger)
finally:
if relay is not None:
relay.close()
_summary(detector, logger)
return 0
finally:
logger.close()
def _planned_frames(args: argparse.Namespace) -> list[bytes]:
if args.frame:
frames = list(args.frame)
elif args.two_frame:
frames = list(CONNECT_LCD_SEQUENCE[:2])
else:
frames = list(CONNECT_LCD_SEQUENCE)
if not frames:
raise SystemExit("no frames selected")
return frames
def _import_serial():
try:
import serial
except ImportError as exc: # pragma: no cover - depends on local environment.
raise SystemExit("pyserial is required; install it with: .\\.venv\\Scripts\\python.exe -m pip install pyserial") from exc
return serial
def _send_frame(device, frame: bytes, logger: BenchLogger, label: str) -> None:
device.write(frame)
device.flush()
logger.chunk("TX", frame)
logger.event(f"SENT {label} checksum_ok={int(frame_checksum_ok(frame))}")
def _read_for(device, detector: FrameDetector, logger: BenchLogger, seconds: float) -> None:
deadline = time.monotonic() + max(0.0, seconds)
while time.monotonic() < deadline:
waiting = getattr(device, "in_waiting", 0)
data = device.read(waiting or 1)
if data:
logger.chunk("RX", data)
for frame, label in detector.feed(data):
logger.event(f"DETECT {label} {format_frame(frame)}")
def _wait_for_ready(
device,
detector: FrameDetector,
logger: BenchLogger,
timeout_seconds: float,
ready_heartbeats: int,
) -> bool:
logger.event(f"WAIT ready heartbeat target={ready_heartbeats} timeout={timeout_seconds:.3f}s")
start_count = detector.labels["heartbeat"]
deadline = time.monotonic() + max(0.0, timeout_seconds)
while time.monotonic() < deadline:
_read_for(device, detector, logger, 0.100)
if detector.labels["heartbeat"] - start_count >= ready_heartbeats:
logger.event(f"READY heartbeat_count={detector.labels['heartbeat']}")
return True
logger.event(f"READY_TIMEOUT heartbeat_count={detector.labels['heartbeat']}")
return False
def _relay_settle(relay, seconds: float, logger: BenchLogger) -> None:
time.sleep(max(0.0, seconds))
_read_relay_lines(relay, logger, prefix="RELAY")
def _relay_command(relay, command: str, logger: BenchLogger) -> None:
relay.write((command.strip() + "\n").encode("utf-8"))
relay.flush()
logger.event(f"RELAY_TX {command.strip()}")
_read_relay_lines(relay, logger, prefix="RELAY_RX")
def _read_relay_lines(relay, logger: BenchLogger, prefix: str) -> None:
deadline = time.monotonic() + 2.0
while time.monotonic() < deadline:
line = relay.readline()
if not line:
return
logger.event(f"{prefix} {line.decode('utf-8', errors='replace').strip()}")
def _prompt_screen(label: str, logger: BenchLogger) -> None:
note = input(f"{label}: type observed LCD text, or press Enter to skip: ").strip()
logger.event(f"SCREEN {label}: {note or '(no note)'}")
def _summary(detector: FrameDetector, logger: BenchLogger) -> None:
logger.emit()
logger.emit("Summary")
logger.emit(f"rx_frames={len(detector.frames)} trailing_unframed_bytes={len(detector.buffer)}")
for label, count in sorted(detector.labels.items()):
logger.emit(f"{label}={count}")
def _parse_byte(text: str) -> int:
token = text.strip()
if token.lower().startswith("0x"):
token = token[2:]
if token.upper().startswith("H'"):
token = token[2:]
if not token or len(token) > 2:
raise argparse.ArgumentTypeError(f"invalid byte token {text!r}")
try:
value = int(token, 16)
except ValueError as exc:
raise argparse.ArgumentTypeError(f"invalid byte token {text!r}") from exc
if not 0 <= value <= 0xFF:
raise argparse.ArgumentTypeError(f"byte out of range {text!r}")
return value
def timestamp() -> str:
return datetime.now().strftime("%H:%M:%S.%f")[:-3]
__all__ = [
"COMMAND7_REPEAT_FRAME",
"CONNECT_LCD_SEQUENCE",
"FrameDetector",
"build_arg_parser",
"format_frame",
"frame_checksum",
"frame_checksum_ok",
"label_frame",
"main",
"parse_frame",
]

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

111
h8536/emulator/__init__.py Normal file
View File

@@ -0,0 +1,111 @@
from __future__ import annotations
from .cli import build_arg_parser, discover_rom_path, load_rom, main
from .constants import (
HEARTBEAT_FRAME,
FRT_TCR_OCIEA,
FRT_TCSR_OCFA,
FRT1_TCR,
FRT1_TCSR,
FRT2_TCR,
FRT2_TCSR,
IPRA,
IPRC,
IPRE,
ON_CHIP_RAM_END,
ON_CHIP_RAM_START,
P9DDR,
P9DR,
RAMCR,
REGISTER_FIELD_END,
REGISTER_FIELD_START,
SCI_SCR_RE,
SCI_SCR_RIE,
SCI_SCR_TE,
SCI_SCR_TIE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
VECTOR_FRT1_OCIA,
VECTOR_INTERVAL_TIMER,
VECTOR_FRT2_OCIA,
VECTOR_SCI1_ERI,
VECTOR_SCI1_RXI,
VECTOR_SCI1_TXI,
WDT_TCSR_R,
)
from .cpu import CPUState
from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent
from .memory import MemoryAccess, MemoryMap, describe_regions
from .peripherals import LCD
from .runner import H8536Emulator, RunReport
from .sci import SCI1, SciTxEvent
__all__ = [
"CPUState",
"EmulatorError",
"FRT1_TCR",
"FRT1_TCSR",
"FRT2_TCR",
"FRT2_TCSR",
"FRT_TCR_OCIEA",
"FRT_TCSR_OCFA",
"HEARTBEAT_FRAME",
"H8536Emulator",
"IPRA",
"IPRC",
"IPRE",
"LCD",
"MemoryAccess",
"MemoryMap",
"ON_CHIP_RAM_END",
"ON_CHIP_RAM_START",
"P9DDR",
"P9DR",
"P9FastPath",
"P9FastPathConfig",
"P9FastPathEvent",
"RAMCR",
"REGISTER_FIELD_END",
"REGISTER_FIELD_START",
"RunReport",
"SCI1",
"SCI1_BRR",
"SCI1_RDR",
"SCI1_SCR",
"SCI1_SMR",
"SCI1_SSR",
"SCI1_TDR",
"SCI_SCR_RE",
"SCI_SCR_RIE",
"SCI_SCR_TE",
"SCI_SCR_TIE",
"SCI_SSR_FER",
"SCI_SSR_ORER",
"SCI_SSR_PER",
"SCI_SSR_RDRF",
"SCI_SSR_TDRE",
"SciTxEvent",
"UnsupportedInstruction",
"VECTOR_FRT1_OCIA",
"VECTOR_INTERVAL_TIMER",
"VECTOR_FRT2_OCIA",
"VECTOR_SCI1_ERI",
"VECTOR_SCI1_RXI",
"VECTOR_SCI1_TXI",
"WDT_TCSR_R",
"build_arg_parser",
"describe_regions",
"discover_rom_path",
"load_rom",
"main",
]

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
from .cli import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,451 @@
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterable, Mapping
from ..formatting import h16
from ..bench_connect_lcd import FrameDetector, format_frame, label_frame
from .cli import load_rom
from .errors import UnsupportedInstruction
from .runner import H8536Emulator
from .rx_probe import (
RunContext,
_interrupt_mask,
_rx_byte_consumed,
_rx_ready,
_run_until,
_sci1_priority,
)
BENCH_CHUNK_RE = re.compile(
r"^\s*(?P<time>\d{1,2}:\d{2}:\d{2}(?:\.\d{1,6})?)\s+"
r"(?P<direction>TX|RX)\s+"
r"(?P<count>\d+)(?:\s+bytes?)?\s+"
r"(?P<byte_text>.*?)\s*$",
re.IGNORECASE,
)
SCREEN_RE = re.compile(
r"^\s*(?P<time>\d{1,2}:\d{2}:\d{2}(?:\.\d{1,6})?)\s+SCREEN\s+"
r"(?P<label>.*?):\s*(?P<note>.*?)\s*$",
re.IGNORECASE,
)
HEX_BYTE_RE = re.compile(r"\b[0-9A-Fa-f]{2}\b")
HEARTBEAT_FRAME = bytes.fromhex("0000000080DA")
CONNECT_OK_RESPONSE = bytes.fromhex("02000200005A")
BENCH_VISIBLE_C0_6020 = bytes.fromhex("0780C060205D")
@dataclass(frozen=True)
class BenchChunk:
timestamp: str
timestamp_ms: int
direction: str
bytes: bytes
raw_line: str
@dataclass(frozen=True)
class BenchFrame:
timestamp: str
timestamp_ms: int
frame: bytes
label: str
@dataclass(frozen=True)
class ScreenNote:
timestamp: str
timestamp_ms: int
label: str
note: str
@dataclass(frozen=True)
class BenchReplayLog:
chunks: tuple[BenchChunk, ...]
tx_frames: tuple[BenchFrame, ...]
rx_frames: tuple[BenchFrame, ...]
screen_notes: tuple[ScreenNote, ...]
@property
def first_tx_ms(self) -> int | None:
return self.tx_frames[0].timestamp_ms if self.tx_frames else None
@property
def last_event_ms(self) -> int | None:
values = [chunk.timestamp_ms for chunk in self.chunks]
values.extend(note.timestamp_ms for note in self.screen_notes)
return max(values) if values else None
@dataclass(frozen=True)
class ReplayFrameResult:
host_frame: bytes
host_timestamp: str
host_delta_ms: int
steps_before: int
steps_during_rx: int
emulator_gap_frames_before: tuple[bytes, ...]
emulator_new_frames: tuple[bytes, ...]
@dataclass(frozen=True)
class BenchReplayResult:
log_path: Path
rom_path: Path
boot_summary: str
host_frames: tuple[BenchFrame, ...]
observed_device_frames: tuple[BenchFrame, ...]
replay_frame_results: tuple[ReplayFrameResult, ...]
emulator_tx_frames: tuple[bytes, ...]
emulator_lcd_display: str
emulator_lcd_line_buffer: str
parity: Mapping[str, Any]
def as_dict(self) -> dict[str, Any]:
return {
"kind": "h8536_emulator_bench_replay",
"log_path": str(self.log_path),
"rom_path": str(self.rom_path),
"boot_summary": self.boot_summary,
"host_frames": [_bench_frame_dict(frame) for frame in self.host_frames],
"observed_device_frames": [_bench_frame_dict(frame) for frame in self.observed_device_frames],
"replay_frame_results": [
{
"host_timestamp": item.host_timestamp,
"host_delta_ms": item.host_delta_ms,
"host_frame": format_frame(item.host_frame),
"steps_before": item.steps_before,
"steps_during_rx": item.steps_during_rx,
"emulator_gap_frames_before": [format_frame(frame) for frame in item.emulator_gap_frames_before],
"emulator_new_frames": [format_frame(frame) for frame in item.emulator_new_frames],
}
for item in self.replay_frame_results
],
"emulator_tx_frames": [format_frame(frame) for frame in self.emulator_tx_frames],
"emulator_lcd_display": self.emulator_lcd_display,
"emulator_lcd_line_buffer": self.emulator_lcd_line_buffer,
"parity": dict(self.parity),
}
def text_lines(self) -> list[str]:
lines = [
f"log={self.log_path}",
f"rom={self.rom_path}",
self.boot_summary,
"bench_host_frames=" + " | ".join(format_frame(frame.frame) for frame in self.host_frames),
"bench_device_nonheartbeat_frames="
+ _format_frame_list(frame.frame for frame in self.observed_device_frames if frame.frame != HEARTBEAT_FRAME),
"emulator_tx_frames=" + _format_frame_list(self.emulator_tx_frames),
f"emulator_lcd_display={self.emulator_lcd_display!r}",
f"emulator_lcd_line_buffer={self.emulator_lcd_line_buffer!r}",
"replay_frames:",
]
for index, item in enumerate(self.replay_frame_results):
lines.append(
(
f" [{index}] {item.host_timestamp} delta={item.host_delta_ms}ms "
f"steps_before={item.steps_before} steps_rx={item.steps_during_rx} "
f"host={format_frame(item.host_frame)} "
f"gap_emu={_format_frame_list(item.emulator_gap_frames_before)} "
f"emu_new={_format_frame_list(item.emulator_new_frames)}"
)
)
lines.append("parity:")
for key, value in self.parity.items():
lines.append(f" {key}={value}")
return lines
@dataclass(frozen=True)
class ReplayConfig:
boot_steps: int = 250_000
per_byte_steps: int = 5_000
steps_per_second: int = 65_000
post_log_steps: int = 50_000
interval_steps: int = 512
frt1_ocia_steps: int = 512
frt2_ocia_steps: int = 512
p9_fast_path: bool = True
p9_fast_input: int = 0xFF
def parse_bench_replay_log_text(text: str) -> BenchReplayLog:
chunks: list[BenchChunk] = []
tx_frames: list[BenchFrame] = []
rx_frames: list[BenchFrame] = []
screen_notes: list[ScreenNote] = []
rx_detector = FrameDetector()
for raw_line in text.splitlines():
screen_match = SCREEN_RE.match(raw_line)
if screen_match:
screen_notes.append(
ScreenNote(
timestamp=screen_match.group("time"),
timestamp_ms=_timestamp_ms(screen_match.group("time")),
label=screen_match.group("label").strip(),
note=screen_match.group("note").strip(),
)
)
continue
match = BENCH_CHUNK_RE.match(raw_line)
if not match:
continue
timestamp = match.group("time")
timestamp_ms = _timestamp_ms(timestamp)
direction = match.group("direction").upper()
data = bytes(int(token, 16) for token in HEX_BYTE_RE.findall(match.group("byte_text")))
declared = int(match.group("count"))
if len(data) != declared:
# Keep the bytes we could parse; the raw line remains available.
pass
chunk = BenchChunk(timestamp, timestamp_ms, direction, data, raw_line)
chunks.append(chunk)
if direction == "TX":
for frame_offset in range(0, len(data), 6):
frame = data[frame_offset : frame_offset + 6]
if len(frame) == 6:
tx_frames.append(BenchFrame(timestamp, timestamp_ms, frame, label_frame(frame)))
else:
for frame, label in rx_detector.feed(data):
rx_frames.append(BenchFrame(timestamp, timestamp_ms, frame, label))
return BenchReplayLog(tuple(chunks), tuple(tx_frames), tuple(rx_frames), tuple(screen_notes))
def parse_bench_replay_log(path: Path) -> BenchReplayLog:
return parse_bench_replay_log_text(path.read_text(encoding="utf-8"))
def run_bench_replay(log_path: Path, *, rom_path: Path | None = None, config: ReplayConfig = ReplayConfig()) -> BenchReplayResult:
bench_log = parse_bench_replay_log(log_path)
rom_bytes, discovered_rom_path = load_rom(rom_path)
emulator = H8536Emulator(
rom_bytes,
interval_steps=config.interval_steps,
frt1_ocia_steps=config.frt1_ocia_steps,
frt2_ocia_steps=config.frt2_ocia_steps,
p9_fast_path_enabled=config.p9_fast_path,
p9_fast_default_input_byte=config.p9_fast_input,
)
context = RunContext()
boot_steps_used, boot_reason = _run_until(emulator, config.boot_steps, _rx_ready, context)
boot_summary = (
f"boot={boot_reason} steps={boot_steps_used} pc={h16(emulator.cpu.pc)} "
f"SCR={emulator.sci1.scr:02X} SSR={emulator.sci1.ssr:02X} "
f"rx_serviceable={int(_rx_ready(emulator))} "
f"sci1_priority={_sci1_priority(emulator)} interrupt_mask={_interrupt_mask(emulator)} "
f"lcd_display={emulator.memory.lcd.display_text(lines=4)!r}"
)
replay_results: list[ReplayFrameResult] = []
previous_tx_ms: int | None = None
for host in bench_log.tx_frames:
delta_ms = 0 if previous_tx_ms is None else max(0, host.timestamp_ms - previous_tx_ms)
tx_frame_start_before_delay = len(emulator.sci1.tx_frames)
steps_before = _run_steps_for_ms(emulator, delta_ms, config.steps_per_second, context)
gap_frames = tuple(emulator.sci1.tx_frames[tx_frame_start_before_delay:])
tx_frame_start = len(emulator.sci1.tx_frames)
steps_during_rx = _inject_host_frame(emulator, host.frame, config.per_byte_steps, context)
replay_results.append(
ReplayFrameResult(
host_frame=host.frame,
host_timestamp=host.timestamp,
host_delta_ms=delta_ms,
steps_before=steps_before,
steps_during_rx=steps_during_rx,
emulator_gap_frames_before=gap_frames,
emulator_new_frames=tuple(emulator.sci1.tx_frames[tx_frame_start:]),
)
)
previous_tx_ms = host.timestamp_ms
_run_steps(emulator, config.post_log_steps, context)
emulator_lcd_display = emulator.memory.lcd.display_text(lines=4, width=16)
emulator_lcd_line_buffer = _ascii_window(emulator, 0xFAF0, 16)
parity = assess_bench_parity(
bench_log,
emulator_tx_frames=emulator.sci1.tx_frames,
emulator_lcd_display=emulator_lcd_display,
emulator_lcd_line_buffer=emulator_lcd_line_buffer,
)
return BenchReplayResult(
log_path=log_path,
rom_path=discovered_rom_path,
boot_summary=boot_summary,
host_frames=bench_log.tx_frames,
observed_device_frames=bench_log.rx_frames,
replay_frame_results=tuple(replay_results),
emulator_tx_frames=tuple(emulator.sci1.tx_frames),
emulator_lcd_display=emulator_lcd_display,
emulator_lcd_line_buffer=emulator_lcd_line_buffer,
parity=parity,
)
def assess_bench_parity(
bench_log: BenchReplayLog,
*,
emulator_tx_frames: Iterable[bytes],
emulator_lcd_display: str,
emulator_lcd_line_buffer: str,
) -> dict[str, Any]:
emulator_frames = list(emulator_tx_frames)
bench_screen_text = " | ".join(note.note for note in bench_log.screen_notes)
bench_reached_ok = _looks_like_connect_ok(bench_screen_text)
emulator_reached_ok = _looks_like_connect_ok(emulator_lcd_display) or _looks_like_connect_ok(emulator_lcd_line_buffer)
bench_nonheartbeat = [frame.frame for frame in bench_log.rx_frames if frame.frame != HEARTBEAT_FRAME]
emulator_nonheartbeat = [frame for frame in emulator_frames if frame != HEARTBEAT_FRAME]
bench_visible_c0_6020 = BENCH_VISIBLE_C0_6020 in bench_nonheartbeat
emulator_visible_c0_6020 = BENCH_VISIBLE_C0_6020 in emulator_nonheartbeat
emulator_connect_ok_response = CONNECT_OK_RESPONSE in emulator_nonheartbeat
mismatch_reasons: list[str] = []
if bench_reached_ok != emulator_reached_ok:
mismatch_reasons.append("lcd_connect_state")
if bench_visible_c0_6020 and not emulator_visible_c0_6020:
mismatch_reasons.append("missing_visible_C0_6020_response")
if not bench_reached_ok and emulator_connect_ok_response:
mismatch_reasons.append("emulator_emitted_connect_ok_response")
return {
"bench_reached_connect_ok": bench_reached_ok,
"emulator_reached_connect_ok": emulator_reached_ok,
"bench_visible_C0_6020": bench_visible_c0_6020,
"emulator_visible_C0_6020": emulator_visible_c0_6020,
"emulator_connect_ok_response": emulator_connect_ok_response,
"bench_nonheartbeat_frames": [format_frame(frame) for frame in bench_nonheartbeat],
"emulator_nonheartbeat_frames": [format_frame(frame) for frame in emulator_nonheartbeat],
"matched": not mismatch_reasons,
"mismatch_reasons": mismatch_reasons,
}
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Replay a real bench serial log into the H8/536 ROM emulator.")
parser.add_argument("log", type=Path, help="bench log produced by scripts/bench_connect_lcd_sequence.py")
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN")
parser.add_argument("--boot-steps", type=int, default=ReplayConfig.boot_steps)
parser.add_argument("--per-byte-steps", type=int, default=ReplayConfig.per_byte_steps)
parser.add_argument("--steps-per-second", type=int, default=ReplayConfig.steps_per_second)
parser.add_argument("--post-log-steps", type=int, default=ReplayConfig.post_log_steps)
parser.add_argument("--interval-steps", type=int, default=ReplayConfig.interval_steps)
parser.add_argument("--frt1-ocia-steps", type=int, default=ReplayConfig.frt1_ocia_steps)
parser.add_argument("--frt2-ocia-steps", type=int, default=ReplayConfig.frt2_ocia_steps)
parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines")
parser.add_argument("--p9-fast-input", type=lambda text: int(text, 0), default=ReplayConfig.p9_fast_input)
parser.add_argument("--assert-bench-parity", action="store_true", help="exit nonzero if emulator behavior diverges from the bench log")
parser.add_argument("--json", action="store_true", help="emit JSON")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_arg_parser().parse_args(argv)
result = run_bench_replay(
args.log,
rom_path=args.rom,
config=ReplayConfig(
boot_steps=args.boot_steps,
per_byte_steps=args.per_byte_steps,
steps_per_second=args.steps_per_second,
post_log_steps=args.post_log_steps,
interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps,
p9_fast_path=not args.no_p9_fast_path,
p9_fast_input=args.p9_fast_input,
),
)
if args.json:
print(json.dumps(result.as_dict(), indent=2))
else:
for line in result.text_lines():
print(line)
if args.assert_bench_parity and not result.parity.get("matched"):
return 1
return 0
def _inject_host_frame(emulator: H8536Emulator, frame: bytes, per_byte_steps: int, context: RunContext) -> int:
steps_total = 0
for value in frame:
emulator.inject_sci1_rx_byte(value)
steps, _reason = _run_until(emulator, per_byte_steps, _rx_byte_consumed, context)
steps_total += steps
return steps_total
def _run_steps_for_ms(emulator: H8536Emulator, delta_ms: int, steps_per_second: int, context: RunContext) -> int:
steps = int((max(0, delta_ms) * max(1, steps_per_second)) / 1000)
return _run_steps(emulator, steps, context)
def _run_steps(emulator: H8536Emulator, steps: int, context: RunContext) -> int:
completed = 0
for _ in range(max(0, steps)):
context.record_pc(emulator.cpu.pc)
try:
emulator.step()
except UnsupportedInstruction as exc:
context.unsupported = str(exc)
break
completed += 1
return completed
def _ascii_window(emulator: H8536Emulator, start: int, length: int) -> str:
chars = []
for offset in range(length):
value = emulator.memory.read8(start + offset)
chars.append(chr(value) if 0x20 <= value <= 0x7E else ".")
return "".join(chars)
def _looks_like_connect_ok(text: str) -> bool:
normalized = " ".join(text.upper().replace(":", " ").split())
return "CONNECT" in normalized and "OK" in normalized and "NOT ACT" not in normalized
def _timestamp_ms(text: str) -> int:
hours, minutes, rest = text.split(":")
if "." in rest:
seconds, fraction = rest.split(".", 1)
else:
seconds, fraction = rest, "0"
fraction = (fraction + "000")[:3]
return ((int(hours) * 60 + int(minutes)) * 60 + int(seconds)) * 1000 + int(fraction)
def _bench_frame_dict(frame: BenchFrame) -> dict[str, Any]:
return {
"timestamp": frame.timestamp,
"timestamp_ms": frame.timestamp_ms,
"frame": format_frame(frame.frame),
"label": frame.label,
}
def _format_frame_list(frames: Iterable[bytes]) -> str:
items = [format_frame(frame) for frame in frames]
return " | ".join(items) if items else "none"
__all__ = [
"BENCH_VISIBLE_C0_6020",
"BenchReplayLog",
"BenchReplayResult",
"ReplayConfig",
"assess_bench_parity",
"main",
"parse_bench_replay_log",
"parse_bench_replay_log_text",
"run_bench_replay",
]

77
h8536/emulator/cli.py Normal file
View File

@@ -0,0 +1,77 @@
from __future__ import annotations
import argparse
from pathlib import Path
from ..formatting import h16, parse_int
from .memory import describe_regions
from .runner import H8536Emulator
def discover_rom_path(root: Path) -> Path | None:
candidates = [
root / "ROM" / "M27C512@DIP28_1.BIN",
root / "rom.bin",
]
candidates.extend(sorted((root / "ROM").glob("*.BIN")) if (root / "ROM").exists() else [])
candidates.extend(sorted((root / "ROM").glob("*.bin")) if (root / "ROM").exists() else [])
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def load_rom(path: Path | None = None, root: Path | None = None) -> tuple[bytes, Path]:
root = root if root is not None else Path.cwd()
rom_path = path if path is not None else discover_rom_path(root)
if rom_path is None:
raise FileNotFoundError(
"could not discover ROM bytes; pass --rom PATH, expected ROM/M27C512@DIP28_1.BIN or another ROM/*.BIN"
)
return rom_path.read_bytes(), rom_path
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Minimal H8/536 emulation harness scaffold")
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present")
parser.add_argument("--max-steps", type=int, default=64, help="maximum CPU steps to execute")
parser.add_argument("--trace", action="store_true", help="print decoded/executed instruction trace")
parser.add_argument("--stop-on-heartbeat", action="store_true", help="stop only when 00 00 00 00 80 DA is emitted through SCI1 TDR")
parser.add_argument("--memory-map", action="store_true", help="print the scaffolded memory map before running")
parser.add_argument("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt")
parser.add_argument("--frt1-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT1 OCIA interrupt")
parser.add_argument("--frt2-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT2 OCIA interrupt")
parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_arg_parser().parse_args(argv)
try:
rom_bytes, rom_path = load_rom(args.rom)
except FileNotFoundError as exc:
print(str(exc))
return 2
emulator = H8536Emulator(
rom_bytes,
interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps,
p9_fast_path_enabled=args.p9_fast_path,
p9_fast_default_input_byte=args.p9_fast_input,
)
print(f"rom={rom_path}")
print(f"reset_vector={h16(emulator.reset_vector())}")
if args.memory_map:
print(describe_regions())
report = emulator.run(args.max_steps, trace=args.trace, stop_on_heartbeat=args.stop_on_heartbeat)
if args.trace:
for line in report.trace:
print(line)
for line in report.summary_lines():
print(line)
if not report.heartbeat_seen:
print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR")
return 0

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
SCI1_SMR = 0xFED8
SCI1_BRR = 0xFED9
SCI1_SCR = 0xFEDA
SCI1_TDR = 0xFEDB
SCI1_SSR = 0xFEDC
SCI1_RDR = 0xFEDD
P9DDR = 0xFEFE
P9DR = 0xFEFF
IPRA = 0xFF00
IPRC = 0xFF02
IPRE = 0xFF04
WDT_TCSR_R = 0xFEEC
FRT1_TCR = 0xFE90
FRT1_TCSR = 0xFE91
FRT2_TCR = 0xFEA0
FRT2_TCSR = 0xFEA1
SCI_SCR_TIE = 0x80
SCI_SCR_RIE = 0x40
SCI_SCR_TE = 0x20
SCI_SCR_RE = 0x10
SCI_SSR_TDRE = 0x80
SCI_SSR_RDRF = 0x40
SCI_SSR_ORER = 0x20
SCI_SSR_FER = 0x10
SCI_SSR_PER = 0x08
FRT_TCR_OCIEA = 0x20
FRT_TCSR_OCFA = 0x20
ON_CHIP_RAM_START = 0xF680
ON_CHIP_RAM_END = 0xFE7F
REGISTER_FIELD_START = 0xFE80
REGISTER_FIELD_END = 0xFFFF
RAMCR = 0xFF11
HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA])
VECTOR_INTERVAL_TIMER = 0x0042
VECTOR_FRT1_OCIA = 0x0062
VECTOR_FRT2_OCIA = 0x006A
VECTOR_SCI1_ERI = 0x0080
VECTOR_SCI1_RXI = 0x0082
VECTOR_SCI1_TXI = 0x0084

34
h8536/emulator/cpu.py Normal file
View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class CPUState:
pc: int = 0
sr: int = 0
br: int = 0
regs: list[int] = field(default_factory=lambda: [0] * 8)
cycles: int = 0
steps: int = 0
z: bool = False
c: bool = False
n: bool = False
v: bool = False
interrupt_depth: int = 0
def s8(value: int) -> int:
return value - 0x100 if value & 0x80 else value
def s16(value: int) -> int:
return value - 0x10000 if value & 0x8000 else value
def mask(size: int) -> int:
return 0xFFFF if size == 2 else 0xFF
def sign_bit(size: int) -> int:
return 0x8000 if size == 2 else 0x80

16
h8536/emulator/errors.py Normal file
View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from ..formatting import h16
class EmulatorError(Exception):
pass
class UnsupportedInstruction(EmulatorError):
def __init__(self, pc: int, raw: bytes, text: str) -> None:
raw_text = " ".join(f"{byte:02X}" for byte in raw)
super().__init__(f"unsupported instruction at {h16(pc)}: {raw_text} {text}".rstrip())
self.pc = pc
self.raw = raw
self.text = text

View File

@@ -0,0 +1,152 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from .cpu import mask, sign_bit
LOC_BFE0_TRANSFER_WRAPPER = 0xBFE0
LOC_BFFE_TRANSFER_WRAPPER = 0xBFFE
LOC_C08B_P9_WRITE_BYTE = 0xC08B
LOC_C0DB_P9_READ_BYTE = 0xC0DB
LOC_C10C_P9_MARKER = 0xC10C
LOC_C121_P9_MARKER = 0xC121
LOC_C142_P9_MARKER = 0xC142
@dataclass(frozen=True)
class P9FastPathConfig:
"""Configuration for optional ROM P9 transfer shortcuts.
The helper assumes the CPU PC is exactly at a known routine entry. It
models the routine as if it completed successfully and returned via RTS.
Integration should keep this disabled unless the runner intentionally opts
into skipping these ROM routines.
"""
enabled: bool = False
write_byte_pc: int = LOC_C08B_P9_WRITE_BYTE
read_byte_pc: int = LOC_C0DB_P9_READ_BYTE
marker_pcs: frozenset[int] = frozenset(
{
LOC_C10C_P9_MARKER,
LOC_C121_P9_MARKER,
LOC_C142_P9_MARKER,
}
)
wrapper_pcs: frozenset[int] = frozenset(
{
LOC_BFE0_TRANSFER_WRAPPER,
LOC_BFFE_TRANSFER_WRAPPER,
}
)
default_input_byte: int = 0xFF
account_step: bool = True
cycles_per_hit: int = 0
@dataclass(frozen=True)
class P9FastPathEvent:
kind: str
pc: int
value: int | None = None
source: str | None = None
queue_depth: int | None = None
def line(self) -> str:
value = "" if self.value is None else f" value={self.value:02X}"
source = "" if self.source is None else f" source={self.source}"
queue_depth = "" if self.queue_depth is None else f" queued={self.queue_depth}"
return f"{self.kind} pc={self.pc:04X}{value}{source}{queue_depth}"
@dataclass
class P9FastPath:
"""Optional fast-path scaffold for ROM P9 bit-transfer routines."""
config: P9FastPathConfig = field(default_factory=P9FastPathConfig)
input_bytes: list[int] = field(default_factory=list)
input_sources: list[str] = field(default_factory=list)
output_bytes: list[int] = field(default_factory=list)
events: list[P9FastPathEvent] = field(default_factory=list)
def __post_init__(self) -> None:
if len(self.input_sources) < len(self.input_bytes):
self.input_sources.extend(["initial"] * (len(self.input_bytes) - len(self.input_sources)))
elif len(self.input_sources) > len(self.input_bytes):
del self.input_sources[len(self.input_bytes) :]
def queue_input(self, *values: int, source: str = "queued") -> None:
self.input_bytes.extend(value & 0xFF for value in values)
self.input_sources.extend(source for _ in values)
def queue_input_script(self, name: str, values: list[int] | tuple[int, ...]) -> None:
self.queue_input(*values, source=f"script:{name}")
def trace_lines(self, limit: int | None = None) -> list[str]:
events = self.events if limit is None else self.events[-limit:]
return [event.line() for event in events]
def try_handle(self, emulator: Any) -> bool:
if not self.config.enabled:
return False
pc = emulator.cpu.pc & 0xFFFF
if pc == (self.config.write_byte_pc & 0xFFFF):
self._handle_write_byte(emulator)
elif pc == (self.config.read_byte_pc & 0xFFFF):
self._handle_read_byte(emulator)
elif pc in self.config.marker_pcs:
self.events.append(P9FastPathEvent("marker", pc))
self._return_from_subroutine(emulator)
elif pc in self.config.wrapper_pcs:
self.events.append(P9FastPathEvent("wrapper_success", pc))
emulator.cpu.regs[0] = 1
self._set_logic_flags(emulator.cpu, 1, 1)
self._return_from_subroutine(emulator)
else:
return False
if self.config.account_step:
emulator.cpu.steps += 1
emulator.cpu.cycles += self.config.cycles_per_hit
return True
def _handle_write_byte(self, emulator: Any) -> None:
pc = emulator.cpu.pc & 0xFFFF
value = emulator.cpu.regs[0] & 0xFF
self.output_bytes.append(value)
self.events.append(P9FastPathEvent("write_byte", pc, value))
emulator.cpu.regs[0] = 1
self._set_logic_flags(emulator.cpu, 1, 1)
self._return_from_subroutine(emulator)
def _handle_read_byte(self, emulator: Any) -> None:
pc = emulator.cpu.pc & 0xFFFF
if self.input_bytes:
value = self.input_bytes.pop(0)
source = self.input_sources.pop(0) if self.input_sources else "queued"
else:
value = self.config.default_input_byte
source = "default_input_byte"
value &= 0xFF
self.events.append(P9FastPathEvent("read_byte", pc, value, source, len(self.input_bytes)))
# The ROM-side read routine yields a byte in R5. Model that as a byte
# register write so the existing high byte is not accidentally clobbered.
emulator.cpu.regs[5] = (emulator.cpu.regs[5] & 0xFF00) | value
self._set_logic_flags(emulator.cpu, value, 1)
self._return_from_subroutine(emulator)
def _return_from_subroutine(self, emulator: Any) -> None:
sp = emulator.cpu.regs[7] & 0xFFFF
emulator.cpu.pc = emulator.memory.read16(sp) & 0xFFFF
emulator.cpu.regs[7] = (sp + 2) & 0xFFFF
def _set_logic_flags(self, cpu: Any, value: int, size: int) -> None:
value &= mask(size)
cpu.z = value == 0
cpu.n = bool(value & sign_bit(size))
cpu.v = False

136
h8536/emulator/memory.py Normal file
View File

@@ -0,0 +1,136 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from ..formatting import h16
from ..memory import MEMORY_REGIONS, MemoryRegion, region_for
from ..rom import Rom
from .constants import (
ON_CHIP_RAM_END,
ON_CHIP_RAM_START,
P9DDR,
P9DR,
REGISTER_FIELD_END,
REGISTER_FIELD_START,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
)
from .peripherals.lcd import LCD, LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
from .peripherals.p9_bus import P9Bus
from .sci import SCI1
@dataclass
class MemoryAccess:
address: int
size: int
value: int
kind: str
region: str
class MemoryMap:
def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None:
self.rom = Rom(rom_bytes, base=0)
self.sci1 = sci1 if sci1 is not None else SCI1()
self.lcd = LCD()
self.p9_bus = P9Bus()
self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1)
self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1)
self.external: dict[int, int] = {}
self.access_log: list[MemoryAccess] = []
self._set_register(SCI1_SMR, self.sci1.smr)
self._set_register(SCI1_BRR, self.sci1.brr)
self._set_register(SCI1_SCR, self.sci1.scr)
self._set_register(SCI1_TDR, self.sci1.tdr)
self._set_register(SCI1_SSR, self.sci1.ssr)
self._set_register(SCI1_RDR, self.sci1.rdr)
def region(self, address: int) -> MemoryRegion:
return region_for(address & 0xFFFF)
def read8(self, address: int) -> int:
address &= 0xFFFF
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
value = self.sci1.read(address)
self._set_register(address, value)
elif address == LCD_E_CLOCK_STATUS:
value = self.lcd.read_status()
elif address == LCD_E_CLOCK_DATA:
value = self.lcd.read_data()
elif address in self.external:
value = self.external[address]
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
value = self.ram[address - ON_CHIP_RAM_START]
elif address == P9DDR:
value = self.p9_bus.read_ddr()
self._set_register(address, value)
elif address == P9DR:
value = self.p9_bus.read_dr()
elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
value = self.registers[address - REGISTER_FIELD_START]
elif self.rom.contains(address):
value = self.rom.u8(address)
else:
value = self.external.get(address, 0xFF)
self._log("read", address, 1, value)
return value
def read16(self, address: int) -> int:
high = self.read8(address)
low = self.read8((address + 1) & 0xFFFF)
return (high << 8) | low
def write8(self, address: int, value: int) -> None:
address &= 0xFFFF
value &= 0xFF
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
self.sci1.write(address, value)
self._set_register(address, self.sci1.read(address))
elif address == LCD_E_CLOCK_STATUS:
self.lcd.write_command(value)
self.external[address] = value
elif address == LCD_E_CLOCK_DATA:
self.lcd.write_data(value)
self.external[address] = value
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
self.ram[address - ON_CHIP_RAM_START] = value
elif address == P9DDR:
self._set_register(address, self.p9_bus.write_ddr(value))
elif address == P9DR:
self._set_register(address, self.p9_bus.write_dr(value))
elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
self._set_register(address, value)
elif self.rom.contains(address):
# The ROM image spans the whole address space, but the H8/536 map
# can place external RAM/peripherals in these ranges. Keep writes
# as external overrides while leaving instruction fetch immutable.
self.external[address] = value
else:
self.external[address] = value
self._log("write", address, 1, value)
def write16(self, address: int, value: int) -> None:
self.write8(address, (value >> 8) & 0xFF)
self.write8((address + 1) & 0xFFFF, value & 0xFF)
def inject_sci1_rx_byte(self, value: int) -> None:
self.sci1.inject_rx(value)
self._set_register(SCI1_RDR, self.sci1.read(SCI1_RDR))
self._set_register(SCI1_SSR, self.sci1.read(SCI1_SSR))
def _set_register(self, address: int, value: int) -> None:
self.registers[address - REGISTER_FIELD_START] = value & 0xFF
def _log(self, kind: str, address: int, size: int, value: int) -> None:
self.access_log.append(MemoryAccess(address, size, value, kind, self.region(address).name))
def describe_regions(regions: Iterable[MemoryRegion] = MEMORY_REGIONS) -> str:
return "\n".join(f"{h16(region.start)}-{h16(region.end)} {region.name} {region.kind}" for region in regions)

View File

@@ -0,0 +1,15 @@
from __future__ import annotations
from .lcd import LCD, LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS, LCD_LINE_WIDTH
from .p9_bus import P9_ACK_BIT, P9_STROBE_BIT, P9Bus, P9StrobeEvent
__all__ = [
"LCD_E_CLOCK_DATA",
"LCD_E_CLOCK_STATUS",
"LCD",
"LCD_LINE_WIDTH",
"P9_ACK_BIT",
"P9_STROBE_BIT",
"P9Bus",
"P9StrobeEvent",
]

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
from dataclasses import dataclass, field
LCD_E_CLOCK_STATUS = 0xF200
LCD_E_CLOCK_DATA = 0xF201
LCD_LINE_WIDTH = 16
LCD_LINE_STARTS = (0x00, 0x40, 0x10, 0x50)
LCD_DDRAM_SIZE = 0x80
@dataclass
class LCD:
ddram: bytearray = field(default_factory=lambda: bytearray([0x20] * LCD_DDRAM_SIZE))
cursor: int = 0
data_latch: int = 0
command_latch: int = 0
def read_status(self) -> int:
return self.cursor & 0x7F
def read_data(self) -> int:
return self.data_latch & 0xFF
def write_command(self, value: int) -> None:
value &= 0xFF
self.command_latch = value
if value & 0x80:
self.cursor = value & 0x7F
elif value == 0x01:
self.ddram[:] = bytes([0x20]) * LCD_DDRAM_SIZE
self.cursor = 0
elif value == 0x02:
self.cursor = 0
def write_data(self, value: int) -> None:
value &= 0xFF
self.data_latch = value
if 0 <= self.cursor < LCD_DDRAM_SIZE:
self.ddram[self.cursor] = value
self.cursor = (self.cursor + 1) & 0x7F
def line_text(self, line: int, width: int = LCD_LINE_WIDTH) -> str:
if not 0 <= line < len(LCD_LINE_STARTS):
raise ValueError(f"LCD line out of range: {line}")
start = LCD_LINE_STARTS[line]
return "".join(_display_char(self.ddram[(start + offset) & 0x7F]) for offset in range(width))
def display_lines(self, lines: int = 4, width: int = LCD_LINE_WIDTH) -> list[str]:
return [self.line_text(line, width) for line in range(lines)]
def display_text(self, lines: int = 4, width: int = LCD_LINE_WIDTH) -> str:
return " | ".join(self.display_lines(lines, width))
def _display_char(value: int) -> str:
return chr(value) if 0x20 <= value <= 0x7E else "."
__all__ = [
"LCD",
"LCD_DDRAM_SIZE",
"LCD_E_CLOCK_DATA",
"LCD_E_CLOCK_STATUS",
"LCD_LINE_STARTS",
"LCD_LINE_WIDTH",
]

View File

@@ -0,0 +1,80 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
P9_ACK_BIT = 0x80
P9_STROBE_BIT = 0x02
@dataclass(frozen=True)
class P9StrobeEvent:
edge: str
ddr: int
dr: int
data_bit: int
bit7_output: bool
class P9Bus:
"""Small model for the ROM's P9 bit-banged serial handshake."""
def __init__(self, ddr: int = 0x00, dr: int = 0x00, input_bits: Iterable[int] = ()) -> None:
self.ddr = ddr & 0xFF
self.dr_latch = dr & 0xFF
self.input_bits: list[int] = [1 if bit else 0 for bit in input_bits]
self.default_input_bit = 0
self.strobe_edges: list[P9StrobeEvent] = []
self.transmitted_bits: list[int] = []
self.byte_candidates: list[int] = []
def write_ddr(self, value: int) -> int:
self.ddr = value & 0xFF
return self.ddr
def write_dr(self, value: int) -> int:
previous = self.dr_latch
self.dr_latch = value & 0xFF
previous_strobe = bool(previous & P9_STROBE_BIT)
current_strobe = bool(self.dr_latch & P9_STROBE_BIT)
if previous_strobe != current_strobe:
edge = "rising" if current_strobe else "falling"
data_bit = 1 if self.dr_latch & P9_ACK_BIT else 0
bit7_output = bool(self.ddr & P9_ACK_BIT)
self.strobe_edges.append(P9StrobeEvent(edge, self.ddr, self.dr_latch, data_bit, bit7_output))
if edge == "rising" and bit7_output:
self._record_transmitted_bit(data_bit)
return self.dr_latch
def read_ddr(self) -> int:
return self.ddr
def read_dr(self) -> int:
value = self.dr_latch
if not (self.ddr & P9_ACK_BIT):
if self.input_bits:
input_bit = self.input_bits.pop(0)
else:
input_bit = self.default_input_bit
if input_bit:
value |= P9_ACK_BIT
else:
value &= ~P9_ACK_BIT
return value & 0xFF
def queue_input_bits(self, bits: Iterable[int]) -> None:
self.input_bits.extend(1 if bit else 0 for bit in bits)
def set_default_input_bit(self, bit: int) -> None:
self.default_input_bit = 1 if bit else 0
def _record_transmitted_bit(self, bit: int) -> None:
self.transmitted_bits.append(bit)
if len(self.transmitted_bits) % 8 == 0:
byte = 0
for data_bit in self.transmitted_bits[-8:]:
byte = (byte << 1) | data_bit
self.byte_candidates.append(byte)

1265
h8536/emulator/probe.py Normal file

File diff suppressed because it is too large Load Diff

681
h8536/emulator/runner.py Normal file
View File

@@ -0,0 +1,681 @@
from __future__ import annotations
from dataclasses import dataclass, field
from ..decoder import H8536Decoder
from ..formatting import h16
from ..rom import DecodeError
from ..vectors import read_vectors_min
from .constants import (
FRT_TCR_OCIEA,
FRT_TCSR_OCFA,
FRT1_TCR,
FRT1_TCSR,
FRT2_TCR,
FRT2_TCSR,
IPRA,
IPRC,
IPRE,
SCI_SCR_RE,
SCI_SCR_RIE,
SCI_SCR_TIE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
VECTOR_FRT1_OCIA,
VECTOR_FRT2_OCIA,
VECTOR_INTERVAL_TIMER,
VECTOR_SCI1_ERI,
VECTOR_SCI1_RXI,
VECTOR_SCI1_TXI,
)
from .cpu import CPUState, mask, s8, s16, sign_bit
from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig
from .memory import MemoryMap
from .sci import SCI1
@dataclass
class RunReport:
steps: int
cycles: int
pc: int
stopped_reason: str
tx_bytes: bytes
tx_frames: list[bytes]
heartbeat_seen: bool
unsupported: str | None = None
trace: list[str] = field(default_factory=list)
def summary_lines(self) -> list[str]:
lines = [
f"steps={self.steps}",
f"cycles={self.cycles}",
f"pc={h16(self.pc)}",
f"stopped={self.stopped_reason}",
"tx_bytes=" + self.tx_bytes.hex(" ").upper(),
"tx_frames=" + ", ".join(frame.hex(" ").upper() for frame in self.tx_frames),
f"heartbeat_seen={self.heartbeat_seen}",
]
if self.unsupported:
lines.append(f"unsupported={self.unsupported}")
lines.append(
"next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow"
)
return lines
class H8536Emulator:
def __init__(
self,
rom_bytes: bytes,
*,
interval_steps: int = 2048,
frt1_ocia_steps: int = 1024,
frt2_ocia_steps: int = 1024,
p9_fast_path: P9FastPath | None = None,
p9_fast_path_enabled: bool = False,
p9_fast_default_input_byte: int = 0xFF,
) -> None:
if not rom_bytes:
raise ValueError("ROM image is empty")
self.sci1 = SCI1()
self.memory = MemoryMap(rom_bytes, self.sci1)
self.p9_fast_path = p9_fast_path or P9FastPath(
P9FastPathConfig(enabled=p9_fast_path_enabled, default_input_byte=p9_fast_default_input_byte)
)
self.cpu = CPUState()
self.vectors = read_vectors_min(self.memory.rom)
self.interval_steps = max(1, interval_steps)
self.frt1_ocia_steps = max(1, frt1_ocia_steps)
self.frt2_ocia_steps = max(1, frt2_ocia_steps)
self._interval_counter = 0
self._frt1_ocia_counter = 0
self._frt2_ocia_counter = 0
self.reset()
def reset(self) -> None:
self.cpu = CPUState(pc=self.reset_vector())
def reset_vector(self) -> int:
if self.memory.rom.contains(0, 2):
return self.memory.rom.u16(0)
raise DecodeError("ROM does not contain a reset vector at H'0000")
def inject_sci1_rx_byte(self, value: int) -> None:
self.memory.inject_sci1_rx_byte(value)
def step(self) -> str:
pc = self.cpu.pc
if self.p9_fast_path.try_handle(self):
self._tick_peripherals()
return f"{h16(pc)}: {'<p9-fast-path>':<17} P9 fast-path"
decoder = H8536Decoder(self.memory.rom, br=self.cpu.br)
ins = decoder.decode(pc)
if not ins.valid:
raise UnsupportedInstruction(pc, ins.raw, ins.text)
next_pc = (pc + ins.size) & 0xFFFF
raw = ins.raw
text = ins.text
if raw[0] == 0x00:
pass
elif raw[0] == 0x02 and len(raw) == 2:
self._pop_register_mask(raw[1])
elif raw[0] == 0x11 and len(raw) >= 2:
next_pc = self._indirect_jump_call(raw, pc, next_pc)
elif raw[0] == 0x12 and len(raw) == 2:
self._push_register_mask(raw[1])
elif raw[0] in (0x01, 0x06, 0x07) and len(raw) == 3 and 0xB8 <= raw[1] <= 0xBF:
next_pc = self._scb(raw, pc, next_pc)
elif raw[0] in (0x04, 0x0C):
next_pc = self._execute_general(pc, next_pc)
elif 0x40 <= raw[0] <= 0x47 and len(raw) == 2:
reg = raw[0] & 0x07
self._cmp(self._reg_read(reg, 1), raw[1], 1)
elif 0x48 <= raw[0] <= 0x4F and len(raw) == 3:
reg = raw[0] & 0x07
self._cmp(self.cpu.regs[reg], int.from_bytes(raw[1:3], "big"), 2)
elif 0x50 <= raw[0] <= 0x57 and len(raw) == 2:
self._reg_write(raw[0] & 0x07, raw[1], 1)
self._set_logic_flags(raw[1], 1)
elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3:
self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big")
self._set_logic_flags(self.cpu.regs[raw[0] & 0x07], 2)
elif raw[0] in (0x0E, 0x1E, 0x18):
next_pc = self._direct_call(raw, next_pc)
elif raw[0] == 0x19:
next_pc = self._pop16()
elif raw[0] == 0x0A:
next_pc = self._return_from_interrupt()
elif raw[0] in (0x15, 0x1D) and len(raw) >= 4:
next_pc = self._execute_general(pc, next_pc)
elif raw[0] in range(0xA0, 0x100):
next_pc = self._execute_general(pc, next_pc)
elif raw[0] in range(0x20, 0x30) and len(raw) == 2:
next_pc = self._branch8(raw, pc, next_pc)
elif raw[0] in range(0x30, 0x40) and len(raw) == 3:
next_pc = self._branch16(raw, pc, next_pc)
else:
raise UnsupportedInstruction(pc, raw, text)
self.cpu.pc = next_pc
self.cpu.steps += 1
self.cpu.cycles += self._rough_cycles(raw)
self._tick_peripherals()
return f"{h16(pc)}: {' '.join(f'{byte:02X}' for byte in raw):<17} {text}"
def run(self, max_steps: int, trace: bool = False, stop_on_heartbeat: bool = False) -> RunReport:
trace_lines: list[str] = []
stopped_reason = "max_steps"
unsupported: str | None = None
for _ in range(max_steps):
try:
line = self.step()
except UnsupportedInstruction as exc:
stopped_reason = "unsupported_instruction"
unsupported = str(exc)
break
if trace:
trace_lines.append(line)
if stop_on_heartbeat and self.sci1.saw_heartbeat():
stopped_reason = "heartbeat"
break
return RunReport(
steps=self.cpu.steps,
cycles=self.cpu.cycles,
pc=self.cpu.pc,
stopped_reason=stopped_reason,
tx_bytes=bytes(self.sci1.tx_bytes),
tx_frames=list(self.sci1.tx_frames),
heartbeat_seen=self.sci1.saw_heartbeat(),
unsupported=unsupported,
trace=trace_lines,
)
def _execute_general(self, pc: int, next_pc: int) -> int:
ea = self._decode_ea(pc)
op = self.memory.rom.u8(pc + int(ea["length"]))
size = int(ea["size"])
raw = self.memory.rom.slice(pc, self._general_length(pc, ea, op))
if op == 0x00:
ext = self.memory.rom.u8(pc + int(ea["length"]) + 1)
ext_base = ext & 0xF8
reg = ext & 0x07
if ext_base == 0x80:
value = self._read_ea(ea, 1)
self._reg_write(reg, value, 1)
self._set_logic_flags(value, 1)
elif ext_base == 0x90:
self._write_ea(ea, self._reg_read(reg, 1), 1)
else:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
return next_pc
if op in (0x06, 0x07):
value = raw[-1] if op == 0x06 else int.from_bytes(raw[-2:], "big")
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
rd = op & 0x07
if ea["mode"] == "imm" and base in (0x48, 0x58, 0x68):
value = self._read_ea(ea, size)
if base == 0x48:
self.cpu.sr |= value
elif base == 0x58:
self.cpu.sr &= value
else:
self.cpu.sr ^= value
elif base == 0x80:
value = self._read_ea(ea, size)
self._reg_write(rd, value, size)
self._set_logic_flags(value, size)
elif base == 0x90:
self._write_ea(ea, self._reg_read(rd, size), size)
elif base == 0x20:
lhs = self._reg_read(rd, size)
rhs = self._read_ea(ea, size)
result = lhs + rhs
self._reg_write(rd, result, size)
self._set_add_flags(lhs, rhs, result, size)
elif base == 0x30:
lhs = self._reg_read(rd, size)
rhs = self._read_ea(ea, size)
result = lhs - rhs
self._reg_write(rd, result, size)
self._set_sub_flags(lhs, rhs, result, size)
elif base == 0x50:
result = self._reg_read(rd, size) & self._read_ea(ea, size)
self._reg_write(rd, result, size)
self._set_logic_flags(result, size)
elif base == 0x40:
result = self._reg_read(rd, size) | self._read_ea(ea, size)
self._reg_write(rd, result, size)
self._set_logic_flags(result, size)
elif base == 0x60:
result = self._reg_read(rd, size) ^ self._read_ea(ea, size)
self._reg_write(rd, result, size)
self._set_logic_flags(result, size)
elif base == 0x70:
self._cmp(self._reg_read(rd, size), self._read_ea(ea, size), size)
elif base == 0xA8:
if size != 1:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
result = (self._reg_read(rd, 1) * self._read_ea(ea, 1)) & 0xFFFF
self.cpu.regs[rd] = result
self._set_logic_flags(result, 2)
self.cpu.c = False
elif op in (0x08, 0x09, 0x0C, 0x0D):
delta = {0x08: 1, 0x09: 2, 0x0C: -1, 0x0D: -2}[op]
old = self._read_ea(ea, size)
result = old + delta
self._write_ea(ea, result, size)
if delta >= 0:
self._set_add_flags(old, delta, result, size)
else:
self._set_sub_flags(old, -delta, result, size)
elif op == 0x13:
self._write_ea(ea, 0, size)
self._set_logic_flags(0, size)
elif op == 0x15:
result = (~self._read_ea(ea, size)) & mask(size)
self._write_ea(ea, result, size)
self._set_logic_flags(result, size)
elif op == 0x16:
self._set_logic_flags(self._read_ea(ea, size), size)
elif op in (0x10, 0x11, 0x12) and ea["mode"] == "reg" and size == 1:
reg = int(ea["reg"])
value = self.cpu.regs[reg] & 0xFFFF
if op == 0x10:
result = ((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF)
elif op == 0x11:
result = s8(value & 0xFF) & 0xFFFF
else:
result = value & 0x00FF
self.cpu.regs[reg] = result
self._set_logic_flags(result, 2)
elif op == 0x1A:
value = self._read_ea(ea, size)
result = (value << 1) & mask(size)
self.cpu.c = bool(value & sign_bit(size))
self._write_ea(ea, result, size)
self._set_logic_flags(result, size)
elif op == 0x1B:
value = self._read_ea(ea, size)
result = value >> 1
self.cpu.c = bool(value & 1)
self._write_ea(ea, result, size)
self._set_logic_flags(result, size)
elif 0xC0 <= op <= 0xFF:
bit = op & 0x0F
self._bit_operation(ea, size, op & 0xF0, bit)
elif base in (0x48, 0x58, 0x68, 0x78):
bit = self._reg_read(rd, 1) & 0x0F
self._bit_operation(ea, size, base + 0x80, bit)
elif base == 0x88:
value = self._read_ea(ea, size)
if size == 2 and rd == 0:
self.cpu.sr = value & 0xFFFF
elif size == 1 and rd == 1:
self.cpu.br = value & 0xFF
else:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
elif base == 0x98:
if size == 2 and rd == 0:
self._write_ea(ea, self.cpu.sr, 2)
elif size == 1 and rd == 1:
self._write_ea(ea, self.cpu.br, 1)
else:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
elif op in (0x04, 0x05):
compare_size = 1 if op == 0x04 else 2
immediate = raw[-1] if op == 0x04 else int.from_bytes(raw[-2:], "big")
self._cmp(self._read_ea(ea, compare_size), immediate, compare_size)
else:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
return next_pc
def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int:
cond = raw[0] & 0x0F
disp = s8(raw[1])
target = (pc + 2 + disp) & 0xFFFF
if self._branch_condition(cond):
return target
return next_pc
def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int:
cond = raw[0] & 0x0F
disp = s16(int.from_bytes(raw[1:3], "big"))
target = (pc + 3 + disp) & 0xFFFF
if self._branch_condition(cond):
return target
return next_pc
def _rough_cycles(self, raw: bytes) -> int:
if raw[0] in (0x15, 0x1D):
return 9
if raw[0] in (0x0E, 0x1E, 0x18):
return 14
if raw[0] in (0x19, 0x0A):
return 12
if raw[0] in range(0x20, 0x40):
return 8 if (raw[0] & 0x0F) == 0 else 4
return 3
def _direct_call(self, raw: bytes, next_pc: int) -> int:
self._push16(next_pc)
if raw[0] == 0x0E:
return (next_pc + s8(raw[1])) & 0xFFFF
if raw[0] == 0x1E:
return (next_pc + s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF
return int.from_bytes(raw[1:3], "big")
def _indirect_jump_call(self, raw: bytes, pc: int, next_pc: int) -> int:
op = raw[1]
if 0xC0 <= op <= 0xDF:
target = self.cpu.regs[op & 0x07] & 0xFFFF
elif 0xE0 <= op <= 0xEF and len(raw) >= 3:
target = (self.cpu.regs[op & 0x07] + s8(raw[2])) & 0xFFFF
elif 0xF0 <= op <= 0xFF and len(raw) >= 4:
target = (self.cpu.regs[op & 0x07] + s16(int.from_bytes(raw[2:4], "big"))) & 0xFFFF
else:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
if 0xC8 <= op <= 0xCF or 0xD8 <= op <= 0xDF or 0xE8 <= op <= 0xEF or 0xF8 <= op <= 0xFF:
self._push16(next_pc)
return target
def _scb(self, raw: bytes, pc: int, next_pc: int) -> int:
reg = raw[1] & 0x07
condition = {0x01: False, 0x06: not self.cpu.z, 0x07: self.cpu.z}[raw[0]]
if condition:
return next_pc
value = (self.cpu.regs[reg] - 1) & 0xFFFF
self.cpu.regs[reg] = value
self.cpu.z = value == 0
if value != 0:
return (pc + 3 + s8(raw[2])) & 0xFFFF
return next_pc
def _tick_peripherals(self) -> None:
self.sci1.tick()
self._interval_counter += 1
self._frt1_ocia_counter += 1
self._frt2_ocia_counter += 1
self._service_pending_interrupt()
def _service_pending_interrupt(self) -> None:
if self.cpu.interrupt_depth:
return
candidates: list[tuple[int, int, str]] = []
sci1_rx_interrupts_enabled = bool(self.sci1.scr & SCI_SCR_RIE and self.sci1.scr & SCI_SCR_RE)
if sci1_rx_interrupts_enabled and self.sci1.ssr & (SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER):
target = self._vector_target(VECTOR_SCI1_ERI)
if target is not None:
candidates.append((self._sci1_priority(), target, "sci1_eri"))
if sci1_rx_interrupts_enabled and self.sci1.ssr & SCI_SSR_RDRF:
target = self._vector_target(VECTOR_SCI1_RXI)
if target is not None:
candidates.append((self._sci1_priority(), target, "sci1_rxi"))
if self.sci1.scr & SCI_SCR_TIE and self.sci1.ssr & SCI_SSR_TDRE:
target = self._vector_target(VECTOR_SCI1_TXI)
if target is not None:
candidates.append((self._sci1_priority(), target, "sci1_txi"))
if self._interval_counter >= self.interval_steps:
target = self._vector_target(VECTOR_INTERVAL_TIMER)
if target is not None:
candidates.append((self._interval_priority(), target, "interval_timer"))
if self._frt1_ocia_pending():
target = self._vector_target(VECTOR_FRT1_OCIA)
if target is not None:
candidates.append((self._frt1_priority(), target, "frt1_ocia"))
if self._frt2_ocia_pending():
target = self._vector_target(VECTOR_FRT2_OCIA)
if target is not None:
candidates.append((self._frt2_priority(), target, "frt2_ocia"))
if not candidates:
return
priority, target, source = max(candidates, key=lambda item: item[0])
if priority <= self._interrupt_mask():
return
if source == "interval_timer":
self._interval_counter = 0
elif source == "frt1_ocia":
self._frt1_ocia_counter = 0
self.memory.write8(FRT1_TCSR, self.memory.read8(FRT1_TCSR) | FRT_TCSR_OCFA)
elif source == "frt2_ocia":
self._frt2_ocia_counter = 0
self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA)
self._enter_interrupt(target)
def _enter_interrupt(self, target: int) -> None:
self._push16(self.cpu.sr)
self._push16(self.cpu.pc)
self.cpu.pc = target & 0xFFFF
self.cpu.interrupt_depth += 1
def _return_from_interrupt(self) -> int:
pc = self._pop16()
self.cpu.sr = self._pop16()
if self.cpu.interrupt_depth:
self.cpu.interrupt_depth -= 1
return pc
def _vector_target(self, vector_address: int) -> int | None:
entry = self.vectors.get(vector_address)
return entry[1] if entry else None
def _interrupt_mask(self) -> int:
return (self.cpu.sr >> 8) & 0x07
def _sci1_priority(self) -> int:
return (self.memory.read8(IPRE) >> 4) & 0x07
def _interval_priority(self) -> int:
return (self.memory.read8(IPRA) >> 4) & 0x07
def _frt1_ocia_pending(self) -> bool:
if self._frt1_ocia_counter < self.frt1_ocia_steps:
return False
return bool(self.memory.read8(FRT1_TCR) & FRT_TCR_OCIEA)
def _frt1_priority(self) -> int:
return (self.memory.read8(IPRC) >> 4) & 0x07
def _frt2_ocia_pending(self) -> bool:
if self._frt2_ocia_counter < self.frt2_ocia_steps:
return False
return bool(self.memory.read8(FRT2_TCR) & FRT_TCR_OCIEA)
def _frt2_priority(self) -> int:
# H8/536 IPRC assigns bits 6..4 to FRT1 and bits 2..0 to FRT2;
# the ROM's IPRC=H'66 therefore gives both timers priority 6.
return self.memory.read8(IPRC) & 0x07
def _push16(self, value: int) -> None:
sp = (self.cpu.regs[7] - 2) & 0xFFFF
self.cpu.regs[7] = sp
self.memory.write16(sp, value)
def _pop16(self) -> int:
sp = self.cpu.regs[7] & 0xFFFF
value = self.memory.read16(sp)
self.cpu.regs[7] = (sp + 2) & 0xFFFF
return value
def _push_register_mask(self, mask_value: int) -> None:
for reg in range(8):
if mask_value & (1 << reg):
self._push16(self.cpu.regs[reg])
def _pop_register_mask(self, mask_value: int) -> None:
for reg in reversed(range(8)):
if mask_value & (1 << reg):
self.cpu.regs[reg] = self._pop16()
def _decode_ea(self, pc: int) -> dict[str, int | str | None]:
first = self.memory.rom.u8(pc)
if 0xA0 <= first <= 0xAF:
return {"mode": "reg", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
if 0xB0 <= first <= 0xBF:
return {"mode": "predec", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
if 0xC0 <= first <= 0xCF:
return {"mode": "postinc", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
if 0xD0 <= first <= 0xDF:
return {"mode": "indirect", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
if 0xE0 <= first <= 0xEF:
return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 2, "value": s8(self.memory.rom.u8(pc + 1))}
if 0xF0 <= first <= 0xFF:
return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 3, "value": s16(self.memory.rom.u16(pc + 1))}
if first in (0x04, 0x0C):
size = 2 if first == 0x0C else 1
value = self.memory.rom.u16(pc + 1) if size == 2 else self.memory.rom.u8(pc + 1)
return {"mode": "imm", "reg": None, "size": size, "length": 3 if size == 2 else 2, "value": value}
if first in (0x15, 0x1D):
return {"mode": "abs16", "reg": None, "size": 2 if first == 0x1D else 1, "length": 3, "value": self.memory.rom.u16(pc + 1)}
raise UnsupportedInstruction(pc, self.memory.rom.slice(pc, 1), H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
def _general_length(self, pc: int, ea: dict[str, int | str | None], op: int) -> int:
length = int(ea["length"]) + 1
if op == 0x00:
return length + 1
if op == 0x06:
return length + 1
if op in (0x07, 0x05):
return length + 2
if op == 0x04:
return length + 1
return length
def _ea_address(self, ea: dict[str, int | str | None], size: int, *, write: bool = False) -> int:
mode = ea["mode"]
reg = ea.get("reg")
if mode == "abs16":
return int(ea["value"]) & 0xFFFF
if mode == "indirect":
return self.cpu.regs[int(reg)] & 0xFFFF
if mode == "disp":
return (self.cpu.regs[int(reg)] + int(ea["value"])) & 0xFFFF
if mode == "predec":
self.cpu.regs[int(reg)] = (self.cpu.regs[int(reg)] - size) & 0xFFFF
return self.cpu.regs[int(reg)]
if mode == "postinc":
address = self.cpu.regs[int(reg)] & 0xFFFF
if not write:
self.cpu.regs[int(reg)] = (self.cpu.regs[int(reg)] + size) & 0xFFFF
return address
raise EmulatorError(f"EA mode {mode} does not have an address")
def _read_ea(self, ea: dict[str, int | str | None], size: int) -> int:
mode = ea["mode"]
if mode == "imm":
return int(ea["value"]) & mask(size)
if mode == "reg":
return self._reg_read(int(ea["reg"]), size)
address = self._ea_address(ea, size)
return self.memory.read16(address) if size == 2 else self.memory.read8(address)
def _write_ea(self, ea: dict[str, int | str | None], value: int, size: int) -> None:
if ea["mode"] == "reg":
self._reg_write(int(ea["reg"]), value, size)
return
address = self._ea_address(ea, size, write=True)
if size == 2:
self.memory.write16(address, value)
else:
self.memory.write8(address, value)
def _reg_read(self, reg: int, size: int) -> int:
value = self.cpu.regs[reg] & 0xFFFF
return value if size == 2 else value & 0xFF
def _reg_write(self, reg: int, value: int, size: int) -> None:
if size == 2:
self.cpu.regs[reg] = value & 0xFFFF
else:
self.cpu.regs[reg] = (self.cpu.regs[reg] & 0xFF00) | (value & 0xFF)
def _cmp(self, lhs: int, rhs: int, size: int) -> None:
self._set_sub_flags(lhs, rhs, lhs - rhs, size)
def _set_logic_flags(self, value: int, size: int) -> None:
value &= mask(size)
self.cpu.z = value == 0
self.cpu.n = bool(value & sign_bit(size))
self.cpu.v = False
def _set_add_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
max_value = mask(size)
sign = sign_bit(size)
result &= max_value
lhs &= max_value
rhs &= max_value
self.cpu.z = result == 0
self.cpu.n = bool(result & sign)
self.cpu.c = lhs + rhs > max_value
self.cpu.v = bool((~(lhs ^ rhs) & (lhs ^ result) & sign) != 0)
def _set_sub_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
max_value = mask(size)
sign = sign_bit(size)
result &= max_value
lhs &= max_value
rhs &= max_value
self.cpu.z = result == 0
self.cpu.n = bool(result & sign)
self.cpu.c = lhs < rhs
self.cpu.v = bool(((lhs ^ rhs) & (lhs ^ result) & sign) != 0)
def _bit_operation(self, ea: dict[str, int | str | None], size: int, op_base: int, bit: int) -> None:
value = self._read_ea(ea, size)
bit &= 15 if size == 2 else 7
bit_mask = 1 << bit
self.cpu.z = not bool(value & bit_mask)
if op_base == 0xC0:
self._write_ea(ea, value | bit_mask, size)
elif op_base == 0xD0:
self._write_ea(ea, value & ~bit_mask, size)
elif op_base == 0xE0:
self._write_ea(ea, value ^ bit_mask, size)
def _branch_condition(self, cond: int) -> bool:
if cond == 0x0:
return True
if cond == 0x1:
return False
if cond == 0x2:
return not self.cpu.c and not self.cpu.z
if cond == 0x3:
return self.cpu.c or self.cpu.z
if cond == 0x4:
return not self.cpu.c
if cond == 0x5:
return self.cpu.c
if cond == 0x6:
return not self.cpu.z
if cond == 0x7:
return self.cpu.z
if cond == 0x8:
return not self.cpu.v
if cond == 0x9:
return self.cpu.v
if cond == 0xA:
return not self.cpu.n
if cond == 0xB:
return self.cpu.n
if cond == 0xC:
return self.cpu.n == self.cpu.v
if cond == 0xD:
return self.cpu.n != self.cpu.v
if cond == 0xE:
return not self.cpu.z and self.cpu.n == self.cpu.v
return self.cpu.z or self.cpu.n != self.cpu.v

475
h8536/emulator/rx_probe.py Normal file
View File

@@ -0,0 +1,475 @@
from __future__ import annotations
import argparse
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Iterable
from ..formatting import h16, parse_int
from .cli import load_rom
from .constants import (
IPRE,
SCI_SCR_RE,
SCI_SCR_RIE,
SCI_SSR_RDRF,
VECTOR_SCI1_RXI,
)
from .errors import UnsupportedInstruction
from .memory import MemoryAccess
from .runner import H8536Emulator
CHECKSUM_SEED = 0x5A
FRAME_LENGTH = 6
CONNECT_LCD_FRAMES = (
bytes.fromhex("04000040001E"),
bytes.fromhex("0400008000DE"),
bytes.fromhex("040000C0009E"),
)
WATCH_PCS = {
0xBB57: "sci1_eri_entry",
0xBB67: "sci1_rxi_entry",
0xBBD6: "rx_checksum_seed",
0xBBF0: "rx_checksum_compare",
0xBC08: "command_dispatch",
0xBD0E: "command_04_handler",
0xBCCD: "command_04_send",
0xBE05: "command_07_handler",
0xBE29: "rx_error_or_retry",
0xBA26: "tx_builder",
0xBA72: "tx_first_byte",
0xBA84: "txi_entry",
0x3ECC: "lcd_line_buffer_entry",
0x3F28: "lcd_driver_stage",
0x3F40: "lcd_port_writer",
}
WATCH_RANGES = (
(0x2CA6, 0x2D20, "connect_display_window"),
(0xBB57, 0xBE6F, "sci1_rx_command_window"),
)
ACCESS_RANGES = (
(0xF850, 0xF85D, "tx_staging_or_frame"),
(0xF860, 0xF86D, "rx_validation_or_capture"),
(0xF870, 0xF96F, "report_queue"),
(0xF970, 0xF9AF, "secondary_dispatch_or_table"),
(0xF9B0, 0xF9C8, "serial_gate_state"),
(0xFAA2, 0xFAA6, "serial_latches"),
(0xFAF0, 0xFAFF, "lcd_line_buffer"),
(0xE000, 0xE001, "primary_table_index_0000"),
(0xE400, 0xE401, "secondary_table_index_0000"),
(0xE800, 0xE801, "current_table_index_0000"),
(0xEC00, 0xEC01, "flag_table_index_0000"),
(0xF200, 0xF201, "lcd_ports"),
)
STATE_BYTES = {
0xF9B0: "queue_head",
0xF9B5: "queue_tail",
0xF9C0: "tx_gate",
0xF9C1: "rx_interbyte_timeout",
0xF9C3: "rx_index",
0xF9C4: "idle_heartbeat_gate",
0xF9C5: "rx_session_timeout",
0xF9C6: "resend_period_hi",
0xF9C7: "resend_period_lo",
0xF9C8: "resend_countdown",
0xFAA2: "session_flags",
0xFAA3: "pending_mask",
0xFAA4: "rx_error_latch",
0xFAA5: "retry_or_gate_flags",
0xFAA6: "retry_counter",
}
STATE_WORDS = {
0xE000: "E000_index_0000_primary",
0xE400: "E400_index_0000_secondary",
0xE800: "E800_index_0000_current",
0xF860: "rx_frame_01",
0xF862: "rx_frame_23",
0xF864: "rx_frame_45",
0xF970: "F970_selector_zero_dispatch",
}
@dataclass
class RunContext:
pc_hits: Counter[str] = field(default_factory=Counter)
first_pcs: list[tuple[int, str]] = field(default_factory=list)
unsupported: str | None = None
def record_pc(self, pc: int) -> None:
label = WATCH_PCS.get(pc)
if label is None:
for start, end, range_label in WATCH_RANGES:
if start <= pc <= end:
label = range_label
break
if label is None:
return
self.pc_hits[label] += 1
if len(self.first_pcs) < 32:
self.first_pcs.append((pc, label))
@dataclass(frozen=True)
class FrameResult:
input_frame: bytes
checksum_ok: bool
steps: int
stopped_reason: str
new_tx_bytes: bytes
new_tx_frames: list[bytes]
state_before: dict[str, int | str]
state_after: dict[str, int | str]
accesses: list[MemoryAccess]
context: RunContext
def lines(self, index: int) -> list[str]:
lines = [
f"host_frame[{index}]={format_frame(self.input_frame)} checksum_ok={int(self.checksum_ok)}",
f" stopped={self.stopped_reason} steps={self.steps}",
f" new_tx_bytes={format_frame(self.new_tx_bytes) if self.new_tx_bytes else 'none'}",
]
if self.new_tx_frames:
lines.append(" new_tx_frames=" + " | ".join(format_frame(frame) for frame in self.new_tx_frames))
else:
lines.append(" new_tx_frames=none")
lcd_display = self.state_after.get("lcd_display_ascii")
if isinstance(lcd_display, str):
lines.append(f" lcd_display={lcd_display!r}")
state_changes = _state_change_lines(self.state_before, self.state_after)
if state_changes:
lines.append(" state_changes:")
lines.extend(f" {line}" for line in state_changes)
pc_lines = _pc_hit_lines(self.context)
if pc_lines:
lines.append(" pc_hits:")
lines.extend(f" {line}" for line in pc_lines)
access_lines = _access_lines(self.accesses)
if access_lines:
lines.append(" interesting_accesses:")
lines.extend(f" {line}" for line in access_lines)
if self.context.unsupported:
lines.append(f" unsupported={self.context.unsupported}")
return lines
def parse_frame(text: str) -> bytes:
normalized = text.strip().replace(",", " ").replace(":", " ").replace("-", " ").replace("_", " ")
parts = normalized.split()
if len(parts) == 1:
compact = parts[0]
if compact.lower().startswith("0x"):
compact = compact[2:]
if compact.upper().startswith("H'"):
compact = compact[2:]
if len(compact) % 2:
raise argparse.ArgumentTypeError("frame compact hex must have an even number of digits")
parts = [compact[index : index + 2] for index in range(0, len(compact), 2)]
values = [_parse_byte(part) for part in parts]
if len(values) == FRAME_LENGTH - 1:
values.append(frame_checksum(bytes(values)))
if len(values) != FRAME_LENGTH:
raise argparse.ArgumentTypeError("frame must contain 5 bytes plus computed checksum or exactly 6 bytes")
return bytes(values)
def frame_checksum(data: bytes) -> int:
checksum = CHECKSUM_SEED
for value in data[: FRAME_LENGTH - 1]:
checksum ^= value
return checksum & 0xFF
def frame_checksum_ok(frame: bytes) -> bool:
return len(frame) == FRAME_LENGTH and frame_checksum(frame) == frame[-1]
def format_frame(data: bytes) -> str:
return data.hex(" ").upper()
def run_rx_probe(
frames: Iterable[bytes],
*,
rom_path: Path | None = None,
boot_steps: int = 250_000,
per_byte_steps: int = 5_000,
post_frame_steps: int = 80_000,
interval_steps: int = 512,
frt1_ocia_steps: int = 512,
frt2_ocia_steps: int = 512,
p9_fast_path: bool = True,
p9_fast_input: int = 0xFF,
stop_after_tx_frame: bool = True,
) -> tuple[Path, H8536Emulator, str, list[FrameResult]]:
rom_bytes, discovered_rom_path = load_rom(rom_path)
emulator = H8536Emulator(
rom_bytes,
interval_steps=interval_steps,
frt1_ocia_steps=frt1_ocia_steps,
frt2_ocia_steps=frt2_ocia_steps,
p9_fast_path_enabled=p9_fast_path,
p9_fast_default_input_byte=p9_fast_input,
)
boot_context = RunContext()
boot_steps_used, boot_reason = _run_until(emulator, boot_steps, _rx_ready, boot_context)
boot_summary = (
f"boot={boot_reason} steps={boot_steps_used} pc={h16(emulator.cpu.pc)} "
f"SCR={emulator.sci1.scr:02X} SSR={emulator.sci1.ssr:02X} "
f"rx_serviceable={int(_rx_ready(emulator))} "
f"lcd_display={emulator.memory.lcd.display_text(lines=4)!r}"
)
results = [
_run_frame(
emulator,
frame,
per_byte_steps=per_byte_steps,
post_frame_steps=post_frame_steps,
stop_after_tx_frame=stop_after_tx_frame,
)
for frame in frames
]
return discovered_rom_path, emulator, boot_summary, results
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Inject host SCI1 frames into the H8/536 emulator and listen for ROM TX responses.")
parser.add_argument("frames", nargs="*", type=parse_frame, help="host frame hex; 5-byte inputs get a 0x5A-XOR checksum appended")
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present")
parser.add_argument("--preset", choices=("connect-lcd",), help="append a built-in host-frame set")
parser.add_argument("--boot-steps", type=int, default=250_000, help="maximum steps to boot until SCI1 RXI is serviceable")
parser.add_argument("--per-byte-steps", type=int, default=5_000, help="maximum steps after each injected RX byte")
parser.add_argument("--post-frame-steps", type=int, default=80_000, help="maximum steps after a full injected frame")
parser.add_argument("--keep-listening", action="store_true", help="use all post-frame steps instead of stopping at the first new TX frame")
parser.add_argument("--interval-steps", type=int, default=512, help="rough step period for the scaffolded interval timer interrupt")
parser.add_argument("--frt1-ocia-steps", type=int, default=512, help="rough step period for the scaffolded FRT1 OCIA interrupt")
parser.add_argument("--frt2-ocia-steps", type=int, default=512, help="rough step period for the scaffolded FRT2 OCIA interrupt")
parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_arg_parser().parse_args(argv)
frames = list(args.frames)
if args.preset == "connect-lcd":
frames.extend(CONNECT_LCD_FRAMES)
if not frames:
raise SystemExit("pass at least one frame or use --preset connect-lcd")
rom_path, emulator, boot_summary, results = run_rx_probe(
frames,
rom_path=args.rom,
boot_steps=args.boot_steps,
per_byte_steps=args.per_byte_steps,
post_frame_steps=args.post_frame_steps,
interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps,
p9_fast_path=not args.no_p9_fast_path,
p9_fast_input=args.p9_fast_input,
stop_after_tx_frame=not args.keep_listening,
)
print(f"rom={rom_path}")
print(f"reset_vector={h16(emulator.reset_vector())}")
print(boot_summary)
for index, result in enumerate(results):
for line in result.lines(index):
print(line)
print("total_tx_frames=" + " | ".join(format_frame(frame) for frame in emulator.sci1.tx_frames))
return 0
def _run_frame(
emulator: H8536Emulator,
frame: bytes,
*,
per_byte_steps: int,
post_frame_steps: int,
stop_after_tx_frame: bool,
) -> FrameResult:
state_before = _state_snapshot(emulator)
log_start = len(emulator.memory.access_log)
tx_byte_start = len(emulator.sci1.tx_bytes)
tx_frame_start = len(emulator.sci1.tx_frames)
context = RunContext()
stopped_reason = "post_frame_steps"
steps_total = 0
for offset, value in enumerate(frame):
emulator.inject_sci1_rx_byte(value)
steps, reason = _run_until(emulator, per_byte_steps, _rx_byte_consumed, context)
steps_total += steps
if reason != "predicate":
stopped_reason = f"rx_byte_{offset}_{reason}"
break
else:
target_frame_count = tx_frame_start + 1
def post_predicate(inner: H8536Emulator) -> bool:
return stop_after_tx_frame and len(inner.sci1.tx_frames) >= target_frame_count
steps, reason = _run_until(emulator, post_frame_steps, post_predicate, context)
steps_total += steps
stopped_reason = "tx_frame" if reason == "predicate" and stop_after_tx_frame else reason
log_end = len(emulator.memory.access_log)
state_after = _state_snapshot(emulator)
return FrameResult(
input_frame=frame,
checksum_ok=frame_checksum_ok(frame),
steps=steps_total,
stopped_reason=stopped_reason,
new_tx_bytes=bytes(emulator.sci1.tx_bytes[tx_byte_start:]),
new_tx_frames=list(emulator.sci1.tx_frames[tx_frame_start:]),
state_before=state_before,
state_after=state_after,
accesses=emulator.memory.access_log[log_start:log_end],
context=context,
)
def _run_until(
emulator: H8536Emulator,
max_steps: int,
predicate: Callable[[H8536Emulator], bool],
context: RunContext,
) -> tuple[int, str]:
for index in range(max_steps):
if predicate(emulator):
return index, "predicate"
pc = emulator.cpu.pc
context.record_pc(pc)
try:
emulator.step()
except UnsupportedInstruction as exc:
context.unsupported = str(exc)
return index, "unsupported_instruction"
return max_steps, "max_steps"
def _rx_ready(emulator: H8536Emulator) -> bool:
if not (emulator.sci1.scr & SCI_SCR_RIE and emulator.sci1.scr & SCI_SCR_RE):
return False
if emulator.vectors.get(VECTOR_SCI1_RXI) is None:
return False
return _sci1_priority(emulator) > _interrupt_mask(emulator)
def _rx_byte_consumed(emulator: H8536Emulator) -> bool:
return not (emulator.sci1.ssr & SCI_SSR_RDRF) and emulator.cpu.interrupt_depth == 0
def _sci1_priority(emulator: H8536Emulator) -> int:
return (emulator.memory.read8(IPRE) >> 4) & 0x07
def _interrupt_mask(emulator: H8536Emulator) -> int:
return (emulator.cpu.sr >> 8) & 0x07
def _state_snapshot(emulator: H8536Emulator) -> dict[str, int | str]:
snapshot: dict[str, int | str] = {}
for address, name in STATE_BYTES.items():
snapshot[name] = emulator.memory.read8(address)
for address, name in STATE_WORDS.items():
snapshot[name] = emulator.memory.read16(address)
snapshot["lcd_line_buffer_ascii"] = _ascii_window(emulator, 0xFAF0, 16)
snapshot["lcd_display_ascii"] = emulator.memory.lcd.display_text(lines=4, width=16)
snapshot["tx_frame_staging"] = format_frame(bytes(emulator.memory.read8(0xF850 + offset) for offset in range(6)))
snapshot["rx_frame_validation"] = format_frame(bytes(emulator.memory.read8(0xF860 + offset) for offset in range(6)))
return snapshot
def _ascii_window(emulator: H8536Emulator, start: int, length: int) -> str:
chars = []
for offset in range(length):
value = emulator.memory.read8(start + offset)
chars.append(chr(value) if 0x20 <= value <= 0x7E else ".")
return "".join(chars)
def _state_change_lines(before: dict[str, int | str], after: dict[str, int | str]) -> list[str]:
lines = []
for key in sorted(after):
if before.get(key) == after[key]:
continue
old = _state_value(before.get(key))
new = _state_value(after[key])
lines.append(f"{key}: {old}->{new}")
return lines
def _state_value(value: int | str | None) -> str:
if isinstance(value, int):
return f"H'{value:04X}" if value > 0xFF else f"H'{value:02X}"
return repr(value)
def _pc_hit_lines(context: RunContext) -> list[str]:
lines = [f"{name}={count}" for name, count in sorted(context.pc_hits.items())]
if context.first_pcs:
first = ", ".join(f"{h16(pc)}:{label}" for pc, label in context.first_pcs[:16])
lines.append(f"first={first}")
return lines
def _access_lines(accesses: list[MemoryAccess]) -> list[str]:
interesting = [access for access in accesses if _interesting_access(access)]
lines = []
for access in interesting[:80]:
label = _access_label(access.address)
lines.append(f"{access.kind:<5} {h16(access.address)} {access.value:02X} {label}")
if len(interesting) > 80:
lines.append(f"... {len(interesting) - 80} more interesting accesses")
return lines
def _interesting_access(access: MemoryAccess) -> bool:
if access.kind == "write":
return _access_label(access.address) != ""
return _access_label(access.address) in {"secondary_dispatch_or_table", "lcd_ports"}
def _access_label(address: int) -> str:
for start, end, label in ACCESS_RANGES:
if start <= address <= end:
return label
return ""
def _parse_byte(text: str) -> int:
token = text.strip()
if token.lower().startswith("0x"):
token = token[2:]
if token.upper().startswith("H'"):
token = token[2:]
if not token or len(token) > 2:
raise argparse.ArgumentTypeError(f"invalid byte token {text!r}")
try:
value = int(token, 16)
except ValueError as exc:
raise argparse.ArgumentTypeError(f"invalid byte token {text!r}") from exc
if not 0 <= value <= 0xFF:
raise argparse.ArgumentTypeError(f"byte out of range {text!r}")
return value
__all__ = [
"CONNECT_LCD_FRAMES",
"format_frame",
"frame_checksum",
"frame_checksum_ok",
"main",
"parse_frame",
"run_rx_probe",
]

119
h8536/emulator/sci.py Normal file
View File

@@ -0,0 +1,119 @@
from __future__ import annotations
from dataclasses import dataclass, field
from .constants import (
HEARTBEAT_FRAME,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
SCI_SCR_TE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
)
@dataclass
class SciTxEvent:
address: int
value: int
scr: int
ssr: int
emitted: bool
@dataclass
class SCI1:
"""Small SCI1 model for the H8/536 serial path.
Manual anchors:
- RDR/TDR/SCR/SSR live at H'FEDD/H'FEDB/H'FEDA/H'FEDC for SCI1.
- SCR bit 7 TIE, bit 6 RIE, bit 5 TE, bit 4 RE.
- SSR bit 7 TDRE, bit 6 RDRF, bits 5..3 ORER/FER/PER.
- Software normally writes TDR after TDRE=1, then clears SSR.TDRE.
- SSR status flags are R/(W)*: writing 0 clears a latched flag, while
writing 1 leaves the hardware-owned flag unchanged.
"""
smr: int = 0x00
brr: int = 0xFF
scr: int = 0x0C
tdr: int = 0xFF
ssr: int = 0x87
rdr: int = 0x00
tx_bytes: list[int] = field(default_factory=list)
tx_events: list[SciTxEvent] = field(default_factory=list)
tx_frames: list[bytes] = field(default_factory=list)
_frame_buffer: bytearray = field(default_factory=bytearray)
tx_ready_delay: int = 0
tx_ready_ticks: int = 2
_tx_ready_pending: bool = False
def read(self, address: int) -> int:
if address == SCI1_SMR:
return self.smr
if address == SCI1_BRR:
return self.brr
if address == SCI1_SCR:
return self.scr
if address == SCI1_TDR:
return self.tdr
if address == SCI1_SSR:
return self.ssr
if address == SCI1_RDR:
return self.rdr
raise KeyError(address)
def write(self, address: int, value: int) -> None:
value &= 0xFF
if address == SCI1_SMR:
self.smr = value
elif address == SCI1_BRR:
self.brr = value
elif address == SCI1_SCR:
self.scr = value
elif address == SCI1_TDR:
self.tdr = value
self._write_tdr(value)
elif address == SCI1_SSR:
self._write_ssr(value)
elif address == SCI1_RDR:
self.rdr = value
else:
raise KeyError(address)
def _write_tdr(self, value: int) -> None:
emitted = bool(self.scr & SCI_SCR_TE)
if emitted:
self.tx_bytes.append(value)
self._frame_buffer.append(value)
if len(self._frame_buffer) == len(HEARTBEAT_FRAME):
self.tx_frames.append(bytes(self._frame_buffer))
self._frame_buffer.clear()
self.tx_ready_delay = max(0, self.tx_ready_ticks)
self._tx_ready_pending = True
self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted))
def _write_ssr(self, value: int) -> None:
writable_zero_flags = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER
self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags)
def inject_rx(self, value: int) -> None:
self.rdr = value & 0xFF
self.ssr |= SCI_SSR_RDRF
def saw_heartbeat(self) -> bool:
return HEARTBEAT_FRAME in self.tx_frames
def tick(self) -> None:
if self._tx_ready_pending and self.tx_ready_delay:
self.tx_ready_delay -= 1
if self._tx_ready_pending and self.tx_ready_delay == 0 and not (self.ssr & SCI_SSR_TDRE):
self.ssr |= SCI_SSR_TDRE
self._tx_ready_pending = False

535
h8536/protocol_capture.py Normal file
View File

@@ -0,0 +1,535 @@
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable, Mapping, TextIO
try: # Keep this module useful even when copied away from the decompiler tree.
from . import protocol_trace as _protocol_trace
except ImportError: # pragma: no cover - exercised only outside package imports.
_protocol_trace = None
CHECKSUM_SEED = getattr(_protocol_trace, "CHECKSUM_SEED", 0x5A)
FRAME_LENGTH = getattr(_protocol_trace, "FRAME_LENGTH", 6)
CAPTURE_LINE_RE = re.compile(
r"^\s*(?P<time>\d{1,2}:\d{2}:\d{2}(?:\.\d{1,6})?)\s+"
r"(?P<direction>RX|TX|FRAME)\s+"
r"(?P<count>\d+)(?:\s+bytes?)?\s+"
r"(?P<byte_text>.*?)\s*$",
re.IGNORECASE,
)
HEX_BYTE_RE = re.compile(r"\b[0-9A-Fa-f]{2}\b")
_FALLBACK_OBSERVED_TX_REPORT_CANDIDATES: dict[tuple[int, int], dict[str, str]] = {
(0x0000, 0x0080): {
"name_candidate": "heartbeat_alive_candidate",
},
(0x0015, 0x8000): {
"name_candidate": "call_button_candidate",
"state_candidate": "active",
},
(0x0015, 0x0000): {
"name_candidate": "call_button_candidate",
"state_candidate": "inactive",
},
(0x0007, 0x8000): {
"name_candidate": "cam_power_button_candidate",
"state_candidate": "active",
},
}
OBSERVED_TX_REPORT_CANDIDATES = getattr(
_protocol_trace,
"OBSERVED_TX_REPORT_CANDIDATES",
_FALLBACK_OBSERVED_TX_REPORT_CANDIDATES,
)
@dataclass(frozen=True)
class CaptureChunk:
chunk_index: int
timestamp: str
timestamp_ms: int
analyzer_direction: str
device_direction: str
declared_count: int
bytes: tuple[int, ...]
raw_line: str
def checksum_for(frame_prefix: Iterable[int]) -> int:
if _protocol_trace is not None and hasattr(_protocol_trace, "checksum_for"):
return int(_protocol_trace.checksum_for(frame_prefix))
value = CHECKSUM_SEED
for byte in frame_prefix:
value ^= byte & 0xFF
return value & 0xFF
def parse_capture_text(text: str) -> list[CaptureChunk]:
chunks: list[CaptureChunk] = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
match = CAPTURE_LINE_RE.match(line)
if not match:
continue
byte_values = tuple(int(token, 16) for token in HEX_BYTE_RE.findall(match.group("byte_text")))
raw_direction = match.group("direction").lower()
analyzer_direction = "rx" if raw_direction == "frame" else raw_direction
chunks.append(
CaptureChunk(
chunk_index=len(chunks),
timestamp=match.group("time"),
timestamp_ms=_timestamp_ms(match.group("time")),
analyzer_direction=analyzer_direction,
device_direction=_device_direction(analyzer_direction),
declared_count=int(match.group("count")),
bytes=byte_values,
raw_line=raw_line,
)
)
if len(byte_values) != int(match.group("count")):
# Preserve the chunk and expose the mismatch in analysis instead of dropping capture evidence.
continue
return chunks
def analyze_capture_text(text: str) -> dict[str, Any]:
return analyze_capture_chunks(parse_capture_text(text))
def analyze_capture_chunks(chunks: Iterable[CaptureChunk]) -> dict[str, Any]:
chunk_list = list(chunks)
frames = _recombine_frames(chunk_list)
groups = _repeated_groups(frames)
gate_session_hints = _gate_session_hints(frames)
return {
"kind": "h8536_protocol_capture",
"frame_length": FRAME_LENGTH,
"checksum_model": {
"algorithm": "xor",
"seed": CHECKSUM_SEED,
"seed_hex": _h8(CHECKSUM_SEED),
"covered_offsets": [0, 1, 2, 3, 4],
"checksum_offset": 5,
},
"chunks": [_chunk_dict(chunk) for chunk in chunk_list],
"chunk_count": len(chunk_list),
"frames": frames,
"frame_count": len(frames),
"repeated_groups": groups,
"repeated_group_count": len(groups),
"gate_session_hints": gate_session_hints,
"direction_note": (
"Capture RX is analyzer-perspective receive; these bytes are device-perspective TX."
),
}
def format_text_report(analysis: Mapping[str, Any]) -> str:
lines = [
"H8/536 capture log",
(
f"chunks={analysis.get('chunk_count', 0)} "
f"frames={analysis.get('frame_count', 0)} "
f"repeated_groups={analysis.get('repeated_group_count', 0)}"
),
]
for frame in analysis.get("frames", []):
label = ""
report = frame.get("report_candidate") or {}
candidate = report.get("observed_candidate") or {}
if candidate.get("name_candidate"):
label = f" {candidate['name_candidate']}"
if candidate.get("state_candidate"):
label += f" state={candidate['state_candidate']}"
split = " split" if frame.get("source_chunk_count", 0) > 1 else ""
lines.append(
(
f"[{frame['frame_index']:04d}] {frame['timestamp']} "
f"{frame['analyzer_direction'].upper()}=>device:{frame['device_direction']} "
f"bytes={' '.join(frame['bytes_hex'])} checksum=ok{split} "
f"index={report.get('index_hex')} value={report.get('value_hex')}{label}"
)
)
for group in analysis.get("repeated_groups", []):
cadence = group.get("cadence_ms") or {}
cadence_text = "n/a"
if cadence.get("average") is not None:
cadence_text = (
f"avg={cadence['average']:.1f}ms "
f"min={cadence['min']}ms max={cadence['max']}ms"
)
lines.append(
(
f"group {group['bytes']} count={group['count']} "
f"span={group['span_ms']}ms cadence={cadence_text}"
)
)
hints = analysis.get("gate_session_hints") or {}
names = hints.get("observed_autonomous_report_names") or []
if names:
lines.append("observed autonomous report candidates: " + ", ".join(names))
heartbeat = hints.get("heartbeat_cadence_ms") or {}
if heartbeat.get("count"):
cadence_text = "n/a"
if heartbeat.get("average") is not None:
cadence_text = (
f"avg={heartbeat['average']:.1f}ms "
f"min={heartbeat['min']}ms max={heartbeat['max']}ms"
)
lines.append(f"heartbeat cadence count={heartbeat['count']} cadence={cadence_text}")
for transition in hints.get("active_inactive_transitions", []):
lines.append(
(
f"transition index={transition['index_hex']} "
f"{transition['from_state']}->{transition['to_state']} "
f"{transition['from_timestamp']}..{transition['to_timestamp']}"
)
)
for interruption in hints.get("heartbeat_interruptions", []):
interrupted_names = ", ".join(
item["name_candidate"] for item in interruption.get("interrupted_by", [])
)
lines.append(
(
f"heartbeat gap {interruption['from_timestamp']}..{interruption['to_timestamp']} "
f"gap={interruption['gap_ms']}ms interrupted_by={interrupted_names}"
)
)
if hints.get("caveat"):
lines.append(f"caveat: {hints['caveat']}")
return "\n".join(lines)
def main(argv: list[str] | None = None, *, stdin: TextIO | None = None, stdout: TextIO | None = None) -> int:
parser = argparse.ArgumentParser(
description="Analyze timestamped H8/536 serial capture logs and recombine 6-byte frames."
)
parser.add_argument("input", nargs="?", help="Capture log path. Use '-' or omit to read stdin.")
parser.add_argument("--json", action="store_true", help="Emit JSON instead of text.")
args = parser.parse_args(argv)
stdin = stdin or sys.stdin
stdout = stdout or sys.stdout
if args.input and args.input != "-":
text = Path(args.input).read_text(encoding="utf-8")
else:
text = stdin.read()
analysis = analyze_capture_text(text)
if args.json:
json.dump(analysis, stdout, indent=2, sort_keys=True)
stdout.write("\n")
else:
stdout.write(format_text_report(analysis))
stdout.write("\n")
return 0
def _recombine_frames(chunks: list[CaptureChunk]) -> list[dict[str, Any]]:
buffers: dict[str, list[dict[str, Any]]] = {}
frames: list[dict[str, Any]] = []
for chunk in chunks:
key = chunk.analyzer_direction
stream = buffers.setdefault(key, [])
for offset, byte in enumerate(chunk.bytes):
stream.append({"byte": byte, "chunk": chunk, "offset": offset})
_drain_valid_frames(stream, frames)
return frames
def _drain_valid_frames(stream: list[dict[str, Any]], frames: list[dict[str, Any]]) -> None:
while len(stream) >= FRAME_LENGTH:
candidate = stream[:FRAME_LENGTH]
values = [int(item["byte"]) for item in candidate]
if checksum_for(values[:5]) == values[5]:
frames.append(_frame_dict(len(frames), candidate))
del stream[:FRAME_LENGTH]
continue
realigned = False
for start in range(1, len(stream) - FRAME_LENGTH + 1):
window = stream[start : start + FRAME_LENGTH]
values = [int(item["byte"]) for item in window]
if checksum_for(values[:5]) == values[5]:
del stream[:start]
realigned = True
break
if not realigned:
break
def _frame_dict(frame_index: int, items: list[dict[str, Any]]) -> dict[str, Any]:
values = [int(item["byte"]) for item in items]
chunks = [item["chunk"] for item in items]
first: CaptureChunk = chunks[0]
source_chunk_indexes = sorted({chunk.chunk_index for chunk in chunks})
return {
"frame_index": frame_index,
"timestamp": first.timestamp,
"timestamp_ms": first.timestamp_ms,
"analyzer_direction": first.analyzer_direction,
"device_direction": first.device_direction,
"bytes": values,
"bytes_hex": [_h8(value) for value in values],
"checksum": {
"valid": True,
"expected": values[5],
"expected_hex": _h8(values[5]),
"actual": values[5],
"actual_hex": _h8(values[5]),
},
"source_chunk_indexes": source_chunk_indexes,
"source_chunk_count": len(source_chunk_indexes),
"report_candidate": _tx_report_candidate(values),
}
def _tx_report_candidate(frame: list[int]) -> dict[str, Any]:
index = (frame[0] << 16) | (frame[1] << 8) | frame[2]
value = (frame[3] << 8) | frame[4]
candidate = OBSERVED_TX_REPORT_CANDIDATES.get((index, value))
return {
"encoding": "observed_tx_index_value_report_candidate",
"confidence": "observed_candidate" if candidate else "unknown",
"index": index,
"index_hex": f"0x{index:06X}" if index > 0xFFFF else _h16(index),
"value": value,
"value_hex": _h16(value),
"observed_candidate": dict(candidate) if candidate else None,
"caveat": "Observed TX report names are capture labels, not proven protocol facts.",
}
def _repeated_groups(frames: list[Mapping[str, Any]]) -> list[dict[str, Any]]:
by_bytes: dict[tuple[int, ...], list[Mapping[str, Any]]] = {}
for frame in frames:
by_bytes.setdefault(tuple(frame["bytes"]), []).append(frame)
groups: list[dict[str, Any]] = []
for values, members in by_bytes.items():
if len(members) < 2:
continue
timestamps = [int(member["timestamp_ms"]) for member in members]
deltas = [right - left for left, right in zip(timestamps, timestamps[1:])]
groups.append(
{
"bytes": " ".join(_h8(value) for value in values),
"count": len(members),
"frame_indexes": [member["frame_index"] for member in members],
"first_timestamp": members[0]["timestamp"],
"last_timestamp": members[-1]["timestamp"],
"span_ms": timestamps[-1] - timestamps[0],
"cadence_ms": {
"samples": deltas,
"average": (sum(deltas) / len(deltas)) if deltas else None,
"min": min(deltas) if deltas else None,
"max": max(deltas) if deltas else None,
},
}
)
return sorted(groups, key=lambda group: (-int(group["count"]), str(group["bytes"])))
def _gate_session_hints(frames: list[Mapping[str, Any]]) -> dict[str, Any]:
observed = [_observed_report_frame(frame) for frame in frames]
observed = [item for item in observed if item is not None]
by_name: dict[str, list[dict[str, Any]]] = {}
for item in observed:
by_name.setdefault(str(item["name_candidate"]), []).append(item)
observed_reports = []
for name, members in sorted(by_name.items()):
observed_reports.append(
{
"name_candidate": name,
"count": len(members),
"first_timestamp": members[0]["timestamp"],
"last_timestamp": members[-1]["timestamp"],
"frame_indexes": [member["frame_index"] for member in members],
"indexes_hex": sorted({str(member["index_hex"]) for member in members}),
"values_hex": sorted({str(member["value_hex"]) for member in members}),
"states": sorted(
{
str(member["state_candidate"])
for member in members
if member.get("state_candidate")
}
),
}
)
heartbeat_frames = [
item for item in observed if item.get("name_candidate") == "heartbeat_alive_candidate"
]
heartbeat_timestamps = [int(item["timestamp_ms"]) for item in heartbeat_frames]
heartbeat_deltas = [
right - left for left, right in zip(heartbeat_timestamps, heartbeat_timestamps[1:])
]
return {
"observed_autonomous_report_names": sorted(by_name),
"observed_reports": observed_reports,
"active_inactive_transitions": _active_inactive_transitions(observed),
"heartbeat_cadence_ms": {
"count": len(heartbeat_frames),
"samples": heartbeat_deltas,
"average": (sum(heartbeat_deltas) / len(heartbeat_deltas)) if heartbeat_deltas else None,
"min": min(heartbeat_deltas) if heartbeat_deltas else None,
"max": max(heartbeat_deltas) if heartbeat_deltas else None,
},
"heartbeat_interruptions": _heartbeat_interruptions(observed),
"caveat": (
"Missing autonomous reports for other controls may reflect host/session gating "
"or capture timing, not proof that local control state did not change."
),
"evidence_scope": "capture_side_observation_only",
}
def _observed_report_frame(frame: Mapping[str, Any]) -> dict[str, Any] | None:
report = frame.get("report_candidate") or {}
candidate = report.get("observed_candidate") or {}
name = candidate.get("name_candidate")
if not name:
return None
return {
"frame_index": frame.get("frame_index"),
"timestamp": frame.get("timestamp"),
"timestamp_ms": frame.get("timestamp_ms"),
"analyzer_direction": frame.get("analyzer_direction"),
"device_direction": frame.get("device_direction"),
"name_candidate": name,
"state_candidate": candidate.get("state_candidate"),
"index": report.get("index"),
"index_hex": report.get("index_hex"),
"value": report.get("value"),
"value_hex": report.get("value_hex"),
}
def _active_inactive_transitions(observed: list[Mapping[str, Any]]) -> list[dict[str, Any]]:
by_index: dict[int, list[Mapping[str, Any]]] = {}
for item in observed:
state = item.get("state_candidate")
index = item.get("index")
if state not in {"active", "inactive"} or not isinstance(index, int):
continue
by_index.setdefault(index, []).append(item)
transitions: list[dict[str, Any]] = []
for index, members in sorted(by_index.items()):
previous: Mapping[str, Any] | None = None
for member in sorted(members, key=lambda item: int(item.get("frame_index") or 0)):
if previous is not None and previous.get("state_candidate") != member.get("state_candidate"):
transitions.append(
{
"index": index,
"index_hex": member.get("index_hex"),
"name_candidate": member.get("name_candidate"),
"from_state": previous.get("state_candidate"),
"to_state": member.get("state_candidate"),
"from_timestamp": previous.get("timestamp"),
"to_timestamp": member.get("timestamp"),
"from_frame_index": previous.get("frame_index"),
"to_frame_index": member.get("frame_index"),
}
)
previous = member
return transitions
def _heartbeat_interruptions(observed: list[Mapping[str, Any]]) -> list[dict[str, Any]]:
interruptions: list[dict[str, Any]] = []
heartbeat_positions = [
index
for index, item in enumerate(observed)
if item.get("name_candidate") == "heartbeat_alive_candidate"
]
for left, right in zip(heartbeat_positions, heartbeat_positions[1:]):
between = [
item
for item in observed[left + 1 : right]
if item.get("name_candidate") != "heartbeat_alive_candidate"
]
if not between:
continue
start = observed[left]
end = observed[right]
interruptions.append(
{
"from_frame_index": start.get("frame_index"),
"to_frame_index": end.get("frame_index"),
"from_timestamp": start.get("timestamp"),
"to_timestamp": end.get("timestamp"),
"gap_ms": int(end.get("timestamp_ms") or 0) - int(start.get("timestamp_ms") or 0),
"interrupted_by": [
{
"frame_index": item.get("frame_index"),
"timestamp": item.get("timestamp"),
"name_candidate": item.get("name_candidate"),
"state_candidate": item.get("state_candidate"),
"index_hex": item.get("index_hex"),
"value_hex": item.get("value_hex"),
}
for item in between
],
}
)
return interruptions
def _chunk_dict(chunk: CaptureChunk) -> dict[str, Any]:
return {
"chunk_index": chunk.chunk_index,
"timestamp": chunk.timestamp,
"timestamp_ms": chunk.timestamp_ms,
"analyzer_direction": chunk.analyzer_direction,
"device_direction": chunk.device_direction,
"declared_count": chunk.declared_count,
"byte_count": len(chunk.bytes),
"count_matches": chunk.declared_count == len(chunk.bytes),
"bytes": list(chunk.bytes),
"bytes_hex": [_h8(byte) for byte in chunk.bytes],
}
def _device_direction(analyzer_direction: str) -> str:
if analyzer_direction == "rx":
return "tx"
if analyzer_direction == "tx":
return "rx"
return "unknown"
def _timestamp_ms(value: str) -> int:
head, _, fraction = value.partition(".")
hours, minutes, seconds = [int(part) for part in head.split(":")]
millis = int((fraction + "000")[:3]) if fraction else 0
return ((hours * 60 + minutes) * 60 + seconds) * 1000 + millis
def _h8(value: int) -> str:
return f"0x{value & 0xFF:02X}"
def _h16(value: int) -> str:
return f"0x{value & 0xFFFF:04X}"
__all__ = [
"CaptureChunk",
"analyze_capture_chunks",
"analyze_capture_text",
"checksum_for",
"format_text_report",
"main",
"parse_capture_text",
]

504
h8536/protocol_trace.py Normal file
View File

@@ -0,0 +1,504 @@
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable, Mapping, TextIO
CHECKSUM_SEED = 0x5A
FRAME_LENGTH = 6
VALID_DIRECTIONS = {"rx", "tx", "auto"}
OBSERVED_TX_REPORT_CANDIDATES = {
(0x0000, 0x0080): {
"name_candidate": "heartbeat_alive_candidate",
},
(0x0007, 0x8000): {
"name_candidate": "cam_power_button_candidate",
"state_candidate": "active",
},
(0x0015, 0x8000): {
"name_candidate": "call_button_candidate",
"state_candidate": "active",
},
(0x0015, 0x0000): {
"name_candidate": "call_button_candidate",
"state_candidate": "inactive",
},
}
@dataclass(frozen=True)
class ByteEvent:
value: int
direction_hint: str | None = None
def checksum_for(frame_prefix: Iterable[int]) -> int:
value = CHECKSUM_SEED
for byte in frame_prefix:
value ^= byte & 0xFF
return value & 0xFF
def decode_trace(
data: bytes | Iterable[int | ByteEvent],
*,
direction: str = "auto",
semantics_path: str | Path | None = None,
) -> dict[str, Any]:
if direction not in VALID_DIRECTIONS:
raise ValueError(f"direction must be one of {sorted(VALID_DIRECTIONS)}")
events = _byte_events(data)
semantics = load_semantics(semantics_path)
frames: list[dict[str, Any]] = []
previous_valid: dict[str, dict[str, Any] | None] = {"rx": None, "tx": None}
complete_len = (len(events) // FRAME_LENGTH) * FRAME_LENGTH
for frame_index, offset in enumerate(range(0, complete_len, FRAME_LENGTH)):
chunk = events[offset : offset + FRAME_LENGTH]
resolved_direction = _frame_direction(chunk, direction)
frame = _decode_frame(
[event.value for event in chunk],
frame_index=frame_index,
byte_offset=offset,
direction=resolved_direction,
semantics=semantics,
previous_valid=previous_valid,
)
frames.append(frame)
if frame["checksum"]["valid"] and resolved_direction in previous_valid:
previous_valid[resolved_direction] = frame
trailing = [event.value for event in events[complete_len:]]
return {
"kind": "h8536_protocol_trace",
"frame_length": FRAME_LENGTH,
"checksum_model": {
"algorithm": "xor",
"seed": CHECKSUM_SEED,
"seed_hex": _h8(CHECKSUM_SEED),
"covered_offsets": [0, 1, 2, 3, 4],
"checksum_offset": 5,
},
"direction_mode": direction,
"semantics": {
"loaded": semantics["loaded"],
"path": str(semantics["path"]) if semantics["path"] else None,
"command_effect_count": len(semantics["command_effects"]),
"response_schema_count": len(semantics["response_schemas"]),
"caveat": (
"Semantic names are evidence-backed candidates imported from decompiler output; "
"trace decoding does not make them protocol facts."
),
},
"frames": frames,
"trailing_bytes": [_h8(byte) for byte in trailing],
"trailing_byte_count": len(trailing),
}
def parse_byte_text(text: str, *, direction_hint: str | None = None) -> list[ByteEvent]:
events: list[ByteEvent] = []
for raw_line in text.splitlines():
line = raw_line.split("#", 1)[0].strip()
if not line:
continue
line_direction = direction_hint
lowered = line.lower()
for prefix in ("rx:", "tx:"):
if lowered.startswith(prefix):
line_direction = prefix[:2]
line = line[len(prefix) :].strip()
break
for token in _tokens(line):
events.extend(_events_from_token(token, line_direction))
return events
def load_semantics(path: str | Path | None = None) -> dict[str, Any]:
candidate = Path(path) if path else Path("build") / "rom_decompiled.json"
if not candidate.exists():
return _empty_semantics(candidate)
try:
with candidate.open("r", encoding="utf-8") as handle:
payload = json.load(handle)
except (OSError, json.JSONDecodeError):
return _empty_semantics(candidate)
serial = payload.get("serial_protocol")
if not isinstance(serial, Mapping):
serial = payload.get("serial_semantics")
if not isinstance(serial, Mapping):
return _empty_semantics(candidate)
protocol = _first_protocol(serial)
command_effects = _mapping_by_command(
_list_value(protocol.get("command_effects")) or _list_value(serial.get("command_effects"))
)
response_schemas = _list_value(protocol.get("response_schema")) or _list_value(
serial.get("response_schema")
)
return {
"loaded": True,
"path": candidate,
"command_effects": command_effects,
"response_schemas": response_schemas,
}
def format_text_report(decoded: Mapping[str, Any]) -> str:
lines = [
"H8/536 protocol trace",
(
f"frames={len(decoded.get('frames', []))} "
f"trailing={decoded.get('trailing_byte_count', 0)} "
f"semantics={'loaded' if decoded.get('semantics', {}).get('loaded') else 'not-loaded'}"
),
]
for frame in decoded.get("frames", []):
checksum = frame["checksum"]
status = "ok" if checksum["valid"] else f"bad expected {checksum['expected_hex']}"
direction = frame.get("direction") or "unknown"
prefix = (
f"[{frame['frame_index']:04d}] {direction:<7} off={frame['byte_offset']:04d} "
f"bytes={' '.join(frame['bytes_hex'])} checksum={status} "
)
if direction == "tx":
report = frame["report"]
candidate = report.get("observed_candidate")
suffix = ""
if candidate:
name = candidate.get("name_candidate")
state = candidate.get("state_candidate")
suffix = f" observed_candidate={name}" if name else " observed_candidate"
if state:
suffix += f" state={state}"
lines.append(
(
f"{prefix}report_index={report['index_hex']} "
f"value={report['value_hex']}{suffix}"
)
)
else:
command = frame["command"]
name = command.get("name_candidate")
suffix = f" {name}" if name else ""
lines.append(
(
f"{prefix}cmd={command['value_hex']}{suffix} "
f"index={frame['index']['combined']} value={frame['payload_value']['word_be_hex']}"
)
)
for annotation in frame.get("stateful_annotations", []):
lines.append(f" candidate: {annotation['kind']} - {annotation['summary']}")
return "\n".join(lines)
def main(argv: list[str] | None = None, *, stdin: TextIO | None = None, stdout: TextIO | None = None) -> int:
parser = argparse.ArgumentParser(
description="Decode H8/536 serial byte captures into 6-byte protocol frames."
)
parser.add_argument("bytes", nargs="*", help="Byte tokens, e.g. 00 01 02 03 04 5E or rx:00010203045E")
parser.add_argument("-i", "--input", help="Input file. Use '-' or omit byte args to read stdin.")
parser.add_argument("--direction", choices=sorted(VALID_DIRECTIONS), default="auto")
parser.add_argument("--json", action="store_true", help="Emit JSON instead of text.")
parser.add_argument(
"--semantics",
default=None,
help="Decompiler JSON path. Defaults to build/rom_decompiled.json when present.",
)
args = parser.parse_args(argv)
stdin = stdin or sys.stdin
stdout = stdout or sys.stdout
events: list[ByteEvent] = []
if args.input:
if args.input == "-":
events.extend(parse_byte_text(stdin.read()))
else:
events.extend(parse_byte_text(Path(args.input).read_text(encoding="utf-8")))
if args.bytes:
events.extend(parse_byte_text(" ".join(args.bytes)))
if not events and not args.input:
events.extend(parse_byte_text(stdin.read()))
decoded = decode_trace(events, direction=args.direction, semantics_path=args.semantics)
if args.json:
json.dump(decoded, stdout, indent=2, sort_keys=True)
stdout.write("\n")
else:
stdout.write(format_text_report(decoded))
stdout.write("\n")
return 0
def _decode_frame(
frame: list[int],
*,
frame_index: int,
byte_offset: int,
direction: str | None,
semantics: Mapping[str, Any],
previous_valid: Mapping[str, dict[str, Any] | None],
) -> dict[str, Any]:
expected = checksum_for(frame[:5])
actual = frame[5]
command_value = frame[0] & 0x07
command_effect = semantics["command_effects"].get(command_value, {})
is_tx_report = direction == "tx"
decoded = {
"frame_index": frame_index,
"byte_offset": byte_offset,
"direction": direction,
"bytes": frame,
"bytes_hex": [_h8(byte) for byte in frame],
"checksum": {
"algorithm": "xor",
"seed": CHECKSUM_SEED,
"expected": expected,
"expected_hex": _h8(expected),
"actual": actual,
"actual_hex": _h8(actual),
"valid": expected == actual,
},
"command": {
"applicable": not is_tx_report,
"source_byte": frame[0],
"source_byte_hex": _h8(frame[0]),
"mask": 0x07,
"value": command_value,
"value_hex": _h8(command_value),
"name_candidate": None if is_tx_report else command_effect.get("name_candidate"),
"effect_candidate": None if is_tx_report else command_effect or None,
"caveat": "TX frames are decoded as report frames; byte0 is not treated as a command."
if is_tx_report
else None,
},
"index": {
"byte1": frame[1],
"byte1_hex": _h8(frame[1]),
"byte1_low3": frame[1] & 0x07,
"byte1_low3_hex": _h8(frame[1] & 0x07),
"byte2": frame[2],
"byte2_hex": _h8(frame[2]),
"combined": ((frame[1] & 0x07) << 8) | frame[2],
"combined_hex": _h16(((frame[1] & 0x07) << 8) | frame[2]),
},
"payload_value": {
"byte3": frame[3],
"byte3_hex": _h8(frame[3]),
"byte4": frame[4],
"byte4_hex": _h8(frame[4]),
"word_be": (frame[3] << 8) | frame[4],
"word_be_hex": _h16((frame[3] << 8) | frame[4]),
"word_le": (frame[4] << 8) | frame[3],
"word_le_hex": _h16((frame[4] << 8) | frame[3]),
},
"report": _tx_report(frame) if is_tx_report else None,
"response_schema_candidates": []
if is_tx_report
else _response_schema_candidates(semantics, command_value),
"stateful_annotations": [],
}
decoded["stateful_annotations"] = _stateful_annotations(decoded, previous_valid)
return decoded
def _tx_report(frame: list[int]) -> dict[str, Any]:
index = (frame[0] << 16) | (frame[1] << 8) | frame[2]
value = (frame[3] << 8) | frame[4]
candidate = OBSERVED_TX_REPORT_CANDIDATES.get((index, value))
return {
"encoding": "observed_tx_index_value_report_candidate",
"confidence": "observed_candidate",
"index_source_offsets": [0, 1, 2],
"index": index,
"index_hex": f"0x{index:06X}" if index > 0xFFFF else _h16(index),
"index_bytes_hex": [_h8(frame[0]), _h8(frame[1]), _h8(frame[2])],
"value_source_offsets": [3, 4],
"value": value,
"value_hex": _h16(value),
"observed_candidate": dict(candidate) if candidate else None,
"caveat": "TX report names are capture-observed candidates, not ROM-derived protocol facts.",
}
def _stateful_annotations(
frame: Mapping[str, Any],
previous_valid: Mapping[str, dict[str, Any] | None],
) -> list[dict[str, Any]]:
annotations: list[dict[str, Any]] = []
if frame.get("direction") == "tx":
return annotations
if frame["command"]["value"] != 0x07:
return annotations
direction = frame.get("direction")
same = previous_valid.get(direction) if direction in previous_valid else None
opposite_direction = "tx" if direction == "rx" else "rx" if direction == "tx" else None
opposite = previous_valid.get(opposite_direction) if opposite_direction else None
annotation = {
"kind": "retransmit_or_error_candidate",
"confidence": "candidate",
"summary": "cmd 0x07 is associated with retry/error handling in decompiler semantics.",
"evidence": ["command_low3 == 0x07"],
"previous_valid_same_direction": _previous_summary(same),
"previous_valid_opposite_direction": _previous_summary(opposite),
}
if same and same.get("bytes") == frame.get("bytes"):
annotation["evidence"].append("matches previous valid frame in same direction")
if opposite and opposite.get("bytes") == frame.get("bytes"):
annotation["evidence"].append("matches previous valid frame in opposite direction")
annotations.append(annotation)
return annotations
def _previous_summary(frame: Mapping[str, Any] | None) -> dict[str, Any] | None:
if not frame:
return None
return {
"frame_index": frame["frame_index"],
"direction": frame.get("direction"),
"bytes_hex": frame["bytes_hex"],
"command": frame["command"]["value_hex"],
"checksum_valid": frame["checksum"]["valid"],
}
def _byte_events(data: bytes | Iterable[int | ByteEvent]) -> list[ByteEvent]:
if isinstance(data, bytes):
return [ByteEvent(byte) for byte in data]
events: list[ByteEvent] = []
for item in data:
if isinstance(item, ByteEvent):
events.append(item)
else:
value = int(item)
if not 0 <= value <= 0xFF:
raise ValueError(f"byte out of range: {value}")
events.append(ByteEvent(value))
return events
def _frame_direction(chunk: list[ByteEvent], mode: str) -> str | None:
if mode in {"rx", "tx"}:
return mode
hints = {event.direction_hint for event in chunk if event.direction_hint in {"rx", "tx"}}
if len(hints) == 1:
return next(iter(hints))
return None
def _tokens(text: str) -> list[str]:
return [token for token in text.replace(",", " ").replace(";", " ").split() if token]
def _events_from_token(token: str, direction_hint: str | None) -> list[ByteEvent]:
lowered = token.lower()
for prefix in ("rx:", "tx:"):
if lowered.startswith(prefix):
return _events_from_token(token[len(prefix) :], prefix[:2])
value_text = token.strip()
if value_text.upper().startswith("H'"):
value_text = "0x" + value_text[2:]
if (
not value_text.lower().startswith("0x")
and len(value_text) > 2
and len(value_text) % 2 == 0
and all(char in "0123456789abcdefABCDEF" for char in value_text)
):
return [
ByteEvent(int(value_text[index : index + 2], 16), direction_hint)
for index in range(0, len(value_text), 2)
]
if value_text.lower().startswith("0x"):
value = int(value_text, 16)
else:
value = int(value_text, 16)
if not 0 <= value <= 0xFF:
raise ValueError(f"byte out of range: {token}")
return [ByteEvent(value, direction_hint)]
def _empty_semantics(path: Path) -> dict[str, Any]:
return {"loaded": False, "path": path, "command_effects": {}, "response_schemas": []}
def _first_protocol(serial: Mapping[str, Any]) -> Mapping[str, Any]:
protocols = serial.get("protocol_semantics")
if isinstance(protocols, list):
for protocol in protocols:
if isinstance(protocol, Mapping):
return protocol
return serial
def _list_value(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _mapping_by_command(items: list[Any]) -> dict[int, Mapping[str, Any]]:
output: dict[int, Mapping[str, Any]] = {}
for item in items:
if not isinstance(item, Mapping):
continue
value = item.get("command_value", item.get("command"))
if isinstance(value, int):
output[value] = item
return output
def _response_schema_candidates(semantics: Mapping[str, Any], command: int) -> list[Mapping[str, Any]]:
matches: list[Mapping[str, Any]] = []
for schema in semantics.get("response_schemas", []):
if not isinstance(schema, Mapping):
continue
constants = _schema_constants(schema)
if command in constants:
matches.append(
{
"response_id": schema.get("response_id"),
"call_address_hex": schema.get("call_address_hex"),
"matched_command_byte_candidate": _h8(command),
"caveat": "Matched schema constants are candidates from decompiler output.",
}
)
return matches
def _schema_constants(value: Any) -> set[int]:
constants: set[int] = set()
if isinstance(value, Mapping):
for key, item in value.items():
if key in {"value", "constant", "constant_value"} and isinstance(item, int):
constants.add(item & 0x07)
constants.update(_schema_constants(item))
elif isinstance(value, list):
for item in value:
constants.update(_schema_constants(item))
return constants
def _h8(value: int) -> str:
return f"0x{value & 0xFF:02X}"
def _h16(value: int) -> str:
return f"0x{value & 0xFFFF:04X}"
__all__ = [
"ByteEvent",
"checksum_for",
"decode_trace",
"format_text_report",
"load_semantics",
"main",
"parse_byte_text",
]

View File

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

View File

@@ -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
@@ -275,6 +276,11 @@ def _serial_reconstruction_lines(serial_reconstruction: dict[str, object] | None
f"checksum {candidate['checksum_address_hex']} seeded by {candidate['checksum_seed_hex']} "
f"(confidence {confidence} {score})",
)
tx_path = candidate.get("tx_path")
if isinstance(tx_path, dict):
lines.append(
f"; TX path: {tx_path.get('summary', 'interrupt-driven TXI path')}",
)
elif kind == "candidate_sci1_rx_frame":
lines.append(
f"; RX candidate: {candidate['frame_length']} bytes "
@@ -286,6 +292,17 @@ def _serial_reconstruction_lines(serial_reconstruction: dict[str, object] | None
caveat = candidate.get("caveat")
if caveat:
lines.append(f"; caveat: {caveat}")
ram_roles = serial_reconstruction.get("ram_roles", [])
if isinstance(ram_roles, list) and ram_roles:
lines.append("; Serial RAM role candidates")
for role in ram_roles:
if not isinstance(role, dict):
continue
address_text = str(role.get("address_hex") or h16(int(role.get("address") or 0)))
lines.append(
f"; {address_text}: "
f"{role.get('name', 'ram_role')} - {role.get('summary', '')}",
)
lines.append("")
return lines
@@ -475,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")

View File

@@ -0,0 +1,548 @@
from __future__ import annotations
import argparse
import json
import re
from collections.abc import Iterable, Mapping
from pathlib import Path
from typing import Any
from .formatting import h16, label_for
JsonObject = dict[str, Any]
DEFAULT_INPUT = Path("build/rom_decompiled.json")
DEFAULT_TEXT_OUTPUT = Path("build/rom_report_sources.txt")
DEFAULT_JSON_OUTPUT = Path("build/rom_report_sources.json")
QUEUE_FUNCTION = 0x3E54
REPORT_INDEX_OF_INTEREST = 0x0007
_LOGICAL_TABLE_OFFSETS = {
0x2000: "primary_value_table_candidate",
0x1C00: "secondary_value_table_candidate",
0x1800: "current_value_table_candidate",
0x1400: "flag_table_candidate",
}
def load_report_source_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 analyze_report_sources(
payload: Mapping[str, Any],
*,
target: int = QUEUE_FUNCTION,
report_index: int = REPORT_INDEX_OF_INTEREST,
window: int = 16,
) -> JsonObject:
instructions = _instruction_sequence(payload.get("instructions"))
functions = _function_ranges(payload)
calls = [
_analyze_call(instructions, index, functions, target, report_index, window)
for index, ins in enumerate(instructions)
if _is_direct_call_to(ins, target)
]
direct_hits = [
call
for call in calls
if call["r2"].get("bit7") is True
and call["r3"].get("classification") == "constant"
and call["r3"].get("value") == report_index
]
dynamic_candidates = [
call
for call in calls
if call["r2"].get("bit7") is not False
and call["r3"].get("classification") in {"dynamic/table-derived", "unknown"}
]
if direct_hits:
conclusion = (
f"At least one direct loc_3E54 caller statically loads report index {report_index:#06x} "
"with R2.bit7 set before the queue call."
)
status = "direct_static_hit"
else:
conclusion = (
f"No direct loc_3E54 caller in this JSON statically loads report index {report_index:#06x}. "
f"{report_index:#06x} remains an observed runtime/capture value unless another indirect "
"or table-dispatch path is proven."
)
status = "not_statically_proven"
return {
"kind": "report_source_trace",
"queue_function": target,
"queue_function_hex": h16(target),
"report_index_of_interest": report_index,
"report_index_of_interest_hex": f"0x{report_index:04X}",
"summary": {
"direct_call_count": len(calls),
"direct_static_hit_count": len(direct_hits),
"dynamic_or_unknown_candidate_count": len(dynamic_candidates),
"status": status,
"conclusion": conclusion,
},
"calls": calls,
"caveats": [
"This is a bounded local static trace, not an emulator run.",
"R3 values classified as dynamic/table-derived may still become 0x0007 at runtime.",
"Indirect dispatch, table handlers, interrupt interleavings, or callers absent from the JSON may still enqueue 0x0007.",
"The generic queue-to-TX path only emits queued entries; this tracer looks for direct report-index sources at loc_3E54 callers.",
],
}
def format_text_report(analysis: Mapping[str, Any]) -> str:
summary = analysis.get("summary", {})
lines = [
"H8/536 loc_3E54 Report Source Trace",
"",
f"Queue function: {analysis.get('queue_function_hex', h16(QUEUE_FUNCTION))}",
f"Report index of interest: {analysis.get('report_index_of_interest_hex', '0x0007')}",
f"Direct callers: {summary.get('direct_call_count', 0)}",
f"Direct static 0x0007 hits: {summary.get('direct_static_hit_count', 0)}",
f"Dynamic/unknown candidates: {summary.get('dynamic_or_unknown_candidate_count', 0)}",
"",
f"Conclusion: {summary.get('conclusion', '')}",
"",
"Call sites:",
]
for call in analysis.get("calls", []):
if not isinstance(call, Mapping):
continue
r2 = call.get("r2", {}) if isinstance(call.get("r2"), Mapping) else {}
r3 = call.get("r3", {}) if isinstance(call.get("r3"), Mapping) else {}
r3_value = r3.get("value_hex") or "<dynamic>"
lines.append(
f"- {call.get('address_hex')} in {call.get('function_label')}: "
f"R2.bit7={_format_bit(r2.get('bit7'))}, "
f"R3={r3_value} ({r3.get('classification', 'unknown')}); "
f"direct_0x0007={call.get('can_directly_enqueue_report_index')}"
)
for source_name, source in (("R2", r2), ("R3", r3)):
evidence = source.get("evidence") if isinstance(source, Mapping) else None
if not isinstance(evidence, Mapping):
continue
text = evidence.get("instruction")
if text:
lines.append(f" {source_name} evidence: {evidence.get('address_hex')} {text}")
table_hints = call.get("table_hints")
if isinstance(table_hints, list) and table_hints:
hints = ", ".join(
f"{item.get('address_hex')} {item.get('table')} via {item.get('operand')}"
for item in table_hints[:4]
if isinstance(item, Mapping)
)
lines.append(f" table/context hints: {hints}")
lines.extend(["", "Caveats:"])
for caveat in analysis.get("caveats", []):
lines.append(f"- {caveat}")
return "\n".join(lines).rstrip() + "\n"
def write_report_sources(input_path: Path, output_path: Path, *, as_json: bool = False) -> JsonObject:
analysis = analyze_report_sources(load_report_source_input(input_path))
output_path.parent.mkdir(parents=True, exist_ok=True)
if as_json:
output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
else:
output_path.write_text(format_text_report(analysis), encoding="utf-8")
return analysis
def main(argv: list[str] | None = None, stdout: Any | None = None) -> int:
parser = argparse.ArgumentParser(
description="Trace direct loc_3E54 report queue callers and their R2/R3 sources.",
)
parser.add_argument(
"input",
nargs="?",
type=Path,
default=DEFAULT_INPUT,
help="structured JSON emitted by h8536_decompiler.py",
)
parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text")
parser.add_argument("--out", type=Path, default=None, help="write report to this path")
parser.add_argument("--window", type=int, default=16, help="bounded backward instruction window per call")
args = parser.parse_args(argv)
stream = stdout
if stream is None:
import sys
stream = sys.stdout
analysis = analyze_report_sources(load_report_source_input(args.input), window=args.window)
rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n" if args.json else format_text_report(analysis)
if args.out:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(rendered, encoding="utf-8")
print(f"wrote {args.out}", file=stream)
else:
print(rendered, end="", file=stream)
return 0
def _analyze_call(
instructions: list[JsonObject],
call_index: int,
functions: list[JsonObject],
target: int,
report_index: int,
window: int,
) -> JsonObject:
call = instructions[call_index]
address = int(call["address"])
function = _function_for_address(functions, address)
local_window = _local_window(instructions, call_index, function, window)
r2 = _resolve_register(local_window, "R2", call, width=8)
r3 = _resolve_register(local_window, "R3", call, width=16)
table_hints = _table_hints(local_window)
can_enqueue = (
r2.get("bit7") is True
and r3.get("classification") == "constant"
and r3.get("value") == report_index
)
return {
"address": address,
"address_hex": h16(address),
"instruction": _instruction_text(call),
"target": target,
"target_hex": h16(target),
"function_start": function.get("start") if function else None,
"function_start_hex": h16(int(function["start"])) if function else None,
"function_label": function.get("label") if function else label_for(address),
"dataflow_block": _dataflow_block(call),
"window_instruction_count": len(local_window),
"window_start": int(local_window[0]["address"]) if local_window else address,
"window_start_hex": h16(int(local_window[0]["address"])) if local_window else h16(address),
"r2": r2,
"r3": r3,
"table_hints": table_hints,
"can_directly_enqueue_report_index": can_enqueue,
"assessment": _call_assessment(r2, r3, report_index),
}
def _resolve_register(window: list[Mapping[str, Any]], register: str, call: Mapping[str, Any], *, width: int) -> JsonObject:
evidence = _trace_register(window, register, seen=set(), width=width)
if evidence is None:
evidence = _dataflow_before(call, register)
if evidence is None:
return {
"register": register,
"classification": "unknown",
"value": None,
"value_hex": None,
"bit7": None if register != "R2" else "unknown",
"evidence": None,
}
if evidence.get("classification") == "constant" and isinstance(evidence.get("value"), int):
value = int(evidence["value"]) & ((1 << width) - 1)
evidence["value"] = value
evidence["value_hex"] = f"0x{value:04X}" if width > 8 else f"0x{value:02X}"
if register == "R2":
evidence["bit7"] = bool(value & 0x80)
elif register == "R2":
evidence["bit7"] = None
evidence["register"] = register
return evidence
def _trace_register(
window: list[Mapping[str, Any]],
register: str,
*,
seen: set[str],
width: int,
) -> JsonObject | None:
register = register.upper()
if register in seen:
return None
seen.add(register)
for ins in reversed(window):
source, destination = _source_destination_operands(str(ins.get("operands", "")))
mnemonic = _mnemonic_root(str(ins.get("mnemonic", "")))
if destination.upper() != register:
if _mutates_register(ins, register):
return _source_record("dynamic/table-derived", ins, reason=f"{mnemonic} mutates {register}")
continue
immediate = _parse_immediate(source)
if mnemonic.startswith("MOV") and immediate is not None:
return _source_record("constant", ins, value=immediate & ((1 << width) - 1), reason="immediate load")
source_register = _register_operand(source)
if mnemonic.startswith("MOV") and source_register:
nested = _trace_register(window[: window.index(ins)], source_register, seen=seen, width=width)
if nested is not None:
nested = dict(nested)
nested["via"] = _evidence_record(ins)
return nested
return _source_record("dynamic/table-derived", ins, reason=f"copied from unresolved {source_register}")
if "@" in source or "@" in destination:
classification = "dynamic/table-derived" if _table_operand(source) or _table_operand(destination) else "dynamic/table-derived"
return _source_record(classification, ins, reason="memory/indexed source")
return _source_record("unknown", ins, reason=f"unsupported writer {mnemonic}")
return None
def _dataflow_before(call: Mapping[str, Any], register: str) -> JsonObject | None:
dataflow = call.get("dataflow")
if not isinstance(dataflow, Mapping):
return None
changes = dataflow.get("changes")
if not isinstance(changes, list):
return None
for change in changes:
if not isinstance(change, Mapping) or change.get("kind") != "register" or str(change.get("name", "")).upper() != register:
continue
before = change.get("before")
if isinstance(before, Mapping) and before.get("known") is True and isinstance(before.get("value"), int):
return {
"classification": "constant",
"value": int(before["value"]),
"value_hex": before.get("hex"),
"reason": "decompiler dataflow before call",
"evidence": {
"address": call.get("address"),
"address_hex": h16(int(call["address"])) if isinstance(call.get("address"), int) else None,
"instruction": before.get("source"),
},
}
if isinstance(before, Mapping) and before.get("known") is False:
return {
"classification": "unknown",
"value": None,
"value_hex": None,
"reason": f"decompiler dataflow: {before.get('reason', 'unknown')}",
"evidence": None,
}
return None
def _call_assessment(r2: Mapping[str, Any], r3: Mapping[str, Any], report_index: int) -> str:
if r2.get("bit7") is False:
return "R2.bit7 appears clear, so loc_3E54 would not enqueue on this local evidence."
if r3.get("classification") == "constant":
if r3.get("value") == report_index:
return f"Direct static enqueue source for {report_index:#06x}."
return f"Direct static enqueue source for {int(r3.get('value', 0)):#06x}, not {report_index:#06x}."
return f"No static {report_index:#06x} constant here; R3 is {r3.get('classification', 'unknown')}."
def _local_window(
instructions: list[JsonObject],
call_index: int,
function: Mapping[str, Any] | None,
window: int,
) -> list[JsonObject]:
call = instructions[call_index]
call_block = _dataflow_block(call)
selected: list[JsonObject] = []
for prior in reversed(instructions[:call_index]):
if len(selected) >= window:
break
address = int(prior["address"])
if function and not (int(function["start"]) <= address <= int(function["end"])):
break
prior_block = _dataflow_block(prior)
if call_block is not None and prior_block is not None and prior_block != call_block:
continue
selected.append(prior)
return list(reversed(selected))
def _table_hints(window: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
hints: list[JsonObject] = []
for ins in window:
operands = str(ins.get("operands", ""))
for operand, table in _table_operands(operands):
hints.append(
{
"address": int(ins["address"]),
"address_hex": h16(int(ins["address"])),
"instruction": _instruction_text(ins),
"operand": operand,
"table": table,
}
)
return hints
def _table_operand(operand: str) -> bool:
return bool(_table_operands(operand))
def _table_operands(operands: str) -> list[tuple[str, str]]:
matches: list[tuple[str, str]] = []
for match in re.finditer(r"@\(-H'([0-9A-Fa-f]+),\s*(R[0-7])\)", operands):
offset = int(match.group(1), 16) & 0xFFFF
table = _LOGICAL_TABLE_OFFSETS.get(offset)
if table:
matches.append((match.group(0), table))
return matches
def _source_record(
classification: str,
ins: Mapping[str, Any],
*,
value: int | None = None,
reason: str,
) -> JsonObject:
return {
"classification": classification,
"value": value,
"value_hex": f"0x{value:04X}" if value is not None else None,
"reason": reason,
"evidence": _evidence_record(ins),
}
def _evidence_record(ins: Mapping[str, Any]) -> JsonObject:
address = ins.get("address")
return {
"address": address,
"address_hex": h16(int(address)) if isinstance(address, int) else None,
"instruction": _instruction_text(ins),
"mnemonic": ins.get("mnemonic"),
"operands": ins.get("operands"),
}
def _is_direct_call_to(ins: Mapping[str, Any], target: int) -> bool:
mnemonic = _mnemonic_root(str(ins.get("mnemonic", "")))
return mnemonic in {"BSR", "JSR"} and target in _targets(ins)
def _instruction_sequence(value: object) -> list[JsonObject]:
if isinstance(value, Mapping):
values: Iterable[Any] = value.values()
elif isinstance(value, list):
values = value
else:
values = []
return sorted(
[item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)],
key=lambda item: int(item["address"]),
)
def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
call_graph = payload.get("call_graph")
nodes = call_graph.get("nodes") if isinstance(call_graph, Mapping) else None
if not isinstance(nodes, list):
return []
ranges: list[JsonObject] = []
for node in nodes:
if not isinstance(node, Mapping):
continue
start = node.get("start")
end = node.get("end")
if isinstance(start, int) and isinstance(end, int):
ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))})
return sorted(ranges, key=lambda item: int(item["start"]))
def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None:
for function in functions:
if int(function["start"]) <= address <= int(function["end"]):
return function
return None
def _dataflow_block(ins: Mapping[str, Any]) -> int | None:
dataflow = ins.get("dataflow")
if isinstance(dataflow, Mapping) and isinstance(dataflow.get("block"), int):
return int(dataflow["block"])
return None
def _targets(ins: Mapping[str, Any]) -> list[int]:
targets = ins.get("targets", [])
return [int(target) for target in targets if isinstance(target, int)] if isinstance(targets, list) else []
def _instruction_text(ins: Mapping[str, Any]) -> str:
text = ins.get("text")
if isinstance(text, str) and text:
return text
operands = str(ins.get("operands", ""))
return f"{ins.get('mnemonic', '')} {operands}".strip()
def _source_destination_operands(operands: str) -> tuple[str, str]:
depth = 0
split_at: int | None = None
for index, char in enumerate(operands):
if char in "({":
depth += 1
elif char in ")}" and depth:
depth -= 1
elif char == "," and depth == 0:
split_at = index
if split_at is None:
operand = operands.strip()
return "", operand
return operands[:split_at].strip(), operands[split_at + 1 :].strip()
def _parse_immediate(operand: str) -> int | None:
text = operand.strip()
if text.startswith("#"):
text = text[1:].strip()
try:
if text.upper().startswith("H'"):
return int(text[2:], 16) & 0xFFFF
if text.upper().startswith("0X"):
return int(text, 16) & 0xFFFF
if text.startswith("$"):
return int(text[1:], 16) & 0xFFFF
return int(text, 10) & 0xFFFF
except ValueError:
return None
def _register_operand(operand: str) -> str | None:
text = operand.strip().upper()
return text if re.fullmatch(r"R[0-7]", text) else None
def _mutates_register(ins: Mapping[str, Any], register: str) -> bool:
mnemonic = _mnemonic_root(str(ins.get("mnemonic", "")))
source, destination = _source_destination_operands(str(ins.get("operands", "")))
if destination.upper() != register.upper():
return False
return not mnemonic.startswith("MOV")
def _mnemonic_root(mnemonic: str) -> str:
return mnemonic.rsplit(".", 1)[0].upper()
def _format_bit(value: Any) -> str:
if value is True:
return "set"
if value is False:
return "clear"
return "unknown"
__all__ = [
"analyze_report_sources",
"format_text_report",
"load_report_source_input",
"main",
"write_report_sources",
]
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -61,11 +61,26 @@ SSR_FLAGS = {
}
SSR_CLEAR_ACTIONS = {
7: ("clear_tdre", "clear {channel} transmit data register empty flag (TDRE)"),
6: ("clear_rdrf", "clear {channel} receive-data-full flag (RDRF)"),
5: ("clear_orer", "clear {channel} overrun error flag (ORER)"),
4: ("clear_fer", "clear {channel} framing error flag (FER)"),
3: ("clear_per", "clear {channel} parity error flag (PER)"),
7: (
"clear_tdre",
"clear {channel} TDRE after TDR write; TXI can fire again when hardware reasserts TDRE",
),
6: (
"clear_rdrf",
"clear {channel} RDRF with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
),
5: (
"clear_orer",
"clear {channel} ORER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
),
4: (
"clear_fer",
"clear {channel} FER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
),
3: (
"clear_per",
"clear {channel} PER with SSR R/(W)* semantics: write 0 clears latched hardware flag, write 1 preserves hardware-owned state",
),
}
@@ -263,7 +278,10 @@ def _scr_bit_event(
verb = "enable" if enabled else "disable"
action_noun = noun.replace("/", "_").replace(" ", "_").lower()
if bit == 7:
comment = f"{verb} {register.channel} TX interrupt (TIE)"
comment = (
f"{verb} {register.channel} TX interrupt (TIE); "
"gates TXI when hardware sets TDRE"
)
elif bit == 6:
comment = f"{verb} {register.channel} receive and receive-error interrupts (RIE)"
elif bit == 5:

552
h8536/serial_gate.py Normal file
View File

@@ -0,0 +1,552 @@
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
from .formatting import h16, label_for
JsonObject = dict[str, Any]
KEY_STATE_ADDRESSES: tuple[int, ...] = (
0xF9B0,
0xF9B4,
0xF9B5,
0xF9B9,
0xF9C0,
0xF9C1,
0xF9C3,
0xF9C4,
0xF9C5,
0xF9C6,
0xF9C8,
0xFAA2,
0xFAA3,
0xFAA5,
)
DEFAULT_INPUT = Path("build/rom_decompiled.json")
CAPTURE_OVERLAY_CAVEAT = (
"Observed report indexes 0x0007 and 0x0015 are capture overlays/runtime queue "
"entries; this analyzer does not treat them as statically proven ROM constants."
)
def load_serial_gate_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 analyze_serial_gate(payload: dict[str, Any]) -> JsonObject:
instructions = _instruction_sequence(payload.get("instructions"))
labels = _collect_labels(payload, instructions)
by_address = {int(ins["address"]): ins for ins in instructions if "address" in ins}
evidence = {
"scheduler_gate_loc_3FD3": _scheduler_gate(by_address),
"queue_send_gate_loc_BAF2": _queue_send_gate(by_address),
"resend_gate_path": _resend_gate_path(by_address),
"rx_session_maintenance": _rx_session_maintenance(by_address),
"idle_heartbeat_gate_loc_4046": _idle_heartbeat_gate(payload, by_address),
"timer_tick_evidence": _timer_tick_evidence(payload, by_address),
}
access_summary = _state_access_summary(instructions, labels)
return {
"kind": "serial_gate",
"summary": {
"state_machine_candidate": "autonomous serial TX/report queue gate",
"confidence": _confidence(evidence),
"basis": "address-driven static evidence from decompiler JSON",
},
"state_addresses": [
{"address": address, "address_hex": h16(address), "symbol": f"ram_{address:04X}"}
for address in KEY_STATE_ADDRESSES
],
"evidence": evidence,
"state_accesses": access_summary,
"caveats": [
CAPTURE_OVERLAY_CAVEAT,
"Queue entries near F870 are reached through RAM-indexed addressing; static JSON proves the access pattern, not the runtime queue contents.",
"Branch predicates are summarized from local instruction order and targets; this is not an emulator trace.",
],
}
def format_text_report(analysis: dict[str, Any]) -> str:
lines = [
"H8/536 Serial Gate/Queue State-Machine Reconstruction",
"",
f"Summary: {analysis['summary']['state_machine_candidate']}",
f"Confidence: {analysis['summary']['confidence']}",
"",
"Evidence:",
]
for key, section in analysis.get("evidence", {}).items():
title = str(section.get("title", key)).rstrip(".")
status = "present" if section.get("present") else "missing"
lines.append(f"- {title}: {status}")
summary = section.get("summary")
if summary:
lines.append(f" {summary}")
for item in section.get("items", []):
lines.append(f" - {item['address_hex']}: {item['text']}")
roles = section.get("candidate_timer_roles", [])
if roles:
lines.append(" Candidate timer roles:")
for role in roles:
lines.append(f" - {role['address_hex']}: {role['role']}")
timer = section.get("timer")
if isinstance(timer, dict):
source = timer.get("source")
handler = timer.get("handler_address_hex")
ocra = timer.get("ocra_value_hex")
period = timer.get("observed_period_ms_candidate")
timer_bits = [str(part) for part in (source, handler, f"OCRA={ocra}" if ocra else "", f"observed period ~= {period}ms" if period else "") if part]
if timer_bits:
lines.append(f" Timer: {', '.join(timer_bits)}")
lines.extend(["", "State address readers/writers:"])
for entry in analysis.get("state_accesses", []):
lines.append(
f"- {entry['address_hex']}: reads={entry['read_count']} "
f"writes={entry['write_count']} read/write={entry['read_write_count']}"
)
samples = entry.get("sample_accesses", [])
if samples:
sample_text = "; ".join(f"{sample['address_hex']} {sample['access']} {sample['text']}" for sample in samples)
lines.append(f" {sample_text}")
lines.extend(["", "Caveats:"])
for caveat in analysis.get("caveats", []):
lines.append(f"- {caveat}")
return "\n".join(lines).rstrip() + "\n"
def write_serial_gate_report(input_path: Path, output_path: Path, *, as_json: bool = False) -> JsonObject:
analysis = analyze_serial_gate(load_serial_gate_input(input_path))
output_path.parent.mkdir(parents=True, exist_ok=True)
if as_json:
output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
else:
output_path.write_text(format_text_report(analysis), encoding="utf-8")
return analysis
def main(argv: list[str] | None = None, stdout: Any | None = None) -> int:
parser = argparse.ArgumentParser(
description="Summarize H8/536 autonomous serial TX/report gates and queue state.",
)
parser.add_argument(
"input",
nargs="?",
type=Path,
default=DEFAULT_INPUT,
help="structured JSON emitted by h8536_decompiler.py",
)
parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text")
parser.add_argument("--out", type=Path, default=None, help="write report to this path")
args = parser.parse_args(argv)
stream = stdout
if stream is None:
import sys
stream = sys.stdout
analysis = analyze_serial_gate(load_serial_gate_input(args.input))
if args.json:
rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n"
else:
rendered = format_text_report(analysis)
if args.out:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(rendered, encoding="utf-8")
print(f"wrote {args.out}", file=stream)
else:
print(rendered, end="", file=stream)
return 0
def _scheduler_gate(by_address: dict[int, JsonObject]) -> JsonObject:
addresses = [0x3FD3, 0x3FD7, 0x3FD9, 0x3FDD, 0x3FDF, 0x3FE3, 0x3FE5, 0x3FE9, 0x3FEB]
items = _items(by_address, addresses)
return {
"title": "loc_3FD3 gate into loc_BAF2",
"present": _has_all(by_address, (0x3FD3, 0x3FD9, 0x3FDF, 0x3FE5, 0x3FEB)),
"summary": (
"Requires FAA2 == 0, allows the FAA5.bit7 path only when F9C3 == 0, "
"then requires F9C0 == 0 before BSR loc_BAF2."
),
"items": items,
"required_addresses_hex": [h16(address) for address in addresses],
}
def _queue_send_gate(by_address: dict[int, JsonObject]) -> JsonObject:
addresses = [
0xBAF2,
0xBAF8,
0xBAFC,
0xBAFE,
0xBB00,
0xBB08,
0xBB1C,
0xBB20,
0xBB2B,
0xBB39,
0xBB3F,
0xBB43,
0xBB46,
0xBB4C,
0xBB51,
]
return {
"title": "loc_BAF2 queue send gate",
"present": _has_all(by_address, (0xBAF2, 0xBAF8, 0xBB08, 0xBB1C, 0xBB39, 0xBB43)),
"summary": (
"F9B5 is compared against F9B0; inequality enters the send path, reads a queued "
"word via the F9B5-derived index around F870, stages F850-F854, and calls BA26 at BB43."
),
"items": _items(by_address, addresses),
"queue_table_candidate": {
"base_address_hex": h16(0xF870),
"index_address_hex": h16(0xF9B5),
"evidence_address_hex": h16(0xBB08),
"addressing_text": _text(by_address, 0xBB08),
},
"staging_addresses_hex": [h16(address) for address in range(0xF850, 0xF855)],
"send_subroutine_hex": h16(0xBA26),
"send_call_address_hex": h16(0xBB43),
}
def _resend_gate_path(by_address: dict[int, JsonObject]) -> JsonObject:
addresses = [0xBE9E, 0xBEA5, 0xBEA9, 0xBEAF, 0xBEB5, 0xBEBB, 0xBEC5, 0xBECB, 0xBED1, 0xBED5]
return {
"title": "resend gate/path",
"present": _has_all(by_address, (0xBE9E, 0xBEA5, 0xBEB5, 0xBEBB, 0xBECB, 0xBED5)),
"summary": (
"BE9E masks FAA5 with FAA3, waits for F9C6/F9C8 timeout gates, then if FAA3.bit7 "
"remains set clears F9C3 and calls BA26 from BED5."
),
"items": _items(by_address, addresses),
"resend_call_address_hex": h16(0xBED5),
"send_subroutine_hex": h16(0xBA26),
}
def _rx_session_maintenance(by_address: dict[int, JsonObject]) -> JsonObject:
addresses = [
0x3FEF,
0x3FF5,
0x3FF9,
0x3FFD,
0x4007,
0xBBCB,
0xBC0F,
0xBC15,
0xBC33,
0xBC5C,
0xBC63,
0xBCD0,
0xBCFD,
0xBD04,
0xBD6D,
0xBD71,
0xBD75,
0xBD79,
0xBDC8,
0xBDCC,
0xBDD0,
0xBDD4,
0xBDF3,
0xBDF7,
0xBDFB,
0xBDFF,
]
return {
"title": "RX/session maintenance",
"present": _has_all(by_address, (0x3FEF, 0x3FF5, 0xBBCB, 0xBC15, 0xBD6D, 0xBD79)),
"summary": (
"F9C5 timeout maintenance clears F9B5/F9B0 and FAA5.bit7; RX command processing "
"uses FAA2 as an in-session latch and paths advance F9B5/F9B0 or clear FAA3/FAA2."
),
"items": _items(by_address, addresses),
}
def _idle_heartbeat_gate(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
vector = _vector_entry(payload, 0x006A, "frt2_ocia")
handler = _int_field(vector, "target") if vector else None
if handler is None:
handler = 0xBF23
addresses = [
0x4046,
0x404A,
0x404C,
0x4050,
0x4052,
0x4056,
0x4058,
0x4059,
0x405F,
0x4063,
0x4067,
0x406C,
0x4070,
0x40E0,
0xBA31,
handler,
0xBF27,
0xBF2D,
]
present = _has_all(by_address, (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D))
return {
"title": "loc_4046 idle heartbeat/report gate",
"present": present,
"summary": (
"F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send "
"reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 "
"can enqueue H'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."
),
"items": _items(by_address, addresses),
"gate_address_hex": h16(0x4046),
"queue_write_address_hex": h16(0x4067),
"initial_reload_address_hex": h16(0x40E0),
"post_tx_reload_address_hex": h16(0xBA31),
"tick_handler_address_hex": h16(handler),
"decrement_address_hex": h16(0xBF2D),
"initial_reload_value_hex": "H'14",
"post_tx_reload_value_hex": "H'07",
"timer": {
"source": "FRT2 OCIA",
"vector_address_hex": h16(0x006A),
"handler_address_hex": h16(handler),
"vector_target_label": str(vector.get("target_label", "")) if vector else "",
"tcr_address_hex": h16(0xFEA0),
"tcsr_address_hex": h16(0xFEA1),
"ocra_address_hex": h16(0xFEA4),
"ocra_value_hex": "H'7A12",
"clock_select": "CKS1=1 CKS0=0 => phi/32",
"observed_period_ms_candidate": 700,
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
},
"candidate_timer_roles": [
{
"address": 0xF9C4,
"address_hex": h16(0xF9C4),
"role": "candidate idle heartbeat/report gate countdown",
"evidence_address_hex": h16(0xBF2D),
}
],
"required_addresses_hex": [h16(address) for address in (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D)],
}
def _timer_tick_evidence(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
vector = _vector_entry(payload, 0x0062, "frt1_ocia")
handler = _int_field(vector, "target") if vector else None
if handler is None:
handler = 0xBEEA
addresses = [handler, 0xBEEE, 0xBEF4, 0xBEF8, 0xBEFE, 0xBF02, 0xBF08]
has_vector = vector is not None and _int_field(vector, "target") == handler
has_handler_clear = _instruction_mentions(by_address.get(handler), ("FRT1_TCSR", "#5"))
decrement_addresses = (0xBEF4, 0xBEFE, 0xBF08)
has_decrements = all(
address in by_address and _access_kind(by_address[address], state_address) == "read_write"
for address, state_address in zip(decrement_addresses, (0xF9C0, 0xF9C1, 0xF9C6))
)
return {
"title": "FRT1 OCIA periodic tick countdowns",
"present": bool((has_vector or has_handler_clear) and has_decrements),
"summary": (
"Static evidence links vector H'0062 to the FRT1 OCIA handler at H'BEEA; the handler "
"clears FRT1_TCSR.OCFA and conditionally decrements H'F9C0, H'F9C1, and H'F9C6."
),
"vector_address_hex": h16(0x0062),
"handler_address_hex": h16(handler),
"vector_target_label": str(vector.get("target_label", "")) if vector else "",
"items": _items(by_address, addresses),
"candidate_timer_roles": [
{
"address": 0xF9C0,
"address_hex": h16(0xF9C0),
"role": "candidate post-TX/report delay countdown",
"evidence_address_hex": h16(0xBEF4),
},
{
"address": 0xF9C1,
"address_hex": h16(0xF9C1),
"role": "candidate secondary delay countdown",
"evidence_address_hex": h16(0xBEFE),
},
{
"address": 0xF9C6,
"address_hex": h16(0xF9C6),
"role": "candidate periodic report/heartbeat countdown",
"evidence_address_hex": h16(0xBF08),
},
],
}
def _state_access_summary(instructions: list[JsonObject], labels: dict[int, str]) -> list[JsonObject]:
result: list[JsonObject] = []
for state_address in KEY_STATE_ADDRESSES:
accesses = []
for ins in instructions:
if state_address not in _reference_addresses(ins):
continue
access = _access_kind(ins, state_address)
accesses.append(
{
"address": int(ins["address"]),
"address_hex": h16(int(ins["address"])),
"function": _function_label_for_address(int(ins["address"]), labels),
"access": access,
"text": str(ins.get("text", "")),
}
)
result.append(
{
"address": state_address,
"address_hex": h16(state_address),
"read_count": sum(1 for access in accesses if access["access"] == "read"),
"write_count": sum(1 for access in accesses if access["access"] == "write"),
"read_write_count": sum(1 for access in accesses if access["access"] == "read_write"),
"accesses": accesses,
"sample_accesses": accesses[:6],
}
)
return result
def _instruction_sequence(raw: Any) -> list[JsonObject]:
if not isinstance(raw, list):
return []
return sorted(
[item for item in raw if isinstance(item, dict) and isinstance(item.get("address"), int)],
key=lambda item: int(item["address"]),
)
def _collect_labels(payload: dict[str, Any], instructions: list[JsonObject]) -> dict[int, str]:
labels: dict[int, str] = {}
nodes = payload.get("call_graph", {}).get("nodes", []) if isinstance(payload.get("call_graph"), dict) else []
if isinstance(nodes, list):
for node in nodes:
if isinstance(node, dict) and isinstance(node.get("start"), int) and node.get("label"):
labels[int(node["start"])] = str(node["label"])
return labels
def _items(by_address: dict[int, JsonObject], addresses: list[int]) -> list[JsonObject]:
return [
{
"address": address,
"address_hex": h16(address),
"text": _text(by_address, address),
"present": address in by_address,
"targets_hex": [h16(target) for target in by_address.get(address, {}).get("targets", []) if isinstance(target, int)],
}
for address in addresses
]
def _has_all(by_address: dict[int, JsonObject], addresses: tuple[int, ...]) -> bool:
return all(address in by_address for address in addresses)
def _vector_entry(payload: dict[str, Any], address: int, name: str) -> JsonObject | None:
vectors = payload.get("vectors", [])
if not isinstance(vectors, list):
return None
for vector in vectors:
if not isinstance(vector, dict):
continue
if _int_field(vector, "address") == address or str(vector.get("name", "")).lower() == name:
return vector
return None
def _int_field(payload: JsonObject | None, key: str, default: int | None = None) -> int | None:
if not isinstance(payload, dict):
return default
value = payload.get(key)
return value if isinstance(value, int) else default
def _instruction_mentions(ins: JsonObject | None, fragments: tuple[str, ...]) -> bool:
if not isinstance(ins, dict):
return False
text = f"{ins.get('text', '')} {ins.get('operands', '')} {ins.get('comment', '')}".upper()
return all(fragment.upper() in text for fragment in fragments)
def _text(by_address: dict[int, JsonObject], address: int) -> str:
return str(by_address.get(address, {}).get("text", "<missing>"))
def _reference_addresses(ins: JsonObject) -> set[int]:
addresses: set[int] = set()
refs = ins.get("references", [])
if isinstance(refs, list):
for ref in refs:
if isinstance(ref, dict) and isinstance(ref.get("address"), int):
addresses.add(int(ref["address"]))
text = str(ins.get("text", ""))
for match in re.finditer(r"@H'([0-9A-Fa-f]{4})", text):
addresses.add(int(match.group(1), 16))
return addresses
def _access_kind(ins: JsonObject, address: int) -> str:
mnemonic = str(ins.get("mnemonic", "")).upper()
operands = str(ins.get("operands", ""))
target = f"@H'{address:04X}"
upper_operands = operands.upper()
if mnemonic.startswith(("TST", "CMP", "BTST")):
return "read"
if mnemonic.startswith("CLR"):
return "write"
if mnemonic.startswith(("BSET", "BCLR", "ADD", "SUB", "INC", "DEC")):
return "read_write"
if mnemonic.startswith("MOV") and "," in upper_operands:
_src, dest = [part.strip() for part in upper_operands.rsplit(",", 1)]
return "write" if target in dest else "read"
if mnemonic.startswith(("AND", "OR", "XOR")) and "," in upper_operands:
_src, dest = [part.strip() for part in upper_operands.rsplit(",", 1)]
return "read_write" if target in dest else "read"
return "read"
def _function_label_for_address(address: int, labels: dict[int, str]) -> str:
starts = [start for start in labels if start <= address]
if not starts:
return label_for(address)
return labels[max(starts)]
def _confidence(evidence: dict[str, JsonObject]) -> str:
present_count = sum(1 for section in evidence.values() if section.get("present"))
if present_count == len(evidence):
return "high"
if present_count >= 2:
return "medium"
return "low"
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -301,11 +301,17 @@ def _declarations(tx_candidate: JsonObject | None, rx_candidate: JsonObject | No
tx_start = _int_field(tx_candidate, "buffer_start", 0xF858)
tx_index = _int_field(tx_candidate, "tx_index_address", 0xF9C2)
length = _int_field(tx_candidate, "frame_length", 6)
checksum_index = max(length - 1, 0)
lines.extend(
[
f"#define TX_FRAME_LENGTH {length}u",
f"#define SCI1_TX_FRAME_LENGTH {length}u",
f"#define SCI1_TX_FRAME_BASE {_c_hex(tx_start)}",
"#define SCI1_TX_FRAME_BYTE(n) MEM8[(u16)(SCI1_TX_FRAME_BASE + (n))]",
f"#define SCI1_TX_FRAME_CHECKSUM SCI1_TX_FRAME_BYTE({checksum_index}u)",
f"#define SCI1_TX_INDEX MEM8[{_c_hex(tx_index)}]",
"#define TX_FRAME_LENGTH SCI1_TX_FRAME_LENGTH",
f"#define TX_FRAME(n) MEM8[(u16)({_c_hex(tx_start)} + (n))]",
f"#define TX_INDEX MEM8[{_c_hex(tx_index)}]",
"#define TX_INDEX SCI1_TX_INDEX",
"",
],
)
@@ -404,6 +410,10 @@ def _semantics_lines(
lines.extend(_table_map_comment_lines(_table_map_list(protocol), opts, prefix=" * "))
lines.extend(_state_variable_comment_lines(protocol.get("state_variable_candidates"), opts, prefix=" * "))
lines.extend(_retry_error_comment_lines(protocol.get("retry_error_model"), opts, prefix=" * "))
lines.extend(_gate_queue_comment_lines(protocol.get("gate_queue_model"), opts, prefix=" * "))
lines.extend(_tx_report_comment_lines(protocol.get("tx_report_model"), opts, prefix=" * "))
lines.extend(_periodic_resend_comment_lines(protocol.get("periodic_resend_model"), opts, prefix=" * "))
lines.extend(_timer_architecture_comment_lines(protocol, opts, prefix=" * "))
lines.append(" */")
lines.append("")
@@ -436,6 +446,12 @@ def _semantics_lines(
" return 0x01FFu;",
"}",
"",
],
)
lines.extend(_gate_queue_predicate_function_lines(protocol.get("gate_queue_model")))
lines.extend(_timer_architecture_function_lines(protocol))
lines.extend(
[
"void sci1_process_candidate_protocol_command(void)",
"{",
" u8 command = sci1_rx_candidate_command();",
@@ -605,6 +621,354 @@ def _retry_error_comment_lines(
return lines
def _gate_queue_comment_lines(
value: object,
opts: SerialPseudocodeOptions,
*,
prefix: str,
) -> list[str]:
if not isinstance(value, dict):
return []
lines = [f"{prefix}gate/queue state machine candidate:"]
for predicate in _object_list(value.get("predicates")):
name = predicate.get("name") or "predicate_candidate"
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"))
commands = ", ".join(str(item) for item in effect.get("command_values_hex", []) if item)
suffix = f"; commands {commands}" if commands else ""
lines.append(f"{prefix}- {name}: {summary}{suffix}")
caveat = str(value.get("caveat") or "").strip()
if caveat:
lines.append(f"{prefix}- caveat: {_comment_text(caveat)}")
evidence = _hex_join(value.get("evidence_addresses_hex"))
if opts.include_evidence and evidence:
lines.append(f"{prefix}- evidence: {evidence}")
return lines
def _gate_queue_predicate_function_lines(value: object) -> list[str]:
if not isinstance(value, dict):
return []
return [
"static bool sci1_candidate_main_report_gate_open(void)",
"{",
" bool session_idle = MEM8[0xFAA2u] == 0u;",
" bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;",
" bool tx_timer_clear = MEM8[0xF9C0u] == 0u;",
"",
" return session_idle && rx_gate_open && tx_timer_clear;",
"}",
"",
"static bool sci1_candidate_report_queue_nonempty(void)",
"{",
" return MEM8[0xF9B5u] != MEM8[0xF9B0u];",
"}",
"",
"static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)",
"{",
" bool idle_timer_clear = MEM8[0xF9C4u] == 0u;",
" bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;",
" bool queue_empty = MEM8[0xF9B0u] == MEM8[0xF9B5u];",
"",
" return idle_timer_clear && rx_gate_open && queue_empty;",
"}",
"",
"static 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;",
" bool period_elapsed = MEM8[0xF9C6u] == 0u && MEM8[0xF9C7u] == 0u;",
" bool resend_countdown_active = MEM8[0xF9C8u] != 0u;",
"",
" return pending && period_elapsed && resend_countdown_active;",
"}",
"",
]
def _tx_report_comment_lines(
value: object,
opts: SerialPseudocodeOptions,
*,
prefix: str,
) -> list[str]:
if not isinstance(value, dict):
return []
entry = value.get("entry_label") or value.get("entry_address_hex") or "TX report path"
source = _comment_text(str(value.get("value_source_candidate") or "current value table"))
lines = [f"{prefix}TX/autonomous report model candidate:"]
lines.append(f"{prefix}- {entry} -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from {source}; byte5 is 0x5A XOR checksum")
overlay = _object_list(value.get("observed_capture_overlay_candidates"))
if overlay:
observed = []
for item in overlay[:3]:
name = item.get("name_candidate") or "observed_report_candidate"
frames = ", ".join(str(frame) for frame in item.get("observed_frames_hex", []) if frame)
if frames:
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)}")
evidence = _hex_join(value.get("evidence_addresses_hex"))
if opts.include_evidence and evidence:
lines.append(f"{prefix}- evidence: {evidence}")
return lines
def _periodic_resend_comment_lines(
value: object,
opts: SerialPseudocodeOptions,
*,
prefix: str,
) -> list[str]:
if not isinstance(value, dict):
return []
lines = [f"{prefix}heartbeat/periodic resend candidate:"]
period = value.get("period_timer")
if isinstance(period, dict):
lines.append(
f"{prefix}- F9C6 reload {period.get('reload_value_hex', '?')}: "
f"{_comment_text(str(period.get('summary') or 'period timer'))}",
)
countdown = value.get("resend_countdown")
if isinstance(countdown, dict):
lines.append(
f"{prefix}- F9C8 reload {countdown.get('reload_value_hex', '?')}: "
f"{_comment_text(str(countdown.get('summary') or 'resend countdown'))}",
)
pending = value.get("pending_mask")
if isinstance(pending, dict):
lines.append(
f"{prefix}- FAA3 mask {pending.get('mask_hex', '?')}: "
f"{_comment_text(str(pending.get('summary') or 'pending mask'))}",
)
resend = value.get("resend_path")
if isinstance(resend, dict):
lines.append(
f"{prefix}- BED5 resend path: {_comment_text(str(resend.get('summary') or 'candidate resend path'))}",
)
evidence = _hex_join(value.get("evidence_addresses_hex"))
if opts.include_evidence and evidence:
lines.append(f"{prefix}- evidence: {evidence}")
return lines
def _timer_architecture_comment_lines(
protocol: JsonObject,
opts: SerialPseudocodeOptions,
*,
prefix: str,
) -> list[str]:
model = _timer_architecture_model(protocol)
if not model:
return []
lines = [f"{prefix}interrupt/timer architecture candidate:"]
sources = _timer_source_models(model)
if sources:
for source_model in sources:
source = str(source_model.get("source") or "timer")
handler = str(source_model.get("handler_address_hex") or source_model.get("vector_address_hex") or "")
details = f" {handler}" if handler else ""
summary = _comment_text(str(source_model.get("summary") or "appears to be a periodic tick ISR for serial counters."))
lines.append(f"{prefix}- {source}{details}: {summary}")
else:
vector = str(model.get("vector_address_hex") or model.get("handler_address_hex") or "H'BEEA")
source = str(model.get("source") or "FRT1 OCIA")
lines.append(
f"{prefix}- {source} {vector} appears to be a periodic tick ISR for serial gate/cadence counters.",
)
counters = _timer_counter_models(model)
for counter in counters:
address = counter.get("address_hex") or _h(_int_field(counter, "address", 0))
name = counter.get("name_candidate") or "counter_candidate"
role = _comment_text(str(counter.get("role") or counter.get("summary") or "candidate decrementing counter"))
lines.append(f"{prefix}- {address} {name}: {role}")
evidence = _hex_join(model.get("evidence_addresses_hex"))
if opts.include_evidence and evidence:
lines.append(f"{prefix}- evidence: {evidence}")
return lines
def _timer_architecture_function_lines(protocol: JsonObject) -> list[str]:
model = _timer_architecture_model(protocol)
if not model:
return []
sources = _timer_source_models(model)
if sources:
lines: list[str] = []
for source_model in sources:
counters = _timer_counter_models(source_model)
if not counters:
continue
source = str(source_model.get("source") or "timer")
handler = str(source_model.get("handler_address_hex") or source_model.get("vector_address_hex") or "")
lines.extend(
_timer_tick_function_lines(
_timer_source_function_name(source),
counters,
f"Candidate periodic tick at {handler or source}: decrement nonzero serial counters.",
)
)
return lines
counters = _timer_counter_models(model)
if not counters:
return []
return _timer_tick_function_lines(
"frt1_ocia_candidate_tick_isr",
counters,
"Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters.",
)
def _timer_tick_function_lines(function_name: str, counters: list[JsonObject], summary: str) -> list[str]:
lines = [
f"void {function_name}(void)",
"{",
f" /* {_comment_text(summary)} */",
]
for counter in counters:
address = _int_field(counter, "address", 0)
if address == 0:
continue
name = _safe_identifier(str(counter.get("name_candidate") or f"counter_{address:04X}")).upper()
lines.extend(
[
f" /* {name}: {_comment_text(str(counter.get('role') or counter.get('summary') or 'candidate counter'))} */",
f" if (MEM8[{_c_hex(address)}] != 0u) {{",
f" MEM8[{_c_hex(address)}] = (u8)(MEM8[{_c_hex(address)}] - 1u);",
" }",
"",
],
)
lines.extend(["}", ""])
return lines
def _timer_source_models(model: JsonObject) -> list[JsonObject]:
return _object_list(model.get("sources"))
def _timer_source_function_name(source: str) -> str:
root = _safe_identifier(source.lower().replace(" ", "_"))
return f"{root}_candidate_tick_isr"
def _timer_architecture_model(protocol: JsonObject) -> JsonObject:
model = protocol.get("timer_interrupt_model")
if isinstance(model, dict):
return model
if isinstance(protocol.get("gate_queue_model"), dict) or isinstance(protocol.get("periodic_resend_model"), dict):
return {
"source": "FRT1/FRT2 OCIA",
"vector_address_hex": "H'BEEA/H'BF23",
"counters": [
{
"address": 0xF9C0,
"address_hex": "H'F9C0",
"name_candidate": "tx_report_gate_counter_candidate",
"role": "candidate gate counter used before entering the report builder.",
},
{
"address": 0xF9C1,
"address_hex": "H'F9C1",
"name_candidate": "rx_interbyte_timeout_candidate",
"role": "candidate RX interbyte timeout counter.",
},
{
"address": 0xF9C6,
"address_hex": "H'F9C6",
"name_candidate": "periodic_resend_cadence_counter_candidate",
"role": "candidate periodic resend/heartbeat cadence counter.",
},
{
"address": 0xF9C4,
"address_hex": "H'F9C4",
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
"role": "candidate idle/default report enqueue countdown.",
},
],
}
return {}
def _timer_counter_models(model: JsonObject) -> list[JsonObject]:
counters = _object_list(model.get("counters"))
if counters:
return counters
return [
{
"address": 0xF9C0,
"address_hex": "H'F9C0",
"name_candidate": "tx_report_gate_counter_candidate",
"role": "candidate gate counter used before entering the report builder.",
},
{
"address": 0xF9C1,
"address_hex": "H'F9C1",
"name_candidate": "rx_interbyte_timeout_candidate",
"role": "candidate RX interbyte timeout counter.",
},
{
"address": 0xF9C6,
"address_hex": "H'F9C6",
"name_candidate": "periodic_resend_cadence_counter_candidate",
"role": "candidate periodic resend/heartbeat cadence counter.",
},
{
"address": 0xF9C4,
"address_hex": "H'F9C4",
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
"role": "candidate idle/default report enqueue countdown.",
},
]
def _command_effect_switch_lines(command: JsonObject) -> list[str]:
effects = _object_list(command.get("effects"))[:3]
lines = []
@@ -699,6 +1063,7 @@ def _tx_functions(candidate: JsonObject, opts: SerialPseudocodeOptions) -> list[
" /* wait for transmit data register empty */",
" }",
"",
" /* First byte is sent synchronously; TIE enables TXI for the remaining bytes. */",
" SCI1_TDR = TX_FRAME(0);",
" TX_INDEX = 1u;",
" SCI1_SSR &= (u8)~SCI_SSR_TDRE;",
@@ -707,6 +1072,11 @@ def _tx_functions(candidate: JsonObject, opts: SerialPseudocodeOptions) -> list[
"",
"void sci1_txi_candidate_isr(void)",
"{",
" /* TXI runs after hardware reasserts SSR.TDRE for the next transmit byte. */",
" if ((SCI1_SSR & SCI_SSR_TDRE) == 0u) {",
" return;",
" }",
"",
" if (TX_INDEX < TX_FRAME_LENGTH) {",
" SCI1_TDR = TX_FRAME(TX_INDEX);",
" TX_INDEX = (u8)(TX_INDEX + 1u);",

View File

@@ -15,6 +15,12 @@ TX_BUFFER_END = TX_CHECKSUM_ADDRESS
TX_INDEX_ADDRESS = 0xF9C2
TX_FRAME_LENGTH = 6
CHECKSUM_SEED = 0x5A
FRT1_OCIA_VECTOR_ADDRESS = 0x0062
FRT1_OCIA_ISR_ADDRESS = 0xBEEA
FRT1_TCSR_ADDRESS = 0xFE91
POST_TX_REPORT_DELAY_ADDRESS = 0xF9C0
SECONDARY_TX_REPORT_DELAY_ADDRESS = 0xF9C1
PERIODIC_REPORT_COUNTDOWN_ADDRESS = 0xF9C6
RX_FRAME_START = 0xF860
RX_CHECKSUM_ADDRESS = 0xF865
@@ -60,7 +66,7 @@ def analyze_serial_reconstruction(
) -> dict[str, object]:
"""Reconstruct conservative serial-frame candidates from independent evidence."""
ordered = _instruction_sequence(instructions)
evidence = _collect_tx_evidence(ordered) + _collect_rx_evidence(ordered)
evidence = _collect_tx_evidence(ordered) + _collect_rx_evidence(ordered) + _collect_timer_role_evidence(ordered)
candidates = [
candidate
for candidate in (
@@ -69,6 +75,7 @@ def analyze_serial_reconstruction(
)
if candidate is not None
]
ram_roles = _ram_roles_from_evidence(evidence)
annotations: dict[int, list[str]] = {}
instruction_metadata: dict[int, list[dict[str, object]]] = {}
@@ -88,9 +95,25 @@ def analyze_serial_reconstruction(
_instruction_metadata(candidate, item, address, comment),
)
for role in ram_roles:
for item in role["evidence"]:
if not isinstance(item, Mapping):
continue
comment = _comment_for_ram_role(role, item)
for address in item.get("addresses", []):
if not isinstance(address, int):
continue
annotations.setdefault(address, [])
if comment not in annotations[address]:
annotations[address].append(comment)
instruction_metadata.setdefault(address, []).append(
_ram_role_instruction_metadata(role, item, address, comment),
)
return {
"kind": "serial_reconstruction",
"candidates": candidates,
"ram_roles": ram_roles,
"evidence": evidence,
"required_evidence": {
"tx": list(_TX_REQUIRED_EVIDENCE),
@@ -139,6 +162,7 @@ def serial_reconstruction_json_payload(analysis: Mapping[str, object] | None) ->
return {
"kind": "serial_reconstruction",
"candidates": [],
"ram_roles": [],
"evidence": [],
"required_evidence": {
"tx": list(_TX_REQUIRED_EVIDENCE),
@@ -148,6 +172,7 @@ def serial_reconstruction_json_payload(analysis: Mapping[str, object] | None) ->
return {
"kind": analysis.get("kind", "serial_reconstruction"),
"candidates": analysis.get("candidates", []),
"ram_roles": analysis.get("ram_roles", []),
"evidence": analysis.get("evidence", []),
"required_evidence": analysis.get(
"required_evidence",
@@ -408,6 +433,69 @@ def _collect_rx_evidence(ordered: list[Instruction]) -> list[dict[str, object]]:
return evidence
def _collect_timer_role_evidence(ordered: list[Instruction]) -> list[dict[str, object]]:
evidence: list[dict[str, object]] = []
tick_ack = [
ins
for ins in ordered
if ins.address == FRT1_OCIA_ISR_ADDRESS and _is_bclr_bit(ins, FRT1_TCSR_ADDRESS, 5)
]
if tick_ack:
evidence.append(
_evidence(
"frt1_ocia_periodic_tick_isr",
tick_ack,
summary=(
f"candidate periodic tick ISR at {h16(FRT1_OCIA_ISR_ADDRESS)} "
f"for FRT1 OCIA vector {h16(FRT1_OCIA_VECTOR_ADDRESS)} clears OCFA"
),
vector_address=FRT1_OCIA_VECTOR_ADDRESS,
vector_address_hex=h16(FRT1_OCIA_VECTOR_ADDRESS),
isr_address=FRT1_OCIA_ISR_ADDRESS,
isr_address_hex=h16(FRT1_OCIA_ISR_ADDRESS),
),
)
for role_name, address, width_bits, summary in (
(
"post_tx_report_delay",
POST_TX_REPORT_DELAY_ADDRESS,
8,
"candidate post-TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR",
),
(
"secondary_tx_report_delay",
SECONDARY_TX_REPORT_DELAY_ADDRESS,
8,
"candidate secondary TX/report delay timer is decremented by the FRT1 OCIA periodic tick ISR",
),
(
"periodic_report_countdown",
PERIODIC_REPORT_COUNTDOWN_ADDRESS,
16,
"candidate periodic report countdown is decremented by the FRT1 OCIA periodic tick ISR",
),
):
sequence = _timer_tick_decrement_sequence(ordered, address)
if sequence:
evidence.append(
_evidence(
f"{role_name}_tick_decrement",
sequence,
summary=summary,
role_name=role_name,
ram_address=address,
ram_address_hex=h16(address),
width_bits=width_bits,
isr_address=FRT1_OCIA_ISR_ADDRESS,
isr_address_hex=h16(FRT1_OCIA_ISR_ADDRESS),
),
)
return evidence
def _tx_candidate_from_evidence(evidence: list[dict[str, object]]) -> dict[str, object] | None:
evidence_by_key = {str(item["kind"]): item for item in evidence}
missing = [key for key in _TX_REQUIRED_EVIDENCE if key not in evidence_by_key]
@@ -436,6 +524,42 @@ def _tx_candidate_from_evidence(evidence: list[dict[str, object]]) -> dict[str,
"checksum_seed": CHECKSUM_SEED,
"checksum_seed_hex": h16(CHECKSUM_SEED),
"checksum_formula": "checksum = 0x5A ^ byte0 ^ byte1 ^ byte2 ^ byte3 ^ byte4",
"roles": [
{
"name": "tx_frame",
"address": TX_BUFFER_START,
"address_hex": h16(TX_BUFFER_START),
"end_address": TX_BUFFER_END,
"end_address_hex": h16(TX_BUFFER_END),
"summary": "evidence-supported candidate SCI1 TX frame buffer",
},
{
"name": "tx_checksum",
"address": TX_CHECKSUM_ADDRESS,
"address_hex": h16(TX_CHECKSUM_ADDRESS),
"checksum_seed": CHECKSUM_SEED,
"checksum_seed_hex": h16(CHECKSUM_SEED),
"summary": "evidence-supported candidate SCI1 TX XOR checksum byte",
},
{
"name": "tx_index",
"address": TX_INDEX_ADDRESS,
"address_hex": h16(TX_INDEX_ADDRESS),
"summary": "evidence-supported candidate SCI1 TX frame index",
},
],
"tx_path": {
"kind": "interrupt_driven_txi",
"initial_tdr_write_address": _last_address(evidence_by_key["initial_send_from_buffer_start"]),
"initial_tdr_write_address_hex": h16(_last_address(evidence_by_key["initial_send_from_buffer_start"])),
"txi_indexed_tdr_write_address": _last_address(evidence_by_key["tx_isr_indexed_send"]),
"txi_indexed_tdr_write_address_hex": h16(_last_address(evidence_by_key["tx_isr_indexed_send"])),
"summary": (
"initial byte is written from the TX frame buffer, then subsequent bytes are sent "
"by the TXI path when TDRE is reasserted"
),
"tdre_caveat": "TDRE reassertion is hardware/emulator timing context; static evidence is the indexed TXI send path.",
},
"confidence": "high",
"confidence_score": 0.95,
"confidence_reason": "all required independent evidence groups were observed",
@@ -462,6 +586,46 @@ def _tx_candidate_from_evidence(evidence: list[dict[str, object]]) -> dict[str,
return candidate
def _ram_roles_from_evidence(evidence: list[dict[str, object]]) -> list[dict[str, object]]:
evidence_by_key = {str(item["kind"]): item for item in evidence}
tick = evidence_by_key.get("frt1_ocia_periodic_tick_isr")
roles: list[dict[str, object]] = []
for role_name, address, width_bits in (
("post_tx_report_delay", POST_TX_REPORT_DELAY_ADDRESS, 8),
("secondary_tx_report_delay", SECONDARY_TX_REPORT_DELAY_ADDRESS, 8),
("periodic_report_countdown", PERIODIC_REPORT_COUNTDOWN_ADDRESS, 16),
):
decrement = evidence_by_key.get(f"{role_name}_tick_decrement")
if decrement is None:
continue
role_evidence = [item for item in (tick, decrement) if isinstance(item, Mapping)]
roles.append(
{
"kind": "candidate_ram_role",
"name": role_name,
"address": address,
"address_hex": h16(address),
"width_bits": width_bits,
"confidence": "candidate/evidence-supported",
"summary": (
f"{role_name} at {h16(address)} is a candidate/evidence-supported RAM timer "
f"role; FRT1 OCIA tick ISR {h16(FRT1_OCIA_ISR_ADDRESS)} decrements it"
),
"caveat": "role name is evidence-supported from static references plus emulator-guided timer behavior, not a proved firmware symbol",
"evidence": role_evidence,
"evidence_addresses": {
str(item["kind"]): list(item["addresses"])
for item in role_evidence
},
"evidence_addresses_hex": {
str(item["kind"]): list(item["addresses_hex"])
for item in role_evidence
},
},
)
return roles
def _rx_candidate_from_evidence(evidence: list[dict[str, object]]) -> dict[str, object] | None:
evidence_by_key = {str(item["kind"]): item for item in evidence}
missing = [key for key in _RX_REQUIRED_EVIDENCE if key not in evidence_by_key]
@@ -572,6 +736,35 @@ def _instruction_metadata(
}
def _comment_for_ram_role(role: Mapping[str, object], item: Mapping[str, object]) -> str:
return (
f"candidate/evidence-supported RAM role {role['name']} at {role['address_hex']}; "
f"evidence: {item['summary']}; confidence {role['confidence']}"
)
def _ram_role_instruction_metadata(
role: Mapping[str, object],
item: Mapping[str, object],
address: int,
comment: str,
) -> dict[str, object]:
return {
"address": address,
"action": "serial_reconstruction_ram_role",
"role_name": role["name"],
"role_kind": role["kind"],
"role_address": role["address"],
"role_address_hex": role["address_hex"],
"evidence": item["kind"],
"evidence_summary": item["summary"],
"evidence_addresses": list(item["addresses"]),
"evidence_addresses_hex": list(item["addresses_hex"]),
"confidence": role["confidence"],
"comment": comment,
}
def _buffer_region_references(ordered: list[Instruction]) -> list[Instruction]:
return [ins for ins in ordered if _buffer_refs(ins)]
@@ -761,6 +954,31 @@ def _rx_xor_checksum_validation(ordered: list[Instruction]) -> list[Instruction]
return []
def _timer_tick_decrement_sequence(ordered: list[Instruction], address: int) -> list[Instruction]:
for index, ins in enumerate(ordered):
if not (
ins.address >= FRT1_OCIA_ISR_ADDRESS
and _mnemonic_root(ins.mnemonic) == "TST"
and address in ins.references
):
continue
window = ordered[index + 1 : index + 5]
decrement = next(
(
candidate
for candidate in window
if candidate.address >= FRT1_OCIA_ISR_ADDRESS
and _is_write_to_address(candidate, address)
and _mnemonic_root(candidate.mnemonic) in {"ADD:Q", "ADD:G", "ADDS", "SUB", "SUBS"}
and _immediate_source_value(candidate.operands) in {0xFFFF, 0xFF, 1}
),
None,
)
if decrement is not None and "-1" in decrement.operands:
return [ins, decrement]
return []
def _rx_rdrf_clear_before_rdr_read(ordered: list[Instruction]) -> list[Instruction]:
for index, ins in enumerate(ordered):
if not _is_bclr_bit(ins, SCI1_SSR_ADDRESS, 6):
@@ -950,6 +1168,13 @@ def _dedupe_ints(values: Iterable[int]) -> list[int]:
return output
def _last_address(item: Mapping[str, object]) -> int:
addresses = item.get("addresses", [])
if not isinstance(addresses, list) or not addresses:
return 0
return int(addresses[-1])
def _instruction_sequence(
instructions: Mapping[int, Instruction] | Iterable[Instruction],
) -> list[Instruction]:
@@ -997,6 +1222,7 @@ def _operand_mentions_address(operand: str, address: int) -> bool:
SCI1_TDR_ADDRESS: ("SCI1_TDR",),
SCI1_SSR_ADDRESS: ("SCI1_SSR",),
SCI1_RDR_ADDRESS: ("SCI1_RDR",),
FRT1_TCSR_ADDRESS: ("FRT1_TCSR",),
TX_BUFFER_START: ("TX_BUFFER",),
TX_CHECKSUM_ADDRESS: ("TX_CHECKSUM",),
TX_INDEX_ADDRESS: ("TX_INDEX",),

View File

@@ -21,6 +21,19 @@ TX_CHECKSUM_ADDRESS = TX_FRAME_END
SEND_BUILDER_ADDRESS = 0xBA26
SEND_BUILDER_LABEL = "loc_BA26"
AUTONOMOUS_TX_REPORT_CALL = 0xBB43
AUTONOMOUS_TX_REPORT_LABEL = "loc_BB43"
MAIN_REPORT_GATE_ENTRY = 0x3FD3
MAIN_REPORT_GATE_CALL = 0x3FEB
SESSION_GATE_ENTRY = 0x3FEF
IDLE_REPORT_GATE_ENTRY = 0x4046
IDLE_REPORT_QUEUE_WRITE = 0x4067
IDLE_REPORT_GATE_END = 0x4070
QUEUE_REPORT_ENTRY = 0xBAF2
RESEND_GATE_ENTRY = 0xBE9E
PERIODIC_RESEND_ENTRY = 0xBED5
FRT1_OCIA_ENTRY = 0xBEEA
FRT2_OCIA_ENTRY = 0xBF23
INDEX_DECODER_ADDRESS = 0x622B
INDEX_DECODER_LABEL = "loc_622B"
CHECKSUM_SEED = 0x5A
@@ -74,8 +87,31 @@ STATE_VARIABLES = {
0xF9B5: "event_queue_write_or_pending_cursor_candidate",
0xF9B9: "event_queue_base_or_current_slot_candidate",
0xF9C0: "serial_tx_busy_timer_candidate",
0xF9C4: "idle_heartbeat_gate_countdown_candidate",
0xF9C5: "rx_session_timeout_candidate",
0xF9C6: "autonomous_report_period_timer_candidate",
0xF9C8: "autonomous_report_resend_countdown_candidate",
}
OBSERVED_TX_REPORT_OVERLAY = [
{
"logical_index": 0x0000,
"name_candidate": "heartbeat_or_idle_report_candidate",
"observed_frames_hex": ["00 00 00 00 80 DA"],
"observed_period_ms_candidate": 700,
},
{
"logical_index": 0x0015,
"name_candidate": "call_button_report_candidate",
"observed_frames_hex": ["00 00 15 80 00 CF", "00 00 15 00 00 4F"],
},
{
"logical_index": 0x0007,
"name_candidate": "camera_power_report_candidate",
"observed_frames_hex": ["00 00 07 80 00 DD"],
},
]
def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"""Infer conservative SCI1 frame/command semantics from decompiler JSON."""
@@ -100,6 +136,10 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"table_map_candidates": [],
"state_variable_candidates": [],
"retry_error_model": None,
"gate_queue_model": None,
"tx_report_model": None,
"periodic_resend_model": None,
"timer_interrupt_model": None,
"confidence": "low",
"confidence_score": 0.0,
"caveat": "No protocol semantics are emitted without both RX and TX serial reconstruction candidates.",
@@ -113,6 +153,10 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
logical_tables = _logical_table_map_candidates(ordered)
state_variables = _state_variable_candidates(ordered)
retry_error_model = _retry_error_model(ordered, responses)
gate_queue_model = _gate_queue_model(ordered, commands)
tx_report_model = _tx_report_model(ordered, responses)
periodic_resend_model = _periodic_resend_model(ordered, responses)
timer_interrupt_model = _timer_interrupt_model(ordered)
evidence = _top_level_evidence(ordered, dispatch, responses, rx_candidate, tx_candidate)
confidence_score = _confidence_score(frame_supported, dispatch, responses, commands)
@@ -164,6 +208,10 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"rx_fields": _rx_field_candidates(ordered, dispatch),
"response_builders": _response_builder_aliases(responses),
"retry_error_model": retry_error_model,
"gate_queue_model": gate_queue_model,
"tx_report_model": tx_report_model,
"periodic_resend_model": periodic_resend_model,
"timer_interrupt_model": timer_interrupt_model,
"evidence": evidence,
}
return {
@@ -181,6 +229,10 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"table_map_candidates": protocol["table_map_candidates"],
"state_variable_candidates": protocol["state_variable_candidates"],
"retry_error_model": protocol["retry_error_model"],
"gate_queue_model": protocol["gate_queue_model"],
"tx_report_model": protocol["tx_report_model"],
"periodic_resend_model": protocol["periodic_resend_model"],
"timer_interrupt_model": protocol["timer_interrupt_model"],
"confidence": protocol["confidence"],
"confidence_score": protocol["confidence_score"],
"caveat": protocol["caveat"],
@@ -1267,10 +1319,17 @@ def _logical_table_map_candidates(ordered: list[JsonObject]) -> list[JsonObject]
def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
candidates: list[JsonObject] = []
state_regions = [
(SERIAL_HANDLER_START, SERIAL_HANDLER_END),
(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END),
(0x40E0, 0x40E0),
(FRT1_OCIA_ENTRY, 0xBF08),
(FRT2_OCIA_ENTRY, 0xBF37),
]
serial_region = [
ins
for ins in ordered
if SERIAL_HANDLER_START <= int(ins.get("address", -1)) <= SERIAL_HANDLER_END
if any(start <= int(ins.get("address", -1)) <= end for start, end in state_regions)
]
if not any(
_has_ref_in_range(ins, min(STATE_VARIABLES), max(STATE_VARIABLES))
@@ -1325,8 +1384,8 @@ def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
"evidence_addresses_hex": _hlist(evidence),
"confidence": "candidate-medium",
"caveat": (
"Role is inferred from references in the serial handler region and remains "
"a state-variable candidate."
"Role is inferred from references in serial handler, gate, and timer regions "
"and remains a state-variable candidate."
),
}
)
@@ -1576,6 +1635,499 @@ def _retry_error_model(ordered: list[JsonObject], responses: list[JsonObject]) -
}
def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) -> JsonObject | None:
evidence = _dedupe_ints(
_addresses_in_ranges(ordered, [(MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)], MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)
+ _addresses_in_ranges(ordered, [(SESSION_GATE_ENTRY, 0x4007)], SESSION_GATE_ENTRY, 0x4007)
+ _addresses_in_ranges(ordered, [(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)], IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)
+ _addresses_in_ranges(ordered, [(QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)], QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)
+ _addresses_in_ranges(ordered, [(RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)], RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)
)
command_ack_values = [
int(command["command_value"])
for command in commands
if command.get("command_value") in {0x05, 0x06}
]
if not evidence and not command_ack_values:
return None
return {
"kind": "serial_gate_queue_state_machine_candidate",
"summary": (
"Conservative model for autonomous report gating, queue cursor comparison, "
"periodic resend, and RX/session side effects."
),
"predicates": [
{
"name": "main_loop_may_enter_report_builder",
"entry_label": "loc_3FD3",
"target_label": "loc_BAF2",
"condition_candidate": (
"FAA2 == 0 && F9C0 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0))"
),
"summary": "Main-loop report gate; session must be idle, TX busy timer clear, and RX gate open.",
"state_addresses_hex": [_h16(0xFAA2), _h16(0xFAA5), _h16(0xF9C3), _h16(0xF9C0)],
"evidence_addresses": _addresses_in_ranges(
ordered,
[(MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)],
MAIN_REPORT_GATE_ENTRY,
MAIN_REPORT_GATE_CALL,
),
},
{
"name": "idle_heartbeat_report_may_enqueue",
"entry_label": "loc_4046",
"target_label": "loc_4067",
"condition_candidate": (
"F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5"
),
"summary": (
"Idle/default report gate; when the FRT2 countdown clears and the queue is "
"empty, loc_4046 can enqueue H'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(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)],
IDLE_REPORT_GATE_ENTRY,
IDLE_REPORT_GATE_END,
),
},
{
"name": "queue_has_pending_report",
"entry_label": "loc_BAF2",
"condition_candidate": "F9B5 != F9B0",
"summary": "Queue/pending cursor gate; non-empty state stages through BB43 before loc_BA26.",
"state_addresses_hex": [_h16(0xF9B5), _h16(0xF9B0)],
"staging_path": ["loc_BAF2", "loc_BB43", "loc_BA26"],
"evidence_addresses": _addresses_in_ranges(
ordered,
[(QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)],
QUEUE_REPORT_ENTRY,
AUTONOMOUS_TX_REPORT_CALL,
),
},
{
"name": "periodic_resend_may_fire",
"entry_label": "loc_BE9E",
"target_label": "loc_BED5",
"condition_candidate": (
"(FAA5 & FAA3 & 0x80) != 0 && F9C6 == 0 && F9C8 != 0 after countdown"
),
"summary": "Resend gate masks pending state with FAA5, checks F9C6/F9C8, then calls BA26 at BED5.",
"state_addresses_hex": [_h16(0xFAA5), _h16(0xFAA3), _h16(0xF9C6), _h16(0xF9C8)],
"evidence_addresses": _addresses_in_ranges(
ordered,
[(RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)],
RESEND_GATE_ENTRY,
PERIODIC_RESEND_ENTRY,
),
},
],
"session_effects": [
{
"name": "rx_completion_sets_session_timer",
"summary": "RX completion sets F9C5 (observed reload H'14) after the sixth byte is captured.",
"state_addresses_hex": [_h16(0xF9C5)],
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C5, 0x14),
},
{
"name": "session_timeout_clears_gate_and_queue",
"entry_label": "loc_3FEF",
"summary": "When F9C5 is clear, loc_3FEF clears F9B5/F9B0 and clears FAA5.bit7; when nonzero, it sets FAA5.bit7.",
"state_addresses_hex": [_h16(0xF9C5), _h16(0xF9B5), _h16(0xF9B0), _h16(0xFAA5)],
"evidence_addresses": _addresses_in_ranges(
ordered,
[(SESSION_GATE_ENTRY, 0x4007)],
SESSION_GATE_ENTRY,
0x4007,
),
},
{
"name": "idle_heartbeat_gate_initial_delay_loaded",
"summary": "Startup/init loads F9C4 with H'14 before the first idle/default report can be queued.",
"state_addresses_hex": [_h16(0xF9C4)],
"reload_value_hex": _h16(0x14, width=2),
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C4, 0x14),
},
{
"name": "idle_heartbeat_gate_post_send_delay_loaded",
"summary": "loc_BA26 reloads F9C4 with H'07 after each send, matching the observed heartbeat spacing.",
"state_addresses_hex": [_h16(0xF9C4)],
"reload_value_hex": _h16(0x07, width=2),
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C4, 0x07),
},
{
"name": "host_ack_can_advance_queue",
"summary": "Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.",
"command_values_hex": [_h16(value, width=2) for value in command_ack_values],
"state_addresses_hex": [_h16(0xF9B5)],
"evidence_addresses": _dedupe_ints(
addr
for command in commands
if command.get("command_value") in {0x05, 0x06}
for addr in command.get("evidence_addresses", [])
if isinstance(addr, int)
),
},
],
"caveat": (
"Many panel controls may require host/session traffic before reporting. Observed "
"autonomous call/camera-power indexes are runtime/capture overlays, not ROM constants."
),
"confidence": "candidate-medium",
"evidence_addresses": evidence,
"evidence_addresses_hex": _hlist(evidence),
}
def _timer_interrupt_model(ordered: list[JsonObject]) -> JsonObject | None:
present_addresses = {int(ins["address"]) for ins in ordered if isinstance(ins.get("address"), int)}
frt1_evidence = _dedupe_ints(
_addresses_in_ranges(ordered, [(FRT1_OCIA_ENTRY, 0xBF08)], FRT1_OCIA_ENTRY, 0xBF08)
)
frt2_evidence = _dedupe_ints(
_addresses_in_ranges(ordered, [(FRT2_OCIA_ENTRY, 0xBF37)], FRT2_OCIA_ENTRY, 0xBF37)
)
sources: list[JsonObject] = []
if frt1_evidence:
frt1_counters = [
counter
for counter in (
_timer_counter(0xF9C0, "tx_report_gate_counter_candidate", "candidate gate counter used before entering the report builder.", 0xBEF4),
_timer_counter(0xF9C1, "rx_interbyte_timeout_candidate", "candidate RX interbyte timeout counter.", 0xBEFE),
_timer_counter(0xF9C6, "periodic_resend_cadence_counter_candidate", "candidate periodic resend/heartbeat cadence counter.", 0xBF08),
)
if int(counter["evidence_address"]) in present_addresses
]
sources.append(
{
"source": "FRT1 OCIA",
"vector_address_hex": _h16(0x0062),
"handler_address": FRT1_OCIA_ENTRY,
"handler_address_hex": _h16(FRT1_OCIA_ENTRY),
"summary": "Candidate periodic tick ISR for serial busy, interbyte, and resend counters.",
"counters": frt1_counters,
"evidence_addresses": frt1_evidence,
"evidence_addresses_hex": _hlist(frt1_evidence),
}
)
if frt2_evidence:
frt2_counters = [
counter
for counter in (
_timer_counter(0xF9C4, "idle_heartbeat_gate_countdown_candidate", "candidate idle/default report enqueue countdown.", 0xBF2D),
_timer_counter(0xF9C5, "rx_session_timeout_candidate", "candidate RX/session maintenance timeout counter.", 0xBF37),
)
if int(counter["evidence_address"]) in present_addresses
]
sources.append(
{
"source": "FRT2 OCIA",
"vector_address_hex": _h16(0x006A),
"handler_address": FRT2_OCIA_ENTRY,
"handler_address_hex": _h16(FRT2_OCIA_ENTRY),
"summary": "Candidate periodic tick ISR for idle heartbeat/report and RX session counters.",
"clock_select": "CKS1=1 CKS0=0 => phi/32",
"ocra_value_hex": "H'7A12",
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
"counters": frt2_counters,
"evidence_addresses": frt2_evidence,
"evidence_addresses_hex": _hlist(frt2_evidence),
}
)
if not sources:
return None
counters = _dedupe_timer_counters(
counter
for source in sources
for counter in source.get("counters", [])
if isinstance(counter, dict)
)
evidence = _dedupe_ints(
addr
for source in sources
for addr in source.get("evidence_addresses", [])
if isinstance(addr, int)
)
return {
"kind": "timer_interrupt_model_candidate",
"source": " / ".join(str(source["source"]) for source in sources),
"summary": "FRT compare-match handlers decrement serial gate, timeout, and cadence counters.",
"sources": sources,
"counters": counters,
"evidence_addresses": evidence,
"evidence_addresses_hex": _hlist(evidence),
"confidence": "candidate-medium",
}
def _timer_counter(address: int, name: str, role: str, evidence_address: int) -> JsonObject:
return {
"address": address,
"address_hex": _h16(address),
"name_candidate": name,
"role": role,
"evidence_address": evidence_address,
"evidence_address_hex": _h16(evidence_address),
}
def _dedupe_timer_counters(counters: Iterable[JsonObject]) -> list[JsonObject]:
output = []
seen: set[int] = set()
for counter in counters:
address = counter.get("address")
if not isinstance(address, int) or address in seen:
continue
seen.add(address)
output.append(counter)
return output
def _tx_report_model(ordered: list[JsonObject], responses: list[JsonObject]) -> JsonObject | None:
report_responses = [
response for response in responses
if response.get("call_address") == AUTONOMOUS_TX_REPORT_CALL
]
if not report_responses:
report_responses = [
response for response in responses
if _response_reads_current_value_table(response)
and not _response_reads_rx_frame(response)
]
if not report_responses:
return None
response_ids = [
str(response["id"])
for response in report_responses
if isinstance(response.get("id"), str)
]
evidence = _dedupe_ints(
addr
for response in report_responses
for addr in response.get("evidence_addresses", [])
if isinstance(addr, int)
)
byte_roles = [
{
"offset": 0,
"field_candidate": "encoded_logical_index_or_report_id_byte0",
"source_candidate": "computed from candidate logical index/report id",
},
{
"offset": 1,
"field_candidate": "encoded_logical_index_or_report_id_byte1",
"source_candidate": "computed from candidate logical index/report id",
},
{
"offset": 2,
"field_candidate": "encoded_logical_index_or_report_id_byte2",
"source_candidate": "computed from candidate logical index/report id",
},
{
"offset": 3,
"field_candidate": "current_value_hi",
"source_candidate": "current_value_table_candidate high byte",
"table_candidate": "current_value_table_candidate",
},
{
"offset": 4,
"field_candidate": "current_value_lo",
"source_candidate": "current_value_table_candidate low byte",
"table_candidate": "current_value_table_candidate",
},
{
"offset": 5,
"field_candidate": "checksum",
"source_candidate": "0x5A XOR TX[0..4]",
},
]
return {
"kind": "bb43_to_ba26_tx_report_model_candidate",
"direction": "device_to_host_autonomous_report_candidate",
"entry_label": AUTONOMOUS_TX_REPORT_LABEL,
"entry_address": AUTONOMOUS_TX_REPORT_CALL,
"entry_address_hex": _h16(AUTONOMOUS_TX_REPORT_CALL),
"send_builder": SEND_BUILDER_LABEL,
"send_builder_address": SEND_BUILDER_ADDRESS,
"send_builder_address_hex": _h16(SEND_BUILDER_ADDRESS),
"response_candidates": _dedupe_strings(response_ids),
"summary": (
"TX report bytes 0..2 are computed encoded logical index/report id bytes, "
"bytes 3..4 come from current_value_table_candidate, and byte5 is the "
"0x5A XOR checksum."
),
"byte_roles": byte_roles,
"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 "
"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."
),
"evidence_addresses": evidence,
"evidence_addresses_hex": _hlist(evidence),
}
def _periodic_resend_model(ordered: list[JsonObject], responses: list[JsonObject]) -> JsonObject | None:
del responses
period_evidence = _state_immediate_evidence(ordered, 0xF9C6, 0x01F4)
countdown_evidence = _state_immediate_evidence(ordered, 0xF9C8, 0x14)
pending_evidence = _state_immediate_evidence(ordered, 0xFAA3, 0x80)
pending_evidence = _dedupe_ints(pending_evidence + _state_bit_evidence(ordered, 0xFAA3, 7))
resend_evidence = [
int(ins["address"])
for ins in ordered
if PERIODIC_RESEND_ENTRY <= int(ins.get("address", -1)) <= SERIAL_HANDLER_END
]
resend_send_evidence = [
int(ins["address"])
for ins in ordered
if PERIODIC_RESEND_ENTRY <= int(ins.get("address", -1)) <= SERIAL_HANDLER_END
and (_is_send_builder_call(ins) or _has_ref_in_range(ins, TX_STAGING_START, TX_FRAME_END))
]
evidence = _dedupe_ints(period_evidence + countdown_evidence + pending_evidence + resend_send_evidence)
if not evidence and not resend_evidence:
return None
return {
"kind": "autonomous_periodic_resend_model_candidate",
"period_timer": {
"address": 0xF9C6,
"address_hex": _h16(0xF9C6),
"reload_value_candidate": 0x01F4,
"reload_value_hex": _h16(0x01F4),
"summary": "Candidate periodic report/heartbeat timer reload.",
"evidence_addresses": period_evidence,
"evidence_addresses_hex": _hlist(period_evidence),
},
"resend_countdown": {
"address": 0xF9C8,
"address_hex": _h16(0xF9C8),
"reload_value_candidate": 0x14,
"reload_value_hex": _h16(0x14, width=2),
"summary": "Candidate periodic resend countdown/retry spacing value.",
"evidence_addresses": countdown_evidence,
"evidence_addresses_hex": _hlist(countdown_evidence),
},
"pending_mask": {
"address": 0xFAA3,
"address_hex": _h16(0xFAA3),
"mask_candidate": 0x80,
"mask_hex": _h16(0x80, width=2),
"summary": "Candidate bit/mask that marks an autonomous report pending.",
"evidence_addresses": pending_evidence,
"evidence_addresses_hex": _hlist(pending_evidence),
},
"resend_path": {
"entry_label": "loc_BED5",
"entry_address": PERIODIC_RESEND_ENTRY,
"entry_address_hex": _h16(PERIODIC_RESEND_ENTRY),
"summary": "Candidate periodic resend path feeding the TX staging/send-builder flow.",
"evidence_addresses": _dedupe_ints(resend_send_evidence or resend_evidence),
"evidence_addresses_hex": _hlist(resend_send_evidence or resend_evidence),
},
"evidence_addresses": evidence,
"evidence_addresses_hex": _hlist(evidence),
"confidence": "candidate-medium" if evidence else "candidate-low",
"caveat": (
"Timer and resend roles are inferred from constants/state references around F9C6, "
"F9C8, FAA3, and loc_BED5; exact scheduling units remain candidate phrasing."
),
}
def _response_reads_current_value_table(response: Mapping[str, Any]) -> bool:
schema = response.get("schema")
if not isinstance(schema, Mapping):
return False
return any(
isinstance(item, Mapping)
and isinstance(item.get("source"), Mapping)
and item["source"].get("kind") == "table"
and item["source"].get("name_candidate") == "current_value_table_candidate"
for item in schema.get("bytes", [])
)
def _response_reads_rx_frame(response: Mapping[str, Any]) -> bool:
schema = response.get("schema")
if not isinstance(schema, Mapping):
return False
return any(
isinstance(item, Mapping)
and isinstance(item.get("source"), Mapping)
and item["source"].get("kind") == "rx_frame_byte"
for item in schema.get("bytes", [])
)
def _state_immediate_evidence(ordered: list[JsonObject], state_address: int, value: int) -> list[int]:
evidence = []
for ins in ordered:
if not _has_ref_in_range(ins, state_address, state_address):
continue
source, _destination = _source_destination_operands(str(ins.get("operands", "")))
if _parse_immediate(source) == value:
evidence.append(int(ins["address"]))
return _dedupe_ints(evidence)
def _state_bit_evidence(ordered: list[JsonObject], state_address: int, bit: int) -> list[int]:
return _dedupe_ints(
int(ins["address"])
for ins in ordered
if _has_ref_in_range(ins, state_address, state_address)
and _bit_number_from_instruction(ins) == bit
)
def _send_builder_candidate(
ordered: list[JsonObject],
responses: list[JsonObject],
@@ -1687,6 +2239,28 @@ def _top_level_evidence(
"response_count": len(responses),
}
)
tx_report_responses = [
response for response in responses
if response.get("call_address") == AUTONOMOUS_TX_REPORT_CALL
]
if tx_report_responses:
addresses = _dedupe_ints(
addr
for response in tx_report_responses
for addr in response.get("evidence_addresses", [])
if isinstance(addr, int)
)
evidence.append(
{
"kind": "bb43_autonomous_tx_report_path",
"summary": (
"BB43 stages a candidate device-to-host report before loc_BA26; this is "
"separate from RX command dispatch."
),
"addresses": addresses,
"addresses_hex": _hlist(addresses),
}
)
rx_payload_reads = [
int(ins["address"])
for ins in ordered

830
h8536/table_xrefs.py Normal file
View File

@@ -0,0 +1,830 @@
from __future__ import annotations
import argparse
import json
import re
from collections.abc import Iterable, Mapping
from pathlib import Path
from typing import Any
from .formatting import h16, label_for
from .serial_semantics import DIRECT_TABLE_TO_LOGICAL_OFFSET, LOGICAL_TABLES
JsonObject = dict[str, Any]
TABLES: tuple[JsonObject, ...] = (
{
"name": "primary_value_table_candidate",
"logical_base_address": 0xE000,
"logical_range_end": 0xE3FF,
"negative_offset": 0x2000,
"element_candidate": "word_value",
"direct_addresses": [0xF900],
"direct_range_end": 0xF91F,
},
{
"name": "secondary_value_table_candidate",
"logical_base_address": 0xE400,
"logical_range_end": 0xE7FF,
"negative_offset": 0x1C00,
"element_candidate": "word_value",
"direct_addresses": [0xF940],
"direct_range_end": 0xF95F,
},
{
"name": "current_value_table_candidate",
"logical_base_address": 0xE800,
"logical_range_end": 0xEBFF,
"negative_offset": 0x1800,
"element_candidate": "word_value",
"direct_addresses": [0xF920],
"direct_range_end": 0xF93F,
},
{
"name": "flag_table_candidate",
"logical_base_address": 0xEC00,
"logical_range_end": 0xEFFF,
"negative_offset": 0x1400,
"element_candidate": "bit_flags",
"direct_addresses": [0xF980],
"direct_range_end": 0xF99F,
},
)
_TABLE_BY_NEGATIVE_OFFSET = {int(item["negative_offset"]): item for item in TABLES}
_TABLE_BY_DIRECT_ADDRESS = {
address: item
for item in TABLES
for address in item["direct_addresses"]
}
LCD_CORRELATION_TERMS = (
"CONNECT",
"CONNECT: OK",
"CONNECT: NOT ACT",
"NOT ACT",
"COMM LINK",
"COMPLETED",
)
def load_table_xref_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 analyze_table_xrefs(payload: Mapping[str, Any]) -> JsonObject:
instructions = _instruction_sequence(payload.get("instructions"))
functions = _function_ranges(payload)
semantic_accesses = _semantic_access_locations(payload)
accesses_by_table = {str(table["name"]): [] for table in TABLES}
for index, ins in enumerate(instructions):
for access in _logical_operand_accesses(instructions, index, functions, semantic_accesses):
accesses_by_table.setdefault(str(access["table"]), []).append(access)
for access in _direct_address_accesses(ins, functions, semantic_accesses):
accesses_by_table.setdefault(str(access["table"]), []).append(access)
tables: list[JsonObject] = []
for table in TABLES:
name = str(table["name"])
accesses = sorted(accesses_by_table.get(name, []), key=lambda item: int(item["instruction_address"]))
reads = sum(1 for access in accesses if access["access"] == "read")
writes = sum(1 for access in accesses if access["access"] == "write")
read_write = sum(1 for access in accesses if access["access"] == "read_write_candidate")
dynamic = sum(1 for access in accesses if access.get("index") == "dynamic")
static_offsets = sorted(
{
int(access["offset"])
for access in accesses
if isinstance(access.get("offset"), int)
}
)
tables.append(
{
"name": name,
"logical_base_address": table["logical_base_address"],
"logical_base_address_hex": h16(int(table["logical_base_address"])),
"logical_range_end": table["logical_range_end"],
"logical_range_end_hex": h16(int(table["logical_range_end"])),
"negative_offset": table["negative_offset"],
"negative_offset_hex": h16(int(table["negative_offset"])),
"element_candidate": table["element_candidate"],
"direct_addresses": table["direct_addresses"],
"direct_addresses_hex": [h16(int(address)) for address in table["direct_addresses"]],
"direct_range_end": table["direct_range_end"],
"direct_range_end_hex": h16(int(table["direct_range_end"])),
"access_count": len(accesses),
"read_count": reads,
"write_count": writes,
"read_write_candidate_count": read_write,
"dynamic_index_count": dynamic,
"static_offsets": static_offsets,
"static_offsets_hex": [h16(offset) for offset in static_offsets],
"functions": _summarize_functions(accesses),
"accesses": accesses,
}
)
return {
"kind": "table_xrefs",
"tables": tables,
"summary": {
"table_count": len(tables),
"access_count": sum(int(table["access_count"]) for table in tables),
"dynamic_index_count": sum(int(table["dynamic_index_count"]) for table in tables),
"source_instruction_count": len(instructions),
},
"lcd_correlation": _lcd_correlation_hints(payload),
"caveat": (
"Static offsets are emitted only when an index register value can be derived from "
"nearby immediate loads in the current JSON. Other indexed accesses are dynamic."
),
}
def generate_table_xref_report(payload: Mapping[str, Any], *, source_name: str = "") -> str:
analysis = analyze_table_xrefs(payload)
lines: list[str] = []
suffix = f" for {source_name}" if source_name else ""
lines.append(f"Table/Index Cross-Reference Report{suffix}")
lines.append("=" * len(lines[0]))
lines.append("")
lines.append(str(analysis["caveat"]))
lines.append("")
lines.extend(_format_lcd_correlation_lines(analysis.get("lcd_correlation")))
if lines[-1] != "":
lines.append("")
for table in analysis["tables"]:
name = str(table["name"])
direct = ", ".join(str(item) for item in table["direct_addresses_hex"])
lines.append(
f"{name} {table['logical_base_address_hex']}-{table['logical_range_end_hex']} "
f"(negative {table['negative_offset_hex']}; direct {direct}-{table['direct_range_end_hex']})"
)
lines.append(
f" accesses={table['access_count']} reads={table['read_count']} "
f"writes={table['write_count']} dynamic={table['dynamic_index_count']}"
)
offsets = table.get("static_offsets_hex") or []
if offsets:
lines.append(f" static offsets: {', '.join(str(item) for item in offsets[:16])}")
function_summaries = table.get("functions") or []
if function_summaries:
joined = ", ".join(
f"{item['label']}:{item['access_count']}" for item in function_summaries[:12]
)
lines.append(f" functions: {joined}")
accesses = table.get("accesses")
if isinstance(accesses, list) and accesses:
for access in accesses[:80]:
lines.append(f" - {_format_access_line(access)}")
if len(accesses) > 80:
lines.append(f" - ... {len(accesses) - 80} more accesses omitted")
else:
lines.append(" no references found in current JSON")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def _format_lcd_correlation_lines(value: Any) -> list[str]:
if not isinstance(value, Mapping):
return []
lines = ["LCD correlation hints"]
for hit in value.get("term_hits", []):
if not isinstance(hit, Mapping):
continue
term = hit.get("term")
count = int(hit.get("hit_count", 0))
if count:
samples = ", ".join(
f"{item['address_hex']} {item['trimmed']!r}"
for item in hit.get("hits", [])[:4]
if isinstance(item, Mapping)
)
lines.append(f" term {term!r}: {count} candidate hit(s): {samples}")
else:
lines.append(f" term {term!r}: no LCD/text candidate hits in current decompile")
builders = value.get("display_builder_targets", [])
if isinstance(builders, list) and builders:
parts = [
f"{item['target_hex']}:{item['xref_count']}"
for item in builders[:8]
if isinstance(item, Mapping)
]
lines.append(f" display builder xrefs: {', '.join(parts)}")
routines = value.get("lcd_driver_routines", [])
if isinstance(routines, list) and routines:
parts = [
f"{item['start_hex']} {item['role_hint']}"
for item in routines[:4]
if isinstance(item, Mapping)
]
lines.append(f" LCD driver routines: {', '.join(parts)}")
lines.append(
" caveat: LCD strings can be builder/script output; absence of a literal term does not disprove runtime composition."
)
return lines
def write_table_xrefs(input_path: Path, output_path: Path, *, as_json: bool = False) -> None:
payload = load_table_xref_input(input_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if as_json:
analysis = analyze_table_xrefs(payload)
analysis["source"] = str(input_path)
output_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
else:
output_path.write_text(generate_table_xref_report(payload, source_name=str(input_path)), encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Generate table/index cross-references for candidate serial protocol data tables.",
)
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_table_xrefs.txt"),
help="table cross-reference report output path",
)
parser.add_argument("--json", action="store_true", help="write structured JSON instead of text")
args = parser.parse_args(argv)
write_table_xrefs(args.input, args.out, as_json=args.json)
print(f"wrote {args.out}")
return 0
def _logical_operand_accesses(
instructions: list[JsonObject],
index: int,
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> list[JsonObject]:
ins = instructions[index]
accesses: list[JsonObject] = []
operands = str(ins.get("operands", ""))
for operand in _negative_indexed_operands(operands):
table = _TABLE_BY_NEGATIVE_OFFSET.get(int(operand["negative_offset"]))
if table is None:
continue
register = str(operand["index_register"])
known = _nearby_register_value(instructions, index, register)
offset: int | str = known if known is not None else "dynamic"
logical_address: int | None = None
if isinstance(offset, int):
logical_address = (int(table["logical_base_address"]) + offset) & 0xFFFF
access = _base_access(ins, functions, semantic_accesses)
access.update(
{
"table": table["name"],
"table_base_address": table["logical_base_address"],
"table_base_address_hex": h16(int(table["logical_base_address"])),
"kind": "logical_negative_indexed_access",
"operand": operand["operand"],
"negative_offset": operand["negative_offset"],
"negative_offset_hex": h16(int(operand["negative_offset"])),
"index_register": register,
"index": offset,
"offset": offset,
"access": _operand_access_kind(ins, str(operand["operand"])),
}
)
if logical_address is not None:
access["logical_address"] = logical_address
access["logical_address_hex"] = h16(logical_address)
accesses.append(access)
return accesses
def _direct_address_accesses(
ins: Mapping[str, Any],
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> list[JsonObject]:
accesses: list[JsonObject] = []
refs = _references(ins)
for address in refs:
logical_table = _table_for_logical_address(address)
if logical_table is not None:
accesses.append(
_direct_logical_address_access(ins, logical_table, address, functions, semantic_accesses),
)
continue
direct_table = _table_for_direct_candidate_address(address)
if direct_table is not None:
accesses.append(
_direct_candidate_address_access(ins, direct_table, address, functions, semantic_accesses),
)
return accesses
def _direct_logical_address_access(
ins: Mapping[str, Any],
table: Mapping[str, Any],
address: int,
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
base = int(table["logical_base_address"])
offset = address - base
access = _base_access(ins, functions, semantic_accesses)
access.update(
{
"table": table["name"],
"table_base_address": base,
"table_base_address_hex": h16(base),
"kind": "direct_logical_address_access",
"direct_address": address,
"direct_address_hex": h16(address),
"logical_address": address,
"logical_address_hex": h16(address),
"index": offset,
"offset": offset,
"offset_hex": h16(offset),
"access": _access_direction(ins, address) or "read_write_candidate",
}
)
return access
def _direct_candidate_address_access(
ins: Mapping[str, Any],
table: Mapping[str, Any],
address: int,
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
base = min(int(item) for item in table["direct_addresses"])
offset = address - base
access = _base_access(ins, functions, semantic_accesses)
logical_offset = DIRECT_TABLE_TO_LOGICAL_OFFSET.get(base)
access.update(
{
"table": table["name"],
"table_base_address": table["logical_base_address"],
"table_base_address_hex": h16(int(table["logical_base_address"])),
"kind": "direct_candidate_address_access",
"direct_address": address,
"direct_address_hex": h16(address),
"direct_base_address": base,
"direct_base_address_hex": h16(base),
"index": offset,
"offset": offset,
"offset_hex": h16(offset),
"access": _access_direction(ins, address) or "read_write_candidate",
}
)
if logical_offset is not None:
access["semantic_negative_offset"] = logical_offset
access["semantic_negative_offset_hex"] = h16(logical_offset)
return access
def _lcd_correlation_hints(payload: Mapping[str, Any]) -> JsonObject:
lcd_text = payload.get("lcd_text")
strings = []
if isinstance(lcd_text, Mapping) and isinstance(lcd_text.get("strings"), list):
strings = [item for item in lcd_text["strings"] if isinstance(item, Mapping)]
term_hits = []
for term in LCD_CORRELATION_TERMS:
hits = []
upper_term = term.upper()
for item in strings:
text = f"{item.get('text', '')} {item.get('trimmed', '')}".upper()
if upper_term not in text:
continue
hits.append(_lcd_string_summary(item))
term_hits.append(
{
"term": term,
"hit_count": len(hits),
"hits": hits[:24],
"status": "candidate_hits" if hits else "not_found",
}
)
builder_targets: dict[int, JsonObject] = {}
for item in strings:
for xref in item.get("xrefs", []):
if not isinstance(xref, Mapping):
continue
following = xref.get("following_bsr")
if not isinstance(following, Mapping) or not isinstance(following.get("target"), int):
continue
target = int(following["target"])
record = builder_targets.setdefault(
target,
{
"target": target,
"target_hex": h16(target),
"xref_count": 0,
"examples": [],
},
)
record["xref_count"] = int(record["xref_count"]) + 1
examples = record["examples"]
if isinstance(examples, list) and len(examples) < 8:
examples.append(
{
"text_address": item.get("address"),
"text_address_hex": h16(int(item["address"])) if isinstance(item.get("address"), int) else None,
"trimmed": item.get("trimmed"),
"xref_address": xref.get("address"),
"xref_address_hex": h16(int(xref["address"])) if isinstance(xref.get("address"), int) else None,
}
)
lcd_driver = payload.get("lcd_driver")
routines = []
if isinstance(lcd_driver, Mapping) and isinstance(lcd_driver.get("routines"), list):
for routine in lcd_driver["routines"]:
if not isinstance(routine, Mapping) or not isinstance(routine.get("start"), int):
continue
routines.append(
{
"start": routine["start"],
"start_hex": h16(int(routine["start"])),
"end": routine.get("end"),
"end_hex": h16(int(routine["end"])) if isinstance(routine.get("end"), int) else None,
"role_hint": routine.get("role_hint"),
"roles": routine.get("roles", []),
}
)
return {
"terms": list(LCD_CORRELATION_TERMS),
"term_hits": term_hits,
"display_builder_targets": sorted(
builder_targets.values(),
key=lambda item: (-int(item["xref_count"]), int(item["target"])),
),
"lcd_driver_routines": routines,
"caveat": (
"This is a static correlation helper. It reports text/script candidates and LCD driver "
"routines in the same decompile; it does not prove a protocol field directly causes a string."
),
}
def _lcd_string_summary(item: Mapping[str, Any]) -> JsonObject:
address = item.get("address")
return {
"address": address,
"address_hex": h16(int(address)) if isinstance(address, int) else None,
"text": item.get("text"),
"trimmed": item.get("trimmed"),
"confidence": item.get("confidence"),
"xref_count": item.get("xref_count", 0),
}
def _base_access(
ins: Mapping[str, Any],
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
address = int(ins["address"])
function = _function_for_address(functions, address)
access: JsonObject = {
"instruction_address": address,
"instruction_address_hex": h16(address),
"mnemonic": str(ins.get("mnemonic", "")),
"operands": str(ins.get("operands", "")),
"instruction": str(ins.get("text") or _instruction_text(ins)),
"references": _references(ins),
"references_hex": [h16(ref) for ref in _references(ins)],
"targets": _targets(ins),
"targets_hex": [h16(target) for target in _targets(ins)],
"label": _label_for_instruction(ins),
"semantic_candidates": semantic_accesses.get(address, []),
}
if function:
access["function_start"] = function["start"]
access["function_start_hex"] = h16(int(function["start"]))
access["function_label"] = function["label"]
return access
def _semantic_access_locations(payload: Mapping[str, Any]) -> dict[int, list[JsonObject]]:
locations: dict[int, list[JsonObject]] = {}
semantics = payload.get("serial_semantics")
if not isinstance(semantics, Mapping):
return locations
sources: list[Any] = []
protocols = semantics.get("protocol_semantics")
if isinstance(protocols, list):
sources.extend(protocols)
sources.append(semantics)
for source in sources:
if not isinstance(source, Mapping):
continue
for item in _table_candidate_items(source.get("table_map_candidates")):
for access in _table_candidate_items(item.get("accesses")):
address = access.get("instruction_address")
if isinstance(address, int):
locations.setdefault(address, []).append(
{
"name_candidate": item.get("name_candidate"),
"kind": item.get("kind"),
"confidence": item.get("confidence"),
}
)
return locations
def _table_candidate_items(value: Any) -> list[Mapping[str, Any]]:
if isinstance(value, Mapping):
return [item for item in value.values() if isinstance(item, Mapping)]
if isinstance(value, list):
return [item for item in value if isinstance(item, Mapping)]
return []
def _format_access_line(access: Mapping[str, Any]) -> str:
function = access.get("function_label") or "<no function>"
operand = access.get("operand") or access.get("direct_address_hex")
index = access.get("index")
if index == "dynamic":
index_text = f"index dynamic via {access.get('index_register')} operand {operand}"
else:
index_text = f"offset {h16(int(index or 0))}"
if access.get("logical_address_hex"):
index_text += f" -> {access['logical_address_hex']}"
elif access.get("direct_address_hex"):
index_text += f" at {access['direct_address_hex']}"
return (
f"{access['instruction_address_hex']} {access['access']} {index_text}; "
f"{function}; {access['instruction']}"
)
def _summarize_functions(accesses: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
summaries: dict[int, JsonObject] = {}
for access in accesses:
start = access.get("function_start")
if not isinstance(start, int):
start = -1
summary = summaries.setdefault(
start,
{
"start": start if start >= 0 else None,
"start_hex": h16(start) if start >= 0 else None,
"label": access.get("function_label") or "<no function>",
"access_count": 0,
"reads": 0,
"writes": 0,
},
)
summary["access_count"] = int(summary["access_count"]) + 1
if access.get("access") == "read":
summary["reads"] = int(summary["reads"]) + 1
elif access.get("access") == "write":
summary["writes"] = int(summary["writes"]) + 1
return sorted(summaries.values(), key=lambda item: (-int(item["access_count"]), str(item["label"])))
def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
call_graph = payload.get("call_graph")
if not isinstance(call_graph, Mapping):
return []
nodes = call_graph.get("nodes")
if not isinstance(nodes, list):
return []
ranges: list[JsonObject] = []
for node in nodes:
if not isinstance(node, Mapping):
continue
start = node.get("start")
end = node.get("end")
if isinstance(start, int) and isinstance(end, int):
ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))})
return sorted(ranges, key=lambda item: int(item["start"]))
def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None:
for function in functions:
if int(function["start"]) <= address <= int(function["end"]):
return function
return None
def _nearby_register_value(instructions: list[JsonObject], index: int, register: str) -> int | None:
register = register.upper()
for prior_index in range(index - 1, max(-1, index - 10), -1):
prior = instructions[prior_index]
source, destination = _source_destination_operands(str(prior.get("operands", "")))
if destination.upper() != register:
continue
value = _parse_immediate(source)
if value is not None:
return value
if _writes_register(prior, register):
return None
return None
def _writes_register(ins: Mapping[str, Any], register: str) -> bool:
_source, destination = _source_destination_operands(str(ins.get("operands", "")))
return destination.upper() == register
def _instruction_sequence(value: object) -> list[JsonObject]:
if isinstance(value, Mapping):
values: Iterable[Any] = value.values()
elif isinstance(value, list):
values = value
else:
values = []
return sorted(
[item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)],
key=lambda item: int(item["address"]),
)
def _label_for_instruction(ins: Mapping[str, Any]) -> str | None:
address = int(ins["address"])
for key in ("label", "target_label"):
value = ins.get(key)
if isinstance(value, str) and value:
return value
if _targets(ins):
return label_for(address)
return None
def _instruction_text(ins: Mapping[str, Any]) -> str:
operands = str(ins.get("operands", ""))
return f"{ins.get('mnemonic', '')} {operands}".strip()
def _references(ins: Mapping[str, Any]) -> list[int]:
refs = ins.get("references", [])
if not isinstance(refs, list):
return []
output: list[int] = []
for ref in refs:
if isinstance(ref, Mapping) and isinstance(ref.get("address"), int):
output.append(int(ref["address"]))
elif isinstance(ref, int):
output.append(ref)
return output
def _targets(ins: Mapping[str, Any]) -> list[int]:
targets = ins.get("targets", [])
if not isinstance(targets, list):
return []
return [int(target) for target in targets if isinstance(target, int)]
def _negative_indexed_operands(operands: str) -> list[JsonObject]:
matches: list[JsonObject] = []
for match in re.finditer(r"@\(-H'([0-9A-Fa-f]+),\s*(R[0-7])\)", operands):
offset = int(match.group(1), 16) & 0xFFFF
if offset not in LOGICAL_TABLES:
continue
matches.append(
{
"operand": match.group(0),
"negative_offset": offset,
"index_register": match.group(2).upper(),
}
)
return matches
def _table_for_logical_address(address: int) -> Mapping[str, Any] | None:
for table in TABLES:
if int(table["logical_base_address"]) <= address <= int(table["logical_range_end"]):
return table
return None
def _table_for_direct_candidate_address(address: int) -> Mapping[str, Any] | None:
for table in TABLES:
direct_addresses = [int(item) for item in table["direct_addresses"]]
if min(direct_addresses) <= address <= int(table["direct_range_end"]):
return table
return None
def _operand_access_kind(ins: Mapping[str, Any], operand: str) -> str:
root = _mnemonic_root(str(ins.get("mnemonic", "")))
source, destination = _source_destination_operands(str(ins.get("operands", "")))
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "TST"}:
return "read"
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
return "write"
if operand in destination and operand not in source:
return "write"
if operand in source and operand not in destination:
return "read"
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
return "write"
return "read_write_candidate"
def _access_direction(ins: Mapping[str, Any], address: int) -> str | None:
root = _mnemonic_root(str(ins.get("mnemonic", "")))
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}:
return "read"
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
return "write"
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
return "write"
if root in {"MOV:G", "MOV:S", "MOVTPE"}:
source, destination = _source_destination_operands(str(ins.get("operands", "")))
if _operand_mentions_address(destination, address):
return "write"
if _operand_mentions_address(source, address):
return "read"
if address in _references(ins):
if destination.startswith("@") and not _operand_mentions_any_reference(source, _references(ins)):
return "write"
if source.startswith("@") and not _operand_mentions_any_reference(destination, _references(ins)):
return "read"
if root in {"MOV:L", "MOV:F"}:
return "read"
if root == "STC":
return "write"
if root == "LDC":
return "read"
return None
def _source_destination_operands(operands: str) -> tuple[str, str]:
depth = 0
split_at: int | None = None
for index, char in enumerate(operands):
if char in "({":
depth += 1
elif char in ")}" and depth:
depth -= 1
elif char == "," and depth == 0:
split_at = index
if split_at is None:
operand = operands.strip()
return "", operand
return operands[:split_at].strip(), operands[split_at + 1 :].strip()
def _parse_immediate(operand: str) -> int | None:
text = operand.strip()
if text.startswith("#"):
text = text[1:].strip()
try:
if text.upper().startswith("H'"):
return int(text[2:], 16) & 0xFFFF
if text.upper().startswith("0X"):
return int(text, 16) & 0xFFFF
if text.upper().startswith("$"):
return int(text[1:], 16) & 0xFFFF
return int(text, 10) & 0xFFFF
except ValueError:
return None
def _operand_mentions_any_reference(operand: str, references: list[int]) -> bool:
return any(_operand_mentions_address(operand, address) for address in references)
def _operand_mentions_address(operand: str, address: int) -> bool:
operand_upper = operand.upper().replace(" ", "")
negative = (0x10000 - address) & 0xFFFF
return (
f"H'{address:04X}" in operand_upper
or f"0X{address:04X}" in operand_upper
or f"${address:04X}" in operand_upper
or f"-H'{negative:04X}" in operand_upper
or f"-0X{negative:04X}" in operand_upper
or f"-${negative:04X}" in operand_upper
)
def _mnemonic_root(mnemonic: str) -> str:
return mnemonic.rsplit(".", 1)[0].upper()
__all__ = [
"analyze_table_xrefs",
"generate_table_xref_report",
"load_table_xref_input",
"main",
"write_table_xrefs",
]

5
h8536_consistency.py Normal file
View File

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

8
h8536_emulator.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 emulation harness."""
from h8536.emulator import main
if __name__ == "__main__":
raise SystemExit(main())

View File

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

8
h8536_emulator_probe.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 emulator progress probe."""
from h8536.emulator.probe import main
if __name__ == "__main__":
raise SystemExit(main())

View File

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

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 protocol capture-log analyzer CLI."""
from h8536.protocol_capture import main
if __name__ == "__main__":
raise SystemExit(main())

8
h8536_protocol_trace.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 protocol trace decoder CLI."""
from h8536.protocol_trace import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 report-source tracer CLI."""
from h8536.report_source_trace import main
if __name__ == "__main__":
raise SystemExit(main())

8
h8536_serial_gate.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 serial gate analyzer CLI."""
from h8536.serial_gate import main
if __name__ == "__main__":
raise SystemExit(main())

5
h8536_table_xrefs.py Normal file
View File

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

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyserial==3.5

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""Bench runner for the CONNECT LCD candidate frame sequence."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from h8536.bench_connect_lcd import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,54 @@
import unittest
from h8536.bench_connect_lcd import (
CONNECT_LCD_SEQUENCE,
FrameDetector,
format_frame,
frame_checksum,
frame_checksum_ok,
label_frame,
parse_frame,
)
class BenchConnectLcdTest(unittest.TestCase):
def test_connect_sequence_matches_emulator_preset(self):
self.assertEqual(
[format_frame(frame) for frame in CONNECT_LCD_SEQUENCE],
[
"04 00 00 40 00 1E",
"04 00 00 80 00 DE",
"04 00 00 C0 00 9E",
],
)
def test_parse_frame_appends_xor_checksum(self):
frame = parse_frame("04 00 00 80 00")
self.assertEqual(frame, bytes.fromhex("0400008000DE"))
self.assertEqual(frame_checksum(frame), 0xDE)
self.assertTrue(frame_checksum_ok(frame))
def test_detector_recombines_split_rx_chunks(self):
detector = FrameDetector()
self.assertEqual(detector.feed(bytes.fromhex("000000")), [])
detected = detector.feed(bytes.fromhex("0080DA02000200005A"))
self.assertEqual(
[(format_frame(frame), label) for frame, label in detected],
[
("00 00 00 00 80 DA", "heartbeat"),
("02 00 02 00 00 5A", "connect_ok_path_response_candidate"),
],
)
def test_label_frame_marks_unlabeled_checksum_ok_frame(self):
self.assertEqual(label_frame(bytes.fromhex("01000000005B")), "checksum_ok_unlabeled")
def test_label_frame_marks_real_bench_c0_6020_response(self):
self.assertEqual(label_frame(bytes.fromhex("0780C060205D")), "visible_C0_6020_family_candidate")
if __name__ == "__main__":
unittest.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()

181
tests/test_emulator.py Normal file
View File

@@ -0,0 +1,181 @@
import unittest
from pathlib import Path
from h8536.emulator import (
HEARTBEAT_FRAME,
IPRA,
IPRE,
ON_CHIP_RAM_START,
REGISTER_FIELD_START,
SCI1_RDR,
SCI1_SCR,
SCI1_SSR,
SCI1_TDR,
SCI_SCR_RE,
SCI_SCR_RIE,
SCI_SCR_TE,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
VECTOR_SCI1_RXI,
H8536Emulator,
MemoryMap,
SCI1,
discover_rom_path,
load_rom,
)
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorHarnessTest(unittest.TestCase):
def test_memory_map_routes_rom_ram_register_and_external(self):
memory = MemoryMap(bytes([0x12, 0x34, 0x56, 0x78]))
self.assertEqual(memory.read8(0x0001), 0x34)
memory.write8(ON_CHIP_RAM_START, 0xA5)
self.assertEqual(memory.read8(ON_CHIP_RAM_START), 0xA5)
memory.write8(REGISTER_FIELD_START, 0x5A)
self.assertEqual(memory.read8(REGISTER_FIELD_START), 0x5A)
memory.write8(0xF000, 0x11)
self.assertEqual(memory.read8(0xF000), 0x11)
def test_sci_transmit_capture_requires_enabled_transmitter(self):
sci = SCI1()
sci.write(SCI1_TDR, 0x33)
self.assertEqual(sci.tx_bytes, [])
self.assertFalse(sci.tx_events[-1].emitted)
sci.write(SCI1_SCR, sci.scr | SCI_SCR_TE)
for byte in HEARTBEAT_FRAME:
sci.write(SCI1_TDR, byte)
self.assertEqual(bytes(sci.tx_bytes), HEARTBEAT_FRAME)
self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME])
self.assertTrue(sci.saw_heartbeat())
self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
def test_vector_decoding_uses_minimum_mode_reset_word(self):
rom = bytearray(0x1004)
rom[0:2] = b"\x10\x00"
rom[0x1000] = 0x00
emulator = H8536Emulator(bytes(rom))
self.assertEqual(emulator.reset_vector(), 0x1000)
self.assertEqual(emulator.cpu.pc, 0x1000)
self.assertEqual(emulator.vectors[0x0000][1], 0x1000)
def test_harness_instantiates_on_repo_artifacts(self):
root = Path(__file__).resolve().parents[1]
rom_path = discover_rom_path(root)
self.assertIsNotNone(rom_path)
rom_bytes, loaded_path = load_rom(root=root)
emulator = H8536Emulator(rom_bytes)
report = emulator.run(max_steps=4)
self.assertEqual(loaded_path, rom_path)
self.assertEqual(emulator.reset_vector(), 0x1000)
self.assertGreaterEqual(len(rom_bytes), 0x1002)
self.assertEqual(report.steps, 4)
self.assertFalse(report.heartbeat_seen)
def test_scb_false_decrements_and_branches_until_zero(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x58\x00\x03" # MOV:I.W #H'0003, R0
rom[0x1003:0x1006] = b"\x01\xB8\xFD" # SCB/F R0, H'1003
emulator = H8536Emulator(bytes(rom))
report = emulator.run(max_steps=4)
self.assertEqual(report.stopped_reason, "max_steps")
self.assertEqual(emulator.cpu.regs[0], 0)
self.assertEqual(emulator.cpu.pc, 0x1006)
def test_bsr_rts_and_stack_restore_return_to_caller(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1005] = b"\x0E\x03" # BSR H'1008
rom[0x1005:0x1008] = b"\x59\x12\x34" # MOV:I.W #H'1234, R1
rom[0x1008:0x100B] = b"\x58\xAB\xCD" # MOV:I.W #H'ABCD, R0
rom[0x100B] = 0x19 # RTS
emulator = H8536Emulator(bytes(rom))
emulator.run(max_steps=5)
self.assertEqual(emulator.cpu.regs[0], 0xABCD)
self.assertEqual(emulator.cpu.regs[1], 0x1234)
self.assertEqual(emulator.cpu.regs[7], 0xFE80)
def test_jmp_register_uses_register_target(self):
rom = rom_with_reset(size=0x1020)
rom[0x1000:0x1003] = b"\x59\x10\x10" # MOV:I.W #H'1010, R1
rom[0x1003:0x1005] = b"\x11\xD1" # JMP @R1
rom[0x1010:0x1013] = b"\x58\x12\x34" # MOV:I.W #H'1234, R0
emulator = H8536Emulator(bytes(rom))
emulator.run(max_steps=3)
self.assertEqual(emulator.cpu.regs[0], 0x1234)
self.assertEqual(emulator.cpu.pc, 0x1013)
def test_sci1_txi_interrupt_can_emit_through_tdr(self):
rom = rom_with_reset(size=0x1040)
rom[0x0084:0x0086] = (0x1010).to_bytes(2, "big")
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1008] = bytes([0x15, (IPRE >> 8) & 0xFF, IPRE & 0xFF, 0x06, 0x50])
rom[0x1008:0x100D] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, 0xA0])
rom[0x100D] = 0x00 # NOP after interrupt return
rom[0x1010:0x1015] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, SCI_SCR_TE])
rom[0x1015:0x101A] = bytes([0x15, (SCI1_TDR >> 8) & 0xFF, SCI1_TDR & 0xFF, 0x06, 0x42])
rom[0x101A] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom))
report = emulator.run(max_steps=8)
self.assertEqual(bytes(emulator.sci1.tx_bytes), b"\x42")
self.assertFalse(report.heartbeat_seen)
def test_sci1_rxi_interrupt_consumes_injected_rdr_byte(self):
rom = rom_with_reset(size=0x1040)
rom[VECTOR_SCI1_RXI : VECTOR_SCI1_RXI + 2] = (0x1010).to_bytes(2, "big")
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1008] = bytes([0x15, (IPRE >> 8) & 0xFF, IPRE & 0xFF, 0x06, 0x70])
rom[0x1008:0x100D] = bytes(
[0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, SCI_SCR_RIE | SCI_SCR_RE],
)
rom[0x100D] = 0x00
rom[0x1010:0x1014] = bytes([0x15, (SCI1_SSR >> 8) & 0xFF, SCI1_SSR & 0xFF, 0xD6])
rom[0x1014:0x1018] = bytes([0x15, (SCI1_RDR >> 8) & 0xFF, SCI1_RDR & 0xFF, 0x80])
rom[0x1018:0x101C] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x90])
rom[0x101C] = 0x0A
emulator = H8536Emulator(bytes(rom))
emulator.run(max_steps=3)
emulator.inject_sci1_rx_byte(0xA5)
emulator.run(max_steps=5)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 0xA5)
self.assertFalse(emulator.sci1.read(SCI1_SSR) & SCI_SSR_RDRF)
def test_interval_interrupt_vector_can_fire(self):
rom = rom_with_reset(size=0x1040)
rom[0x0042:0x0044] = (0x1020).to_bytes(2, "big")
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1008] = bytes([0x15, (IPRA >> 8) & 0xFF, IPRA & 0xFF, 0x06, 0x70])
rom[0x1008:0x100A] = b"\x20\xFC" # BRA H'1006-ish idle loop
rom[0x1020:0x1025] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x06, 0x99])
rom[0x1025] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), interval_steps=2)
emulator.run(max_steps=8)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 0x99)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,74 @@
import unittest
from h8536.emulator import H8536Emulator
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorAddressingTest(unittest.TestCase):
def test_txi_indexed_byte_load_uses_signed_word_displacement_and_full_index_register(self):
rom = rom_with_reset()
rom[0x1000:0x1004] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
emulator = H8536Emulator(bytes(rom))
emulator.cpu.regs[0] = 5
emulator.memory.write8(0xF85D, 0xA6)
emulator.step()
self.assertEqual(emulator.cpu.regs[0], 0x00A6)
self.assertEqual(emulator.cpu.pc, 0x1004)
def test_txi_indexed_byte_load_preserves_destination_high_byte(self):
rom = rom_with_reset()
rom[0x1000:0x1004] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
emulator = H8536Emulator(bytes(rom))
emulator.cpu.regs[0] = 0x0105
emulator.memory.write8(0xF95D, 0x4B)
emulator.step()
self.assertEqual(emulator.cpu.regs[0], 0x014B)
def test_extu_byte_clears_high_byte_before_txi_indexed_load(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\xA0\x12" # EXTU.B R0
rom[0x1002:0x1006] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
emulator = H8536Emulator(bytes(rom))
emulator.cpu.regs[0] = 0x0105
emulator.memory.write8(0xF85D, 0xC7)
emulator.memory.write8(0xF95D, 0x99)
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[0], 0x00C7)
self.assertEqual(emulator.cpu.pc, 0x1006)
def test_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
emulator = H8536Emulator(bytes(rom))
emulator.cpu.regs[1] = 0xFE90
emulator.memory.write8(0xFE8E, 0x37)
emulator.step()
self.assertEqual(emulator.cpu.regs[1], 0xFE37)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,62 @@
import unittest
from h8536.emulator.bench_replay import (
BENCH_VISIBLE_C0_6020,
CONNECT_OK_RESPONSE,
assess_bench_parity,
parse_bench_replay_log_text,
)
SAMPLE_LOG = """\
CONNECT LCD bench sequence
21:44:28.062 TX 006 bytes 04 00 00 40 00 1E
21:44:28.215 TX 006 bytes 04 00 00 80 00 DE
21:44:28.218 RX 001 bytes 07
21:44:28.218 RX 002 bytes 80 C0
21:44:28.233 RX 001 bytes 60
21:44:28.234 RX 002 bytes 20 5D
21:44:29.149 RX 006 bytes 00 00 00 00 80 DA
21:44:36.078 SCREEN LCD after CONNECT sequence: CONNECT NOT ACT
"""
class EmulatorBenchReplayTest(unittest.TestCase):
def test_parse_bench_log_extracts_tx_and_recombined_rx_frames(self):
log = parse_bench_replay_log_text(SAMPLE_LOG)
self.assertEqual([frame.frame.hex().upper() for frame in log.tx_frames], ["04000040001E", "0400008000DE"])
self.assertEqual(log.rx_frames[0].frame, BENCH_VISIBLE_C0_6020)
self.assertEqual(log.rx_frames[0].label, "visible_C0_6020_family_candidate")
self.assertEqual(log.screen_notes[-1].note, "CONNECT NOT ACT")
def test_parity_flags_emulator_connect_ok_when_bench_stayed_not_active(self):
log = parse_bench_replay_log_text(SAMPLE_LOG)
parity = assess_bench_parity(
log,
emulator_tx_frames=[CONNECT_OK_RESPONSE],
emulator_lcd_display=" CONNECT: OK | | | ",
emulator_lcd_line_buffer=" CONNECT: OK ",
)
self.assertFalse(parity["matched"])
self.assertIn("lcd_connect_state", parity["mismatch_reasons"])
self.assertIn("missing_visible_C0_6020_response", parity["mismatch_reasons"])
self.assertIn("emulator_emitted_connect_ok_response", parity["mismatch_reasons"])
def test_parity_passes_for_matching_not_active_visible_response(self):
log = parse_bench_replay_log_text(SAMPLE_LOG)
parity = assess_bench_parity(
log,
emulator_tx_frames=[BENCH_VISIBLE_C0_6020],
emulator_lcd_display=" CONNECT:NOT ACT | | | ",
emulator_lcd_line_buffer=" CONNECT:NOT ACT",
)
self.assertTrue(parity["matched"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,110 @@
import unittest
from h8536.emulator.fast_paths import (
LOC_C08B_P9_WRITE_BYTE,
LOC_C0DB_P9_READ_BYTE,
P9FastPath,
P9FastPathConfig,
)
from h8536.emulator.runner import H8536Emulator
def rom_with_reset(*, reset: int = 0x1000, size: int = 0xD000) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class P9FastPathTest(unittest.TestCase):
def test_disabled_fast_path_does_not_handle_known_pc(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE
fast_path = P9FastPath()
self.assertFalse(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.pc, LOC_C08B_P9_WRITE_BYTE)
def test_c08b_write_byte_logs_r0_sets_success_and_returns_to_caller(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE
emulator.cpu.regs[0] = 0x12A5
emulator.cpu.regs[7] = 0xFE7E
emulator.cpu.c = True
emulator.cpu.v = True
emulator.memory.write16(0xFE7E, 0x3456)
fast_path = P9FastPath(P9FastPathConfig(enabled=True))
self.assertTrue(fast_path.try_handle(emulator))
self.assertEqual(fast_path.output_bytes, [0xA5])
self.assertEqual(fast_path.events[-1].kind, "write_byte")
self.assertEqual(fast_path.events[-1].value, 0xA5)
self.assertEqual(emulator.cpu.regs[0], 1)
self.assertEqual(emulator.cpu.pc, 0x3456)
self.assertEqual(emulator.cpu.regs[7], 0xFE80)
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
self.assertEqual(emulator.cpu.steps, 1)
def test_c0db_read_byte_puts_queued_byte_in_r5_low_and_returns(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
emulator.cpu.regs[5] = 0xBE00
emulator.cpu.regs[7] = 0xFE7C
emulator.memory.write16(0xFE7C, 0x4567)
fast_path = P9FastPath(P9FastPathConfig(enabled=True), input_bytes=[0x3C])
self.assertTrue(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.regs[5], 0xBE3C)
self.assertEqual(emulator.cpu.pc, 0x4567)
self.assertEqual(emulator.cpu.regs[7], 0xFE7E)
self.assertEqual(fast_path.events[-1].kind, "read_byte")
self.assertEqual(fast_path.events[-1].value, 0x3C)
self.assertEqual(fast_path.events[-1].source, "initial")
self.assertEqual(fast_path.events[-1].queue_depth, 0)
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
def test_c0db_read_byte_uses_default_when_queue_is_empty(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
emulator.cpu.regs[7] = 0xFE80
emulator.memory.write16(0xFE80, 0x5678)
fast_path = P9FastPath(P9FastPathConfig(enabled=True, default_input_byte=0x81))
self.assertTrue(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.regs[5], 0x0081)
self.assertEqual(emulator.cpu.pc, 0x5678)
self.assertEqual(fast_path.events[-1].source, "default_input_byte")
self.assertEqual(fast_path.events[-1].queue_depth, 0)
self.assertIn("source=default_input_byte", fast_path.events[-1].line())
self.assertFalse(emulator.cpu.z)
self.assertTrue(emulator.cpu.n)
def test_named_input_script_records_read_source_and_remaining_depth(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
emulator.cpu.regs[7] = 0xFE82
emulator.memory.write16(0xFE82, 0x6789)
fast_path = P9FastPath(P9FastPathConfig(enabled=True))
fast_path.queue_input_script("idle-panel", [0x00, 0x80])
self.assertTrue(fast_path.try_handle(emulator))
event = fast_path.events[-1]
self.assertEqual(emulator.cpu.regs[5], 0x0000)
self.assertEqual(event.kind, "read_byte")
self.assertEqual(event.value, 0x00)
self.assertEqual(event.source, "script:idle-panel")
self.assertEqual(event.queue_depth, 1)
self.assertEqual(fast_path.trace_lines(), ["read_byte pc=C0DB value=00 source=script:idle-panel queued=1"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,116 @@
import unittest
from h8536.emulator import H8536Emulator
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorMovFlagTest(unittest.TestCase):
def test_mov_e_byte_zero_sets_z_for_beq_and_preserves_c(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\x50\x00" # MOV:E.B #H'00, R0
rom[0x1002:0x1004] = b"\x27\x02" # BEQ H'1006
rom[0x1004:0x1006] = b"\x51\x01" # MOV:E.B #H'01, R1
rom[0x1006:0x1008] = b"\x52\x02" # MOV:E.B #H'02, R2
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[1], 0)
self.assertEqual(emulator.cpu.regs[2], 2)
self.assertEqual(emulator.cpu.pc, 0x1008)
def test_mov_e_byte_one_clears_z_for_beq_and_preserves_c(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\x50\x01" # MOV:E.B #H'01, R0
rom[0x1002:0x1004] = b"\x27\x02" # BEQ H'1006
rom[0x1004:0x1006] = b"\x51\x01" # MOV:E.B #H'01, R1
rom[0x1006:0x1008] = b"\x52\x02" # MOV:E.B #H'02, R2
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[1], 1)
self.assertEqual(emulator.cpu.regs[2], 0)
self.assertEqual(emulator.cpu.pc, 0x1006)
def test_mov_i_word_immediate_sets_word_flags_and_preserves_c(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x58\x80\x00" # MOV:I.W #H'8000, R0
rom[0x1003:0x1006] = b"\x59\x00\x00" # MOV:I.W #H'0000, R1
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertEqual(emulator.cpu.regs[0], 0x8000)
self.assertFalse(emulator.cpu.z)
self.assertTrue(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
emulator.step()
self.assertEqual(emulator.cpu.regs[1], 0)
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
def test_mulxu_byte_immediate_writes_word_result_and_clears_carry(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\x53\x12" # MOV:E.B #H'12, R3
rom[0x1002:0x1005] = b"\x04\x10\xAB" # MULXU.B #H'10, R3
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[3], 0x0120)
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertFalse(emulator.cpu.c)
def test_not_byte_memory_updates_logic_flags_and_preserves_carry(self):
rom = rom_with_reset()
rom[0x1000:0x1005] = b"\x15\xF6\x80\x15\x00" # NOT.B @H'F680, then NOP
emulator = H8536Emulator(bytes(rom))
emulator.memory.write8(0xF680, 0xFF)
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertEqual(emulator.memory.read8(0xF680), 0x00)
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,48 @@
import unittest
from h8536.emulator import MemoryMap
from h8536.emulator.peripherals import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
class EmulatorLcdBusTest(unittest.TestCase):
def test_status_read_reports_ready_even_after_command_write(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(LCD_E_CLOCK_STATUS, 0x80)
self.assertEqual(memory.read8(LCD_E_CLOCK_STATUS), 0x00)
def test_data_read_returns_data_latch_defaulting_to_zero(self):
memory = MemoryMap(b"\x00" * 4)
self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x00)
memory.write8(LCD_E_CLOCK_DATA, 0x41)
self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x41)
def test_command_sets_ddram_cursor_and_data_updates_display_text(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(LCD_E_CLOCK_STATUS, 0x80)
for value in b"CONNECT: NOT ACT":
memory.write8(LCD_E_CLOCK_DATA, value)
self.assertEqual(memory.lcd.line_text(0), "CONNECT: NOT ACT")
def test_rom_line_mapping_matches_16x4_lcd_addresses(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(LCD_E_CLOCK_STATUS, 0xC0)
memory.write8(LCD_E_CLOCK_DATA, ord("1"))
memory.write8(LCD_E_CLOCK_STATUS, 0x90)
memory.write8(LCD_E_CLOCK_DATA, ord("2"))
memory.write8(LCD_E_CLOCK_STATUS, 0xD0)
memory.write8(LCD_E_CLOCK_DATA, ord("3"))
self.assertTrue(memory.lcd.line_text(1).startswith("1"))
self.assertTrue(memory.lcd.line_text(2).startswith("2"))
self.assertTrue(memory.lcd.line_text(3).startswith("3"))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,360 @@
import unittest
from collections import Counter
from h8536.emulator.probe import (
DEFAULT_WATCH_PCS,
ProbeReport,
RAMLifecycleTrace,
ReportGateTrace,
ReportQueueTrace,
TXFrameSnapshot,
TXFrameWriteTrace,
parse_watch_pc,
parse_tx_frame,
run_probe,
)
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorProbeTest(unittest.TestCase):
def test_parse_watch_pc_accepts_h8_hex_forms(self):
self.assertEqual(parse_watch_pc("C08B"), 0xC08B)
self.assertEqual(parse_watch_pc("0xC08B"), 0xC08B)
self.assertEqual(parse_watch_pc("H'C08B"), 0xC08B)
def test_parse_tx_frame_accepts_spaced_and_compact_hex(self):
expected = bytes.fromhex("00 00 00 00 80 DA")
self.assertEqual(parse_tx_frame("00 00 00 00 80 DA"), expected)
self.assertEqual(parse_tx_frame("0000000080DA"), expected)
self.assertEqual(parse_tx_frame("00,00,00,00,80,DA"), expected)
def test_default_watch_pcs_include_bit_bang_transfer_path(self):
self.assertIn(0xC08B, DEFAULT_WATCH_PCS)
self.assertIn(0xC0DB, DEFAULT_WATCH_PCS)
self.assertIn(0xC121, DEFAULT_WATCH_PCS)
self.assertIn(0xBFE0, DEFAULT_WATCH_PCS)
self.assertIn(0xBFFE, DEFAULT_WATCH_PCS)
self.assertIn(0xC059, DEFAULT_WATCH_PCS)
def test_watch_snapshot_includes_bsr_return_address_on_stack(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1005] = b"\x0E\x03" # BSR H'1008, return H'1005
rom[0x1005:0x1008] = b"\x59\x12\x34" # MOV:I.W #H'1234, R1
rom[0x1008] = 0x19 # RTS
report = run_probe(
bytes(rom),
max_steps=4,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(0x1008,),
watch_snapshot_limit=4,
watch_pc_limit=2,
watch_min_interval=0,
)
self.assertEqual(len(report.watch_snapshots), 1)
snapshot = report.watch_snapshots[0]
self.assertEqual(snapshot.pc, 0x1008)
self.assertEqual(snapshot.sp, 0xFE7E)
self.assertIn((0xFE7E, 0x1005), snapshot.stack_words)
self.assertIn((0x1005, 0x1003), snapshot.callers)
self.assertIn("H'1005<-H'1003", snapshot.line())
self.assertTrue(any("recent_watch_snapshots:" == line for line in report.lines()))
def test_report_lines_include_compact_tx_frame_snapshots(self):
staging_and_frame = bytes.fromhex("11 22 33 44 55 66 77 88 00 00 00 00 80 DA")
report = ProbeReport(
steps=99,
pc=0xBA72,
stopped_reason="tx",
hot_pcs=Counter({0xBA72: 1}),
tx_frame_snapshots=[
TXFrameSnapshot(
step=98,
pc=0xBA72,
label="first_tdr",
bytes_f850_f85d=staging_and_frame,
computed_checksum=0xDA,
stored_checksum=0xDA,
checksum_ok=True,
)
],
)
lines = report.lines()
self.assertIn("recent_tx_frame_snapshots:", lines)
self.assertIn(
" step=98 pc=H'BA72 first_tdr "
"F850-F85D=11 22 33 44 55 66 77 88 00 00 00 00 80 DA "
"TX=00 00 00 00 80 DA computed=DA stored=DA checksum_ok=1",
lines,
)
def test_report_lines_include_target_frame_diff_and_write_sources(self):
report = ProbeReport(
steps=10,
pc=0x1004,
stopped_reason="max_steps",
hot_pcs=Counter({0x1000: 1}),
final_tx_frame=bytes.fromhex("00 01 7F 00 00 24"),
target_frame=bytes.fromhex("00 00 00 00 80 DA"),
tx_frame_write_traces=[
TXFrameWriteTrace(
step=2,
pc=0x1003,
address=0xF859,
old_value=0x00,
new_value=0x01,
frame_after=bytes.fromhex("00 01 00 00 00 00"),
instruction="H'1003: 1D F8 58 90 MOV:G.W R0, @H'F858",
regs=(0x017F, 0, 0, 0, 0, 0, 0, 0),
target_value=0x00,
)
],
tx_target_divergences=[
TXFrameWriteTrace(
step=2,
pc=0x1003,
address=0xF859,
old_value=0x00,
new_value=0x01,
frame_after=bytes.fromhex("00 01 00 00 00 00"),
instruction="H'1003: 1D F8 58 90 MOV:G.W R0, @H'F858",
regs=(0x017F, 0, 0, 0, 0, 0, 0, 0),
target_value=0x00,
)
],
report_queue_traces=[
ReportQueueTrace(
step=3,
pc=0x1007,
kind="queue_write",
head=0,
tail=0,
queue_index=0,
address=0xF870,
old_word=0x0000,
new_word=0x00FF,
instruction="H'1003: 1D F8 70 93 MOV:G.W R3, @H'F870",
regs=(0, 0, 0, 0x00FF, 0, 0, 0, 0),
)
],
report_gate_traces=[
ReportGateTrace(
step=4,
pc=0x4050,
label="faa5_clear_enqueue_branch",
f9c4=0,
faa5=0,
f9c3=0,
head=0,
tail=0,
regs=(0, 0, 0, 0, 0, 0, 0, 0),
z=True,
c=False,
n=False,
decision="enqueue_candidate_faa5_clear",
f9c4_last_write_step=3,
f9c4_last_write_pc=0x40E0,
f9c4_last_write_value=0x00,
f9c4_last_write_age=1,
f9c4_last_nonzero_step=2,
f9c4_last_nonzero_pc=0x40D8,
f9c4_last_nonzero_value=0x14,
f9c4_last_nonzero_age=2,
)
],
ram_lifecycle_traces=[
RAMLifecycleTrace(
step=3,
pc=0x40E0,
address=0xF9C4,
name="F9C4_report_gate_timer",
old_value=0x14,
new_value=0x00,
instruction="H'40E0: 15 F9 C4 13 CLR.B @H'F9C4",
regs=(0, 0, 0, 0, 0, 0, 0, 0),
)
],
)
lines = report.lines()
self.assertIn(
"target_frame=target=00 00 00 00 80 DA current=00 01 7F 00 00 24 "
"diffs=1:01!=00 2:7F!=00 4:00!=80 5:24!=DA pre_checksum_diffs=1,2,4",
lines,
)
self.assertIn("recent_tx_frame_writes:", lines)
self.assertIn("first_target_divergences:", lines)
self.assertIn("recent_report_queue:", lines)
self.assertIn("recent_report_gates:", lines)
self.assertIn("recent_ram_lifecycle:", lines)
self.assertTrue(any("TX[1]" in line and "target=00 DIFF" in line for line in lines))
self.assertTrue(any("queue_write" in line and "word=H'0000->H'00FF" in line for line in lines))
self.assertTrue(any("decision=enqueue_candidate_faa5_clear" in line for line in lines))
self.assertTrue(any("F9C4_last=step=3@H'40E0 value=00 age=1" in line for line in lines))
self.assertTrue(any("F9C4_last_nonzero=step=2@H'40D8 value=14 age=2" in line for line in lines))
self.assertTrue(any("F9C4_report_gate_timer H'F9C4 14->00" in line for line in lines))
def test_run_probe_captures_tx_frame_watch_pc_and_bad_checksum(self):
rom = rom_with_reset(reset=0xBA26, size=0xBB00)
rom[0xBA26] = 0xFF
report = run_probe(
bytes(rom),
max_steps=1,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(),
tx_frame_snapshot_limit=4,
)
self.assertEqual(len(report.tx_frame_snapshots), 1)
snapshot = report.tx_frame_snapshots[0]
self.assertEqual(snapshot.pc, 0xBA26)
self.assertEqual(snapshot.label, "builder_entry")
self.assertEqual(snapshot.frame_bytes, b"\x00\x00\x00\x00\x00\x00")
self.assertEqual(snapshot.computed_checksum, 0x5A)
self.assertEqual(snapshot.stored_checksum, 0x00)
self.assertFalse(snapshot.checksum_ok)
def test_run_probe_traces_tx_frame_writes_against_target(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x58\x01\x7F" # MOV:I.W #H'017F, R0
rom[0x1003:0x1007] = b"\x1D\xF8\x58\x90" # MOV:G.W R0, @H'F858
report = run_probe(
bytes(rom),
max_steps=2,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(),
trace_frame_sources=True,
target_frame=bytes.fromhex("00 00 00 00 80 DA"),
frame_write_trace_limit=4,
)
self.assertEqual(report.final_tx_frame[:3], b"\x01\x7F\x00")
self.assertEqual(len(report.tx_frame_write_traces), 2)
self.assertEqual(report.tx_frame_write_traces[0].address, 0xF858)
self.assertEqual(report.tx_frame_write_traces[0].target_value, 0x00)
self.assertIn("MOV:G.W R0, @H'F858", report.tx_frame_write_traces[0].instruction)
self.assertEqual(report.tx_target_divergences, [])
self.assertTrue(any("target_frame=" in line for line in report.lines()))
def test_run_probe_traces_report_queue_writes_and_cursors(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x5B\x00\xFF" # MOV:I.W #H'00FF, R3
rom[0x1003:0x1007] = b"\x1D\xF8\x70\x93" # MOV:G.W R3, @H'F870
rom[0x1007:0x100B] = b"\x15\xF9\xB0\x08" # ADD:Q.B #1, @H'F9B0
report = run_probe(
bytes(rom),
max_steps=3,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(),
trace_report_queue=True,
report_queue_trace_limit=8,
watch_report_ids=(0x00FF,),
)
queue_writes = [trace for trace in report.report_queue_traces if trace.kind == "queue_write"]
cursor_writes = [trace for trace in report.report_queue_traces if trace.kind == "cursor_head_write"]
self.assertEqual(len(queue_writes), 1)
self.assertEqual(queue_writes[0].queue_index, 0)
self.assertEqual(queue_writes[0].old_word, 0x0000)
self.assertEqual(queue_writes[0].new_word, 0x00FF)
self.assertEqual(report.report_queue_first_writes, queue_writes)
self.assertEqual(report.report_queue_first_nonzero_writes, queue_writes)
self.assertEqual(report.report_queue_watch_hits, queue_writes)
self.assertEqual(len(cursor_writes), 1)
self.assertEqual(cursor_writes[0].old_value, 0x00)
self.assertEqual(cursor_writes[0].new_value, 0x01)
self.assertIn("recent_report_queue:", report.lines())
def test_run_probe_traces_ram_lifecycle_writes_and_last_nonzero_value(self):
rom = rom_with_reset()
rom[0x1000:0x1005] = b"\x15\xF9\xC4\x06\x14" # MOV:G.B #H'14, @H'F9C4
rom[0x1005:0x1009] = b"\x15\xF9\xC4\x13" # CLR.B @H'F9C4
report = run_probe(
bytes(rom),
max_steps=2,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(),
trace_ram_lifecycle=True,
ram_lifecycle_trace_limit=4,
)
self.assertEqual(len(report.ram_lifecycle_traces), 2)
set_trace, clear_trace = report.ram_lifecycle_traces
self.assertEqual(set_trace.address, 0xF9C4)
self.assertEqual(set_trace.old_value, 0x00)
self.assertEqual(set_trace.new_value, 0x14)
self.assertEqual(clear_trace.old_value, 0x14)
self.assertEqual(clear_trace.new_value, 0x00)
self.assertEqual(report.ram_lifecycle_last_writes, [clear_trace])
self.assertEqual(report.ram_lifecycle_last_nonzero_writes, [set_trace])
lines = report.lines()
self.assertIn("recent_ram_lifecycle:", lines)
self.assertIn("ram_lifecycle_last_writes:", lines)
self.assertIn("ram_lifecycle_last_nonzero_writes:", lines)
def test_run_probe_traces_loc_4046_report_gates(self):
rom = rom_with_reset(reset=0x4046, size=0x4080)
rom[0x4046:0x404A] = b"\x15\xF9\xC4\x16" # TST.B @H'F9C4
rom[0x404A:0x404C] = b"\x26\x0C" # BNE H'4058
rom[0x404C:0x4050] = b"\x15\xFA\xA5\xF7" # BTST.B #7, @H'FAA5
rom[0x4050:0x4052] = b"\x27\x07" # BEQ H'4059
rom[0x4052:0x4056] = b"\x15\xF9\xC3\x16" # TST.B @H'F9C3
rom[0x4056:0x4058] = b"\x27\x01" # BEQ H'4059
rom[0x4058] = 0x19 # RTS
rom[0x4059:0x405D] = b"\x15\xF9\xB0\x82" # MOV:G.B @H'F9B0, R2
rom[0x405D:0x405F] = b"\xA2\x12" # EXTU.B R2
rom[0x405F:0x4063] = b"\x15\xF9\xB5\x72" # CMP:G.B @H'F9B5, R2
rom[0x4063:0x4065] = b"\x26\x0F" # BNE H'4074
rom[0x4065:0x4067] = b"\xA2\x1A" # SHLL.B R2
rom[0x4067:0x406C] = b"\xFA\xF8\x70\x06\x00" # MOV:G.W #H'00, @(-H'0790,R2)
rom[0x406C:0x4070] = b"\x15\xF9\xB0\x08" # ADD:Q.B #1, @H'F9B0
rom[0x4070:0x4074] = b"\x15\xF9\xB0\xD7" # BCLR.B #7, @H'F9B0
report = run_probe(
bytes(rom),
max_steps=10,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(),
trace_report_gates=True,
report_gate_trace_limit=16,
)
decisions = {trace.pc: trace.decision for trace in report.report_gate_traces}
self.assertEqual(decisions[0x4046], "f9c4_zero_continue")
self.assertEqual(decisions[0x4050], "enqueue_candidate_faa5_clear")
self.assertEqual(decisions[0x4063], "enqueue_zero_report")
self.assertEqual(decisions[0x4067], "write_report_0000_to_queue_slot")
self.assertTrue(any("recent_report_gates:" == line for line in report.lines()))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,104 @@
import unittest
from collections import Counter
from h8536.emulator.constants import IPRE, SCI1_SCR, SCI1_TDR, SCI_SCR_TE
from h8536.emulator.probe import ProbeReport, SCI1Snapshot, SCI1TXISummary, run_probe
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1040) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorProbeSciTest(unittest.TestCase):
def test_report_lines_include_compact_sci_state_and_txi_summary(self):
report = ProbeReport(
steps=12,
pc=0xBA68,
stopped_reason="max_steps",
hot_pcs=Counter({0xBA68: 7}),
tx_bytes=b"\x00\x03",
sci_accesses=[
"old write SCR=00",
"H'BA60 read SSR=00",
"H'BA64 write SCR=A0",
"H'BA68 write TDR=03",
],
sci1=SCI1Snapshot(
smr=0x00,
brr=0x07,
scr=0xA0,
ssr=0x80,
tdr=0x03,
rdr=0x00,
tx_ready_delay=1,
),
sci1_txi=SCI1TXISummary(
tie=True,
te=True,
tdre=True,
vector_target=0xBA68,
priority=6,
interrupt_mask=2,
interrupt_depth=0,
),
)
lines = report.lines()
self.assertIn("sci1=SMR=00 BRR=07 SCR=A0 SSR=80 TDR=03 RDR=00 tx_ready_delay=1", lines)
self.assertIn(
"sci1_txi=TIE=1 TE=1 TDRE=1 vector=H'BA68 priority=6 mask=2 depth=0 pending=1 serviceable=1",
lines,
)
self.assertIn("recent_sci:", lines)
self.assertIn(" H'BA60 read SSR=00", lines)
self.assertIn(" H'BA68 write TDR=03", lines)
def test_report_lines_bound_recent_sci_accesses(self):
report = ProbeReport(
steps=1,
pc=0x1000,
stopped_reason="max_steps",
sci_accesses=[f"H'{idx:04X} read SSR={idx:02X}" for idx in range(20)],
)
lines = report.lines()
self.assertNotIn(" H'0000 read SSR=00", lines)
self.assertIn(" H'0004 read SSR=04", lines)
self.assertIn(" H'0013 read SSR=13", lines)
def test_run_probe_tracks_sci_register_accesses_and_final_txi_state(self):
rom = rom_with_reset()
rom[0x0084:0x0086] = (0x1010).to_bytes(2, "big")
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1008] = bytes([0x15, (IPRE >> 8) & 0xFF, IPRE & 0xFF, 0x06, 0x50])
rom[0x1008:0x100D] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, 0xA0])
rom[0x100D] = 0x00
rom[0x1010:0x1015] = bytes([0x15, (SCI1_SCR >> 8) & 0xFF, SCI1_SCR & 0xFF, 0x06, SCI_SCR_TE])
rom[0x1015:0x101A] = bytes([0x15, (SCI1_TDR >> 8) & 0xFF, SCI1_TDR & 0xFF, 0x06, 0x42])
rom[0x101A] = 0x0A
report = run_probe(
bytes(rom),
max_steps=8,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
sci_log_limit=8,
watch_pcs=(),
)
self.assertEqual(report.tx_bytes, b"\x42")
self.assertIsNotNone(report.sci1)
self.assertIsNotNone(report.sci1_txi)
self.assertTrue(any("write SCR=A0" in line for line in report.sci_accesses))
self.assertTrue(any("write SCR=20" in line for line in report.sci_accesses))
self.assertTrue(any("write TDR=42" in line for line in report.sci_accesses))
self.assertTrue(any(line.startswith("sci1=SMR=00") for line in report.lines()))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,27 @@
import argparse
import unittest
from h8536.emulator.rx_probe import frame_checksum, frame_checksum_ok, parse_frame
class EmulatorRxProbeTest(unittest.TestCase):
def test_parse_frame_accepts_five_bytes_and_appends_checksum(self):
frame = parse_frame("04 00 00 40 00")
self.assertEqual(frame, bytes.fromhex("04000040001E"))
self.assertTrue(frame_checksum_ok(frame))
def test_parse_frame_accepts_compact_checked_frame(self):
frame = parse_frame("0780684030C5")
self.assertEqual(frame, bytes.fromhex("0780684030C5"))
self.assertEqual(frame_checksum(frame), 0xC5)
self.assertTrue(frame_checksum_ok(frame))
def test_parse_frame_rejects_wrong_length(self):
with self.assertRaises(argparse.ArgumentTypeError):
parse_frame("04 00 00 40")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,71 @@
import unittest
from h8536.emulator import (
HEARTBEAT_FRAME,
SCI1_SCR,
SCI1_SSR,
SCI1_TDR,
SCI_SCR_TE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
SCI1,
)
class SciTimingTest(unittest.TestCase):
def test_ssr_write_one_does_not_set_hardware_flags(self):
sci = SCI1()
sci.ssr = 0x00
sci.write(SCI1_SSR, SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER)
self.assertEqual(sci.read(SCI1_SSR) & 0xF8, 0x00)
def test_ssr_write_zero_clears_selected_writable_flags(self):
sci = SCI1()
sci.ssr = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER | 0x07
sci.write(SCI1_SSR, sci.ssr & ~SCI_SSR_RDRF)
self.assertEqual(sci.read(SCI1_SSR) & SCI_SSR_RDRF, 0x00)
self.assertEqual(
sci.read(SCI1_SSR) & (SCI_SSR_TDRE | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER),
SCI_SSR_TDRE | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER,
)
def test_tdr_write_then_ssr_clear_delays_tdre_until_ticks(self):
sci = SCI1(tx_ready_ticks=2)
sci.write(SCI1_SCR, sci.read(SCI1_SCR) | SCI_SCR_TE)
self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
sci.write(SCI1_TDR, 0x42)
sci.write(SCI1_SSR, sci.read(SCI1_SSR) & ~SCI_SSR_TDRE)
self.assertFalse(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
sci.tick()
self.assertFalse(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
sci.tick()
self.assertTrue(sci.read(SCI1_SSR) & SCI_SSR_TDRE)
def test_transmit_capture_requires_te_enabled(self):
sci = SCI1()
sci.write(SCI1_TDR, 0x33)
self.assertEqual(sci.tx_bytes, [])
self.assertFalse(sci.tx_events[-1].emitted)
sci.write(SCI1_SCR, sci.read(SCI1_SCR) | SCI_SCR_TE)
for byte in HEARTBEAT_FRAME:
sci.write(SCI1_TDR, byte)
self.assertEqual(bytes(sci.tx_bytes), HEARTBEAT_FRAME)
self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME])
self.assertTrue(sci.saw_heartbeat())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,131 @@
import unittest
from h8536.emulator import H8536Emulator, ON_CHIP_RAM_START
from h8536.emulator.constants import (
FRT_TCR_OCIEA,
FRT_TCSR_OCFA,
FRT1_TCR,
FRT1_TCSR,
FRT2_TCR,
FRT2_TCSR,
IPRC,
VECTOR_FRT1_OCIA,
VECTOR_FRT2_OCIA,
)
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1040) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
def write_mov_b_abs_imm(rom: bytearray, address: int, target: int, value: int) -> int:
rom[address : address + 5] = bytes([0x15, (target >> 8) & 0xFF, target & 0xFF, 0x06, value & 0xFF])
return address + 5
class Frt2OciaTimerTest(unittest.TestCase):
def test_frt1_ocia_vector_can_fire_and_decrement_ram(self):
rom = rom_with_reset()
rom[VECTOR_FRT1_OCIA : VECTOR_FRT1_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
# IPRC bits 6..4 are FRT1 and bits 2..0 are FRT2, so H'60 makes
# only the FRT1 priority field high enough to pass interrupt mask 0.
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x60)
pc = write_mov_b_abs_imm(rom, pc, FRT1_TCR, FRT_TCR_OCIEA)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
isr = 0x1020
rom[isr : isr + 4] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C])
isr += 4
rom[isr : isr + 4] = bytes([0x15, (FRT1_TCSR >> 8) & 0xFF, FRT1_TCSR & 0xFF, 0xD5])
rom[isr + 4] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt1_ocia_steps=2)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=5)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 2)
self.assertFalse(emulator.memory.read8(FRT1_TCSR) & FRT_TCSR_OCFA)
self.assertEqual(emulator.cpu.pc, isr + 4)
def test_frt1_ocia_does_not_fire_when_ociea_disabled(self):
rom = rom_with_reset()
rom[VECTOR_FRT1_OCIA : VECTOR_FRT1_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x60)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
rom[0x1020 : 0x1024] = bytes(
[0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C]
)
rom[0x1024] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt1_ocia_steps=1)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=8)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 3)
self.assertEqual(emulator.memory.read8(FRT1_TCSR) & FRT_TCSR_OCFA, 0)
self.assertEqual(emulator.cpu.pc, pc)
def test_frt2_ocia_vector_can_fire_and_decrement_ram(self):
rom = rom_with_reset()
rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
# IPRC bits 6..4 are FRT1 and bits 2..0 are FRT2, so H'06 makes
# only the FRT2 priority field high enough to pass interrupt mask 0.
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x06)
pc = write_mov_b_abs_imm(rom, pc, FRT2_TCR, FRT_TCR_OCIEA)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
isr = 0x1020
rom[isr : isr + 4] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C])
isr += 4
rom[isr : isr + 4] = bytes([0x15, (FRT2_TCSR >> 8) & 0xFF, FRT2_TCSR & 0xFF, 0xD5])
rom[isr + 4] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt2_ocia_steps=2)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=5)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 2)
self.assertFalse(emulator.memory.read8(FRT2_TCSR) & FRT_TCSR_OCFA)
self.assertEqual(emulator.cpu.pc, isr + 4)
def test_frt2_ocia_does_not_fire_when_ociea_disabled(self):
rom = rom_with_reset()
rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x06)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
rom[0x1020 : 0x1024] = bytes(
[0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C]
)
rom[0x1024] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt2_ocia_steps=1)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=8)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 3)
self.assertEqual(emulator.memory.read8(FRT2_TCSR) & FRT_TCSR_OCFA, 0)
self.assertEqual(emulator.cpu.pc, pc)
if __name__ == "__main__":
unittest.main()

42
tests/test_p9_bus.py Normal file
View File

@@ -0,0 +1,42 @@
import unittest
from h8536.emulator import MemoryMap, P9DDR, P9DR
from h8536.emulator.peripherals import P9Bus
class P9BusTest(unittest.TestCase):
def test_bit7_input_uses_queued_then_default_low_response(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(P9DDR, 0x13)
memory.write8(P9DR, 0x80)
memory.p9_bus.queue_input_bits([1])
self.assertEqual(memory.read8(P9DDR), 0x13)
self.assertEqual(memory.read8(P9DR) & 0x80, 0x80)
self.assertEqual(memory.read8(P9DR) & 0x80, 0x00)
self.assertEqual(memory.registers[P9DR - 0xFE80], 0x80)
def test_bit7_output_reads_latch(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(P9DDR, 0x93)
memory.write8(P9DR, 0x80)
self.assertEqual(memory.read8(P9DDR), 0x93)
self.assertEqual(memory.read8(P9DR) & 0x80, 0x80)
self.assertEqual(memory.registers[P9DR - 0xFE80], 0x80)
def test_strobe_rising_edges_capture_output_bits_and_byte_candidates(self):
bus = P9Bus()
bus.write_ddr(0x93)
for bit in (1, 0, 1, 0, 0, 1, 0, 1):
bus.write_dr(0x80 if bit else 0x00)
bus.write_dr((0x80 if bit else 0x00) | 0x02)
bus.write_dr(0x80 if bit else 0x00)
self.assertEqual(bus.transmitted_bits, [1, 0, 1, 0, 0, 1, 0, 1])
self.assertEqual(bus.byte_candidates, [0xA5])
self.assertEqual([event.edge for event in bus.strobe_edges[:2]], ["rising", "falling"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,172 @@
import io
import json
import unittest
from pathlib import Path
from h8536.protocol_capture import analyze_capture_text, format_text_report, main, parse_capture_text
class ProtocolCaptureTest(unittest.TestCase):
def test_parses_timestamped_capture_chunks(self):
chunks = parse_capture_text("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n")
self.assertEqual(len(chunks), 1)
self.assertEqual(chunks[0].timestamp_ms, 57975502)
self.assertEqual(chunks[0].analyzer_direction, "rx")
self.assertEqual(chunks[0].device_direction, "tx")
self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x15, 0x80, 0x00, 0xCF))
def test_parses_idle_frame_lines_without_direction_token(self):
chunks = parse_capture_text("11:54:40.567 frame 006 00 00 00 00 80 DA\n")
self.assertEqual(len(chunks), 1)
self.assertEqual(chunks[0].timestamp_ms, 42880567)
self.assertEqual(chunks[0].analyzer_direction, "rx")
self.assertEqual(chunks[0].device_direction, "tx")
self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x00, 0x00, 0x80, 0xDA))
def test_recombines_user_split_rx_chunks_into_valid_call_frame(self):
analysis = analyze_capture_text(
"16:06:15.502 RX 003 bytes 00 00 15\n"
"16:06:15.506 RX 003 bytes 80 00 CF\n"
)
self.assertEqual(analysis["frame_count"], 1)
frame = analysis["frames"][0]
self.assertEqual(frame["source_chunk_indexes"], [0, 1])
self.assertEqual(frame["analyzer_direction"], "rx")
self.assertEqual(frame["device_direction"], "tx")
self.assertEqual(frame["report_candidate"]["index"], 0x15)
self.assertEqual(frame["report_candidate"]["value"], 0x8000)
self.assertEqual(
frame["report_candidate"]["observed_candidate"]["name_candidate"],
"call_button_candidate",
)
self.assertEqual(frame["report_candidate"]["observed_candidate"]["state_candidate"], "active")
def test_recombines_split_chunks_with_multiple_frames_and_labels_known_reports(self):
analysis = analyze_capture_text(
"16:06:15.500 RX 004 bytes 00 00 00 00\n"
"16:06:15.502 RX 005 bytes 80 DA 00 00 07\n"
"16:06:15.504 RX 003 bytes 80 00 DD\n"
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
)
self.assertEqual(analysis["frame_count"], 3)
names = [
frame["report_candidate"]["observed_candidate"]["name_candidate"]
for frame in analysis["frames"]
]
self.assertEqual(
names,
[
"heartbeat_alive_candidate",
"cam_power_button_candidate",
"heartbeat_alive_candidate",
],
)
self.assertEqual(analysis["repeated_group_count"], 1)
group = analysis["repeated_groups"][0]
self.assertEqual(group["count"], 2)
self.assertEqual(group["cadence_ms"]["samples"], [100])
def test_text_report_mentions_split_label_and_cadence(self):
report = format_text_report(
analyze_capture_text(
"16:06:15.502 RX 003 bytes 00 00 15\n"
"16:06:15.506 RX 003 bytes 80 00 CF\n"
"16:06:15.602 RX 006 bytes 00 00 15 80 00 CF\n"
)
)
self.assertIn("call_button_candidate state=active", report)
self.assertIn("checksum=ok split", report)
self.assertIn("cadence=avg=100.0ms", report)
def test_gate_session_hints_summarize_reports_transitions_and_heartbeat_interruptions(self):
analysis = analyze_capture_text(
"16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n"
"16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n"
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
"16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n"
"16:06:15.700 RX 006 bytes 00 00 00 00 80 DA\n"
)
hints = analysis["gate_session_hints"]
self.assertEqual(
hints["observed_autonomous_report_names"],
["call_button_candidate", "heartbeat_alive_candidate"],
)
reports = {item["name_candidate"]: item for item in hints["observed_reports"]}
self.assertEqual(reports["heartbeat_alive_candidate"]["count"], 3)
self.assertEqual(reports["heartbeat_alive_candidate"]["first_timestamp"], "16:06:15.500")
self.assertEqual(reports["heartbeat_alive_candidate"]["last_timestamp"], "16:06:15.700")
self.assertEqual(reports["call_button_candidate"]["states"], ["active", "inactive"])
self.assertEqual(hints["heartbeat_cadence_ms"]["samples"], [100, 100])
self.assertEqual(hints["heartbeat_cadence_ms"]["average"], 100.0)
self.assertEqual(len(hints["heartbeat_interruptions"]), 2)
self.assertEqual(
hints["heartbeat_interruptions"][0]["interrupted_by"][0]["name_candidate"],
"call_button_candidate",
)
self.assertEqual(len(hints["active_inactive_transitions"]), 1)
transition = hints["active_inactive_transitions"][0]
self.assertEqual(transition["index_hex"], "0x0015")
self.assertEqual(transition["from_state"], "active")
self.assertEqual(transition["to_state"], "inactive")
self.assertEqual(hints["evidence_scope"], "capture_side_observation_only")
self.assertIn("host/session gating", hints["caveat"])
def test_text_report_mentions_gate_session_hints_and_caveat(self):
report = format_text_report(
analyze_capture_text(
"16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n"
"16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n"
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
"16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n"
)
)
self.assertIn(
"observed autonomous report candidates: call_button_candidate, heartbeat_alive_candidate",
report,
)
self.assertIn("heartbeat cadence count=2 cadence=avg=100.0ms", report)
self.assertIn("transition index=0x0015 active->inactive", report)
self.assertIn("heartbeat gap 16:06:15.500..16:06:15.600", report)
self.assertIn("caveat: Missing autonomous reports", report)
def test_cli_json_output(self):
output = io.StringIO()
rc = main(
["--json", "-"],
stdin=io.StringIO("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n"),
stdout=output,
)
self.assertEqual(rc, 0)
payload = json.loads(output.getvalue())
self.assertEqual(payload["frames"][0]["report_candidate"]["index"], 0x15)
def test_idle_reference_capture_when_present(self):
path = Path("ROM/rcp-txd-idle-only.txt")
if not path.exists():
self.skipTest("idle reference capture is not present")
analysis = analyze_capture_text(path.read_text(encoding="utf-8"))
self.assertGreaterEqual(analysis["frame_count"], 10)
self.assertEqual(
analysis["gate_session_hints"]["observed_autonomous_report_names"],
["heartbeat_alive_candidate"],
)
heartbeat = analysis["gate_session_hints"]["heartbeat_cadence_ms"]
self.assertEqual(heartbeat["count"], analysis["frame_count"])
self.assertGreater(heartbeat["average"], 600)
self.assertLess(heartbeat["average"], 800)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,154 @@
import io
import json
import tempfile
import unittest
from pathlib import Path
from h8536.protocol_trace import (
checksum_for,
decode_trace,
format_text_report,
main,
parse_byte_text,
)
def frame(prefix: list[int]) -> list[int]:
return prefix + [checksum_for(prefix)]
class ProtocolTraceTest(unittest.TestCase):
def test_decodes_six_byte_frame_fields_and_checksum(self):
decoded = decode_trace(frame([0x08, 0x85, 0x34, 0x12, 0xAB]), direction="rx")
only = decoded["frames"][0]
self.assertTrue(only["checksum"]["valid"])
self.assertEqual(only["direction"], "rx")
self.assertEqual(only["command"]["value"], 0)
self.assertEqual(only["index"]["byte1_low3"], 5)
self.assertEqual(only["index"]["combined"], 0x0534)
self.assertEqual(only["payload_value"]["word_be"], 0x12AB)
self.assertEqual(only["payload_value"]["word_le"], 0xAB12)
def test_reports_bad_checksum_and_trailing_bytes(self):
decoded = decode_trace([0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x99])
only = decoded["frames"][0]
self.assertFalse(only["checksum"]["valid"])
self.assertEqual(only["checksum"]["expected"], checksum_for([1, 2, 3, 4, 5]))
self.assertEqual(decoded["trailing_bytes"], ["0x99"])
def test_loads_semantic_command_names_from_decompiler_json(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "rom.json"
path.write_text(
json.dumps(
{
"serial_semantics": {
"protocol_semantics": [
{
"command_effects": [
{
"command_value": 1,
"name_candidate": "read_value",
}
],
"response_schema": [],
}
]
}
}
),
encoding="utf-8",
)
decoded = decode_trace(frame([0x09, 0, 1, 2, 3]), semantics_path=path)
self.assertTrue(decoded["semantics"]["loaded"])
self.assertEqual(decoded["frames"][0]["command"]["name_candidate"], "read_value")
def test_marks_rx_cmd_7_as_retransmit_or_error_candidate_with_previous_frame(self):
prior = frame([0x01, 0, 0, 0, 0])
retry = frame([0x07, 0, 0, 0, 0])
decoded = decode_trace(prior + retry, direction="rx")
annotation = decoded["frames"][1]["stateful_annotations"][0]
self.assertEqual(annotation["kind"], "retransmit_or_error_candidate")
self.assertEqual(annotation["previous_valid_same_direction"]["frame_index"], 0)
def test_decodes_observed_tx_frames_as_reports(self):
samples = [
([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA], 0x0000, 0x0080, "heartbeat_alive_candidate", None),
([0x00, 0x00, 0x15, 0x80, 0x00, 0xCF], 0x0015, 0x8000, "call_button_candidate", "active"),
([0x00, 0x00, 0x15, 0x00, 0x00, 0x4F], 0x0015, 0x0000, "call_button_candidate", "inactive"),
([0x00, 0x00, 0x07, 0x80, 0x00, 0xDD], 0x0007, 0x8000, "cam_power_button_candidate", "active"),
]
decoded = decode_trace([byte for sample, *_ in samples for byte in sample], direction="tx")
for actual, (_, index, value, name, state) in zip(decoded["frames"], samples):
self.assertTrue(actual["checksum"]["valid"])
self.assertFalse(actual["command"]["applicable"])
self.assertIsNone(actual["command"]["name_candidate"])
self.assertEqual(actual["response_schema_candidates"], [])
self.assertEqual(actual["stateful_annotations"], [])
self.assertEqual(actual["report"]["index"], index)
self.assertEqual(actual["report"]["value"], value)
self.assertEqual(actual["report"]["observed_candidate"]["name_candidate"], name)
self.assertEqual(actual["report"]["observed_candidate"].get("state_candidate"), state)
def test_tx_text_report_does_not_render_cmd_or_set_value_acked(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "rom.json"
path.write_text(
json.dumps(
{
"serial_semantics": {
"command_effects": [
{
"command_value": 0,
"name_candidate": "set_value_acked",
}
]
}
}
),
encoding="utf-8",
)
decoded = decode_trace(
[0x00, 0x00, 0x15, 0x80, 0x00, 0xCF],
direction="tx",
semantics_path=path,
)
text = format_text_report(decoded)
self.assertIn("report_index=0x0015", text)
self.assertIn("observed_candidate=call_button_candidate", text)
self.assertNotIn("cmd=", text)
self.assertNotIn("set_value_acked", text)
def test_auto_direction_uses_rx_tx_prefixes(self):
events = parse_byte_text(
"rx: 01 00 00 00 00 5B\n"
"tx: 04 00 00 00 00 5E\n"
)
decoded = decode_trace(events, direction="auto")
self.assertEqual(decoded["frames"][0]["direction"], "rx")
self.assertEqual(decoded["frames"][1]["direction"], "tx")
def test_cli_json_output(self):
output = io.StringIO()
rc = main(["--json", "01", "00", "00", "00", "00", "5B"], stdout=output)
self.assertEqual(rc, 0)
payload = json.loads(output.getvalue())
self.assertEqual(payload["frames"][0]["command"]["value"], 1)
if __name__ == "__main__":
unittest.main()

View File

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

View File

@@ -0,0 +1,135 @@
import io
import json
import tempfile
import unittest
from pathlib import Path
from h8536.report_source_trace import analyze_report_sources, format_text_report, main, write_report_sources
def ins(
address: int,
mnemonic: str,
operands: str = "",
*,
text: str | None = None,
targets: list[int] | None = None,
block: int | None = None,
) -> dict[str, object]:
row: dict[str, object] = {
"address": address,
"mnemonic": mnemonic,
"operands": operands,
"text": text or f"{mnemonic} {operands}".strip(),
"kind": "call" if mnemonic in {"BSR", "JSR"} else "normal",
"targets": targets or [],
"references": [],
}
if block is not None:
row["dataflow"] = {"block": block}
return row
def payload() -> dict[str, object]:
return {
"call_graph": {
"nodes": [
{"start": 0x1000, "end": 0x10FF, "label": "loc_1000"},
{"start": 0x2000, "end": 0x20FF, "label": "loc_2000"},
{"start": 0x3000, "end": 0x30FF, "label": "loc_3000"},
],
},
"instructions": [
ins(0x1000, "MOV:E.B", "#H'80, R2", block=0x1000),
ins(0x1002, "MOV:I.W", "#H'0007, R3", block=0x1000),
ins(0x1005, "BSR", "loc_3E54", targets=[0x3E54], block=0x1000),
ins(0x2000, "MOV:I.W", "#H'0012, R5", block=0x2000),
ins(0x2003, "CMP:G.W", "@(-H'2000,R5), R1", block=0x2000),
ins(0x2007, "MOV:E.B", "#H'80, R2", block=0x2000),
ins(0x2009, "MOV:G.W", "R5, R3", block=0x2000),
ins(0x200B, "BSR", "loc_3E54", targets=[0x3E54], block=0x2000),
ins(0x3000, "MOV:E.B", "#H'00, R2", block=0x3000),
ins(0x3002, "MOV:I.W", "#H'0007, R3", block=0x3000),
ins(0x3005, "BSR", "loc_3E54", targets=[0x3E54], block=0x3000),
],
}
class ReportSourceTraceTest(unittest.TestCase):
def test_finds_direct_static_report_index_0007(self):
analysis = analyze_report_sources(payload())
self.assertEqual(analysis["summary"]["direct_call_count"], 3)
self.assertEqual(analysis["summary"]["direct_static_hit_count"], 1)
hit = analysis["calls"][0]
self.assertTrue(hit["can_directly_enqueue_report_index"])
self.assertEqual(hit["r2"]["bit7"], True)
self.assertEqual(hit["r3"]["classification"], "constant")
self.assertEqual(hit["r3"]["value"], 0x0007)
self.assertIn("Direct static enqueue source", hit["assessment"])
def test_classifies_table_context_and_clear_gate(self):
analysis = analyze_report_sources(payload())
dynamic = analysis["calls"][1]
gated_off = analysis["calls"][2]
self.assertEqual(dynamic["r3"]["classification"], "constant")
self.assertEqual(dynamic["r3"]["value"], 0x0012)
self.assertEqual(dynamic["table_hints"][0]["table"], "primary_value_table_candidate")
self.assertFalse(gated_off["can_directly_enqueue_report_index"])
self.assertEqual(gated_off["r2"]["bit7"], False)
self.assertIn("would not enqueue", gated_off["assessment"])
def test_text_report_mentions_conclusion_and_caveats(self):
text = format_text_report(analyze_report_sources(payload()))
self.assertIn("loc_3E54 Report Source Trace", text)
self.assertIn("Direct static 0x0007 hits: 1", text)
self.assertIn("Indirect dispatch", text)
self.assertIn("R3 evidence", text)
def test_cli_json_output_and_out_file(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"
output_path = Path(tmp) / "sources.json"
input_path.write_text(json.dumps(payload()), encoding="utf-8")
stdout = io.StringIO()
rc = main(["--json", "--out", str(output_path), str(input_path)], stdout=stdout)
self.assertEqual(rc, 0)
self.assertIn("wrote", stdout.getvalue())
written = json.loads(output_path.read_text(encoding="utf-8"))
self.assertEqual(written["kind"], "report_source_trace")
self.assertEqual(written["summary"]["direct_static_hit_count"], 1)
def test_write_text_output(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"
output_path = Path(tmp) / "sources.txt"
input_path.write_text(json.dumps(payload()), encoding="utf-8")
analysis = write_report_sources(input_path, output_path)
self.assertEqual(analysis["kind"], "report_source_trace")
self.assertIn("Report Source Trace", output_path.read_text(encoding="utf-8"))
def test_real_rom_smoke_when_present(self):
path = Path("build/rom_decompiled.json")
if not path.exists():
self.skipTest("build/rom_decompiled.json is not present")
payload_real = json.loads(path.read_text(encoding="utf-8"))
analysis = analyze_report_sources(payload_real)
self.assertEqual(analysis["kind"], "report_source_trace")
self.assertGreaterEqual(analysis["summary"]["direct_call_count"], 1)
self.assertIn("0x0007", analysis["summary"]["conclusion"])
for call in analysis["calls"]:
self.assertIn("address_hex", call)
self.assertIn("r2", call)
self.assertIn("r3", call)
if __name__ == "__main__":
unittest.main()

View File

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

205
tests/test_serial_gate.py Normal file
View File

@@ -0,0 +1,205 @@
import io
import json
import tempfile
import unittest
from pathlib import Path
from h8536.serial_gate import analyze_serial_gate, format_text_report, main
def ins(
address: int,
text: str,
mnemonic: str | None = None,
operands: str = "",
references: list[int] | None = None,
targets: list[int] | None = None,
) -> dict[str, object]:
refs = [{"address": ref, "symbol": f"ram_{ref:04X}", "region": "on_chip_ram", "kind": "ram"} for ref in references or []]
return {
"address": address,
"text": text,
"mnemonic": mnemonic or text.split()[0],
"operands": operands or (text.split(" ", 1)[1] if " " in text else ""),
"kind": "call" if text.startswith("BSR") else "normal",
"targets": targets or [],
"references": refs,
}
def fixture_payload() -> dict[str, object]:
rows = [
ins(0x3FD3, "TST.B @H'FAA2", references=[0xFAA2]),
ins(0x3FD7, "BNE loc_3FEE", targets=[0x3FEE]),
ins(0x3FD9, "BTST.B #7, @H'FAA5", references=[0xFAA5]),
ins(0x3FDD, "BEQ loc_3FE5", targets=[0x3FE5]),
ins(0x3FDF, "TST.B @H'F9C3", references=[0xF9C3]),
ins(0x3FE3, "BNE loc_3FEE", targets=[0x3FEE]),
ins(0x3FE5, "TST.B @H'F9C0", references=[0xF9C0]),
ins(0x3FE9, "BNE loc_3FEE", targets=[0x3FEE]),
ins(0x3FEB, "BSR loc_BAF2", targets=[0xBAF2]),
ins(0x3FEF, "TST.B @H'F9C5", references=[0xF9C5]),
ins(0x3FF5, "CLR.B @H'F9B5", references=[0xF9B5]),
ins(0x3FF9, "CLR.B @H'F9B0", references=[0xF9B0]),
ins(0x4046, "TST.B @H'F9C4", references=[0xF9C4]),
ins(0x404A, "BNE loc_4058", targets=[0x4058]),
ins(0x404C, "BTST.B #7, @H'FAA5", references=[0xFAA5]),
ins(0x4050, "BEQ loc_4059", targets=[0x4059]),
ins(0x4052, "TST.B @H'F9C3", references=[0xF9C3]),
ins(0x4056, "BEQ loc_4059", targets=[0x4059]),
ins(0x4058, "RTS"),
ins(0x4059, "MOV:G.B @H'F9B0, R2", references=[0xF9B0]),
ins(0x405F, "CMP:G.B @H'F9B5, R2", references=[0xF9B5]),
ins(0x4063, "BNE loc_4074", targets=[0x4074]),
ins(0x4067, "MOV:G.W #H'00, @(-H'0790,R2)"),
ins(0x406C, "ADD:Q.B #1, @H'F9B0", references=[0xF9B0]),
ins(0x4070, "BCLR.B #7, @H'F9B0", references=[0xF9B0]),
ins(0x40E0, "MOV:G.B #H'14, @H'F9C4", references=[0xF9C4]),
ins(0xBAF2, "MOV:G.B @H'F9B5, R1", references=[0xF9B5]),
ins(0xBAF8, "CMP:G.B @H'F9B0, R1", references=[0xF9B0]),
ins(0xBAFC, "BNE loc_BB00", targets=[0xBB00]),
ins(0xBAFE, "BRA loc_BB56", targets=[0xBB56]),
ins(0xBB00, "BSET.B #3, @H'FAA2", references=[0xFAA2]),
ins(0xBB08, "MOV:G.W @(-H'0790,R0), R0"),
ins(0xBB1C, "MOV:G.B R1, @H'F850", references=[0xF850]),
ins(0xBB20, "MOV:G.B R5, @H'F852", references=[0xF852]),
ins(0xBB2B, "MOV:G.B R5, @H'F851", references=[0xF851]),
ins(0xBB39, "MOV:G.B R4, @H'F854", references=[0xF854]),
ins(0xBB3F, "MOV:G.B R4, @H'F853", references=[0xF853]),
ins(0xBB43, "BSR loc_BA26", targets=[0xBA26]),
ins(0xBB46, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]),
ins(0xBB4C, "MOV:G.B #H'14, @H'F9C8", references=[0xF9C8]),
ins(0xBB51, "MOV:G.B #H'80, @H'FAA3", references=[0xFAA3]),
ins(0xBBCB, "CLR.B @H'F9C3", references=[0xF9C3]),
ins(0xBC0F, "TST.B @H'FAA2", references=[0xFAA2]),
ins(0xBC15, "BSET.B #7, @H'FAA2", references=[0xFAA2]),
ins(0xBD6D, "ADD:Q.B #1, @H'F9B5", references=[0xF9B5]),
ins(0xBD71, "BCLR.B #7, @H'F9B5", references=[0xF9B5]),
ins(0xBD75, "CLR.B @H'FAA3", references=[0xFAA3]),
ins(0xBD79, "CLR.B @H'FAA2", references=[0xFAA2]),
ins(0xBE9E, "MOV:G.B @H'FAA5, R0", references=[0xFAA5]),
ins(0xBEA5, "AND.B @H'FAA3, R0", references=[0xFAA3]),
ins(0xBEA9, "MOV:G.B R0, @H'FAA3", references=[0xFAA3]),
ins(0xBEAF, "CLR.B @H'FAA2", references=[0xFAA2]),
ins(0xBA31, "MOV:G.B #H'07, @H'F9C4", references=[0xF9C4]),
ins(0xBEB5, "TST.W @H'F9C6", references=[0xF9C6]),
ins(0xBEBB, "TST.B @H'F9C8", references=[0xF9C8]),
ins(0xBEC5, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]),
ins(0xBECB, "BTST.B #7, @H'FAA3", references=[0xFAA3]),
ins(0xBED1, "CLR.B @H'F9C3", references=[0xF9C3]),
ins(0xBED5, "BSR loc_BA26", targets=[0xBA26]),
ins(0xBEEA, "BCLR.B #5, @FRT1_TCSR"),
ins(0xBEEE, "TST.B @H'F9C0", references=[0xF9C0]),
ins(0xBEF2, "BEQ loc_BEF8", targets=[0xBEF8]),
ins(0xBEF4, "ADD:Q.B #-1, @H'F9C0", references=[0xF9C0]),
ins(0xBEF8, "TST.B @H'F9C1", references=[0xF9C1]),
ins(0xBEFC, "BEQ loc_BF02", targets=[0xBF02]),
ins(0xBEFE, "ADD:Q.B #-1, @H'F9C1", references=[0xF9C1]),
ins(0xBF02, "TST.W @H'F9C6", references=[0xF9C6]),
ins(0xBF06, "BEQ loc_BF0C", targets=[0xBF0C]),
ins(0xBF08, "ADD:Q.W #-1, @H'F9C6", references=[0xF9C6]),
ins(0xBF23, "BCLR.B #5, @FRT2_TCSR"),
ins(0xBF27, "TST.B @H'F9C4", references=[0xF9C4]),
ins(0xBF2D, "ADD:Q.B #-1, @H'F9C4", references=[0xF9C4]),
]
return {
"vectors": [
{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"},
{"address": 0x006A, "name": "frt2_ocia", "target": 0xBF23, "target_label": "vec_frt2_ocia_BF23"},
],
"call_graph": {"nodes": [{"start": 0x3FD3, "label": "loc_3FD3"}, {"start": 0xBAF2, "label": "loc_BAF2"}]},
"instructions": rows,
}
class SerialGateTest(unittest.TestCase):
def test_reconstructs_requested_gate_evidence(self):
analysis = analyze_serial_gate(fixture_payload())
self.assertEqual(analysis["summary"]["confidence"], "high")
self.assertTrue(analysis["evidence"]["scheduler_gate_loc_3FD3"]["present"])
self.assertIn("FAA2 == 0", analysis["evidence"]["scheduler_gate_loc_3FD3"]["summary"])
self.assertTrue(analysis["evidence"]["queue_send_gate_loc_BAF2"]["present"])
self.assertEqual(analysis["evidence"]["queue_send_gate_loc_BAF2"]["queue_table_candidate"]["base_address_hex"], "H'F870")
self.assertEqual(analysis["evidence"]["queue_send_gate_loc_BAF2"]["send_call_address_hex"], "H'BB43")
self.assertTrue(analysis["evidence"]["resend_gate_path"]["present"])
self.assertEqual(analysis["evidence"]["resend_gate_path"]["resend_call_address_hex"], "H'BED5")
self.assertTrue(analysis["evidence"]["rx_session_maintenance"]["present"])
self.assertTrue(any("0x0007" in caveat and "0x0015" in caveat for caveat in analysis["caveats"]))
def test_json_analysis_includes_frt1_tick_timer_roles(self):
analysis = analyze_serial_gate(fixture_payload())
tick = analysis["evidence"]["timer_tick_evidence"]
self.assertTrue(tick["present"])
self.assertEqual(tick["vector_address_hex"], "H'0062")
self.assertEqual(tick["handler_address_hex"], "H'BEEA")
self.assertIn("FRT1_TCSR.OCFA", tick["summary"])
roles = {role["address"]: role for role in tick["candidate_timer_roles"]}
self.assertIn("post-TX/report delay", roles[0xF9C0]["role"])
self.assertIn("secondary delay", roles[0xF9C1]["role"])
self.assertIn("periodic report/heartbeat", roles[0xF9C6]["role"])
def test_json_analysis_includes_frt2_idle_heartbeat_gate(self):
analysis = analyze_serial_gate(fixture_payload())
gate = analysis["evidence"]["idle_heartbeat_gate_loc_4046"]
self.assertTrue(gate["present"])
self.assertEqual(gate["timer"]["handler_address_hex"], "H'BF23")
self.assertEqual(gate["post_tx_reload_value_hex"], "H'07")
self.assertIn("0.7s", gate["summary"])
roles = {role["address"]: role for role in gate["candidate_timer_roles"]}
self.assertIn("heartbeat", roles[0xF9C4]["role"])
def test_summarizes_key_state_readers_and_writers(self):
analysis = analyze_serial_gate(fixture_payload())
accesses = {entry["address"]: entry for entry in analysis["state_accesses"]}
self.assertGreaterEqual(accesses[0xF9B5]["read_count"], 1)
self.assertGreaterEqual(accesses[0xF9B5]["read_write_count"], 1)
self.assertGreaterEqual(accesses[0xF9C1]["read_write_count"], 1)
self.assertGreaterEqual(accesses[0xF9C4]["read_write_count"], 1)
self.assertGreaterEqual(accesses[0xFAA3]["write_count"], 1)
self.assertIn("sample_accesses", accesses[0xFAA2])
def test_text_report_mentions_caveat_and_gate(self):
text = format_text_report(analyze_serial_gate(fixture_payload()))
self.assertIn("loc_3FD3 gate into loc_BAF2", text)
self.assertIn("capture overlays/runtime queue entries", text)
self.assertIn("H'F9B5", text)
def test_text_report_mentions_frt1_tick_timer_roles(self):
text = format_text_report(analyze_serial_gate(fixture_payload()))
self.assertIn("FRT1 OCIA periodic tick countdowns: present", text)
self.assertIn("H'BEEA: BCLR.B #5, @FRT1_TCSR", text)
self.assertIn("H'F9C0: candidate post-TX/report delay countdown", text)
self.assertIn("H'F9C1: candidate secondary delay countdown", text)
self.assertIn("H'F9C6: candidate periodic report/heartbeat countdown", text)
def test_text_report_mentions_frt2_idle_heartbeat_gate(self):
text = format_text_report(analyze_serial_gate(fixture_payload()))
self.assertIn("loc_4046 idle heartbeat/report gate: present", text)
self.assertIn("FRT2 OCIA", text)
self.assertIn("H'F9C4: candidate idle heartbeat/report gate countdown", text)
self.assertIn("observed period ~= 700ms", text)
def test_cli_json_output_and_out_file(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"
output_path = Path(tmp) / "gate.json"
input_path.write_text(json.dumps(fixture_payload()), encoding="utf-8")
stdout = io.StringIO()
rc = main(["--json", "--out", str(output_path), str(input_path)], stdout=stdout)
self.assertEqual(rc, 0)
self.assertIn("wrote", stdout.getvalue())
payload = json.loads(output_path.read_text(encoding="utf-8"))
self.assertEqual(payload["kind"], "serial_gate")
if __name__ == "__main__":
unittest.main()

View File

@@ -160,12 +160,23 @@ class SerialPseudocodeTest(unittest.TestCase):
self.assertIn("No SCI1 RXI/TXI DTC vector entries are present", text)
self.assertIn("MAX202 pin 11 traces to H8 pin 66", text)
self.assertIn("Manual/0900766b802125d0.md:15823 TDR transmit data register", text)
self.assertIn("#define SCI1_TX_FRAME_LENGTH 6u", text)
self.assertIn("#define SCI1_TX_FRAME_BASE 0xF858u", text)
self.assertIn("#define SCI1_TX_FRAME_BYTE(n) MEM8[(u16)(SCI1_TX_FRAME_BASE + (n))]", text)
self.assertIn("#define SCI1_TX_FRAME_CHECKSUM SCI1_TX_FRAME_BYTE(5u)", text)
self.assertIn("#define SCI1_TX_INDEX MEM8[0xF9C2u]", text)
self.assertIn("#define TX_FRAME(n) MEM8[(u16)(0xF858u + (n))]", text)
self.assertIn("#define RX_CAPTURE(n) MEM8[(u16)(0xF868u + (n))]", text)
self.assertIn("checksum ^= TX_FRAME(4);", text)
self.assertIn("TX_FRAME(5) = sci1_tx_candidate_checksum();", text)
self.assertIn("First byte is sent synchronously; TIE enables TXI for the remaining bytes.", text)
self.assertIn("SCI1_TDR = TX_FRAME(0);", text)
self.assertIn("TX_INDEX = 1u;", text)
self.assertIn("SCI1_SCR |= SCI_SCR_TIE;", text)
self.assertIn("void sci1_txi_candidate_isr(void)", text)
self.assertIn("TXI runs after hardware reasserts SSR.TDRE", text)
self.assertIn("if ((SCI1_SSR & SCI_SSR_TDRE) == 0u)", text)
self.assertIn("SCI1_TDR = TX_FRAME(TX_INDEX);", text)
self.assertIn("SCI1_SSR &= (u8)~SCI_SSR_RDRF;\n byte = SCI1_RDR;", text)
self.assertIn("RX_CAPTURE(RX_INDEX) = byte;", text)
self.assertIn("return sci1_process_rx_candidate_frame();", text)
@@ -270,6 +281,120 @@ class SerialPseudocodeTest(unittest.TestCase):
},
"evidence_addresses_hex": ["H'BBD6", "H'BE29"],
},
"gate_queue_model": {
"predicates": [
{
"name": "main_loop_may_enter_report_builder",
"condition_candidate": "FAA2 == 0 && F9C0 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0))",
"summary": "loc_3FD3 gates loc_BAF2.",
},
{
"name": "queue_has_pending_report",
"condition_candidate": "F9B5 != F9B0",
"summary": "Queue non-empty path stages through BB43/BA26.",
},
{
"name": "periodic_resend_may_fire",
"condition_candidate": "(FAA5 & FAA3 & 0x80) != 0 && F9C6 == 0 && F9C8 != 0",
"summary": "BE9E/BED5 resend gate.",
},
],
"session_effects": [
{
"name": "rx_completion_sets_session_timer",
"summary": "RX completion sets F9C5.",
},
{
"name": "session_timeout_clears_gate_and_queue",
"summary": "loc_3FEF clears F9B5/F9B0 and clears or sets FAA5.",
},
{
"name": "host_ack_can_advance_queue",
"summary": "Commands 0x05/0x06 can ack or advance F9B5.",
"command_values_hex": ["H'05", "H'06"],
},
],
"caveat": "Many panel controls may require host/session traffic before reporting; observed autonomous call/cam-power indexes are runtime/capture overlays, not ROM constants.",
"evidence_addresses_hex": ["H'3FD3", "H'3FEB", "H'BAF2", "H'BB43", "H'BE9E", "H'BED5"],
},
"tx_report_model": {
"entry_label": "loc_BB43",
"value_source_candidate": "current_value_table_candidate",
"observed_capture_overlay_candidates": [
{
"name_candidate": "heartbeat_or_idle_report_candidate",
"observed_frames_hex": ["00 00 00 00 80 DA"],
},
{
"name_candidate": "call_button_report_candidate",
"observed_frames_hex": ["00 00 15 80 00 CF", "00 00 15 00 00 4F"],
},
{
"name_candidate": "camera_power_report_candidate",
"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"],
},
"periodic_resend_model": {
"period_timer": {
"reload_value_hex": "H'01F4",
"summary": "Candidate periodic report/heartbeat timer reload.",
},
"resend_countdown": {
"reload_value_hex": "H'14",
"summary": "Candidate periodic resend countdown.",
},
"pending_mask": {
"mask_hex": "H'80",
"summary": "Candidate autonomous report pending mask.",
},
"resend_path": {
"summary": "Candidate periodic resend path feeding TX staging.",
},
"evidence_addresses_hex": ["H'BE90", "H'BED5"],
},
"timer_interrupt_model": {
"source": "FRT1 OCIA",
"vector_address_hex": "H'BEEA",
"counters": [
{
"address": 0xF9C0,
"address_hex": "H'F9C0",
"name_candidate": "tx_report_gate_counter_candidate",
"role": "candidate report gate counter.",
},
{
"address": 0xF9C1,
"address_hex": "H'F9C1",
"name_candidate": "rx_interbyte_timeout_candidate",
"role": "candidate RX interbyte timeout counter.",
},
{
"address": 0xF9C6,
"address_hex": "H'F9C6",
"name_candidate": "periodic_resend_cadence_counter_candidate",
"role": "candidate periodic resend cadence counter.",
},
],
"evidence_addresses_hex": ["H'BEEA", "H'BEF4"],
},
}
]
}
@@ -287,8 +412,69 @@ class SerialPseudocodeTest(unittest.TestCase):
self.assertIn("serial_session_flags_candidate H'FAA2: reads 1, writes 2; bits 7", text)
self.assertIn("retry/error model candidate:", text)
self.assertIn("checksum path: 0x5A-seeded XOR over RX[0..4] differs from RX[5] -> loc_BE29", text)
self.assertIn("gate/queue state machine candidate:", text)
self.assertIn("main_loop_may_enter_report_builder: FAA2 == 0 && F9C0 == 0", text)
self.assertIn("queue_has_pending_report: F9B5 != F9B0", text)
self.assertIn("host_ack_can_advance_queue: Commands 0x05/0x06 can ack or advance F9B5", text)
self.assertIn("static bool sci1_candidate_main_report_gate_open(void)", text)
self.assertIn("static bool sci1_candidate_report_queue_nonempty(void)", text)
self.assertIn("static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)", text)
self.assertIn("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)
self.assertIn("interrupt/timer architecture candidate:", text)
self.assertIn("FRT1 OCIA H'BEEA appears to be a periodic tick ISR", text)
self.assertIn("H'F9C0 tx_report_gate_counter_candidate: candidate report gate counter.", text)
self.assertIn("H'F9C1 rx_interbyte_timeout_candidate: candidate RX interbyte timeout counter.", text)
self.assertIn("H'F9C6 periodic_resend_cadence_counter_candidate: candidate periodic resend cadence counter.", text)
self.assertIn("void frt1_ocia_candidate_tick_isr(void)", text)
self.assertIn("MEM8[0xF9C0u] = (u8)(MEM8[0xF9C0u] - 1u);", text)
self.assertIn("MEM8[0xF9C1u] = (u8)(MEM8[0xF9C1u] - 1u);", text)
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
def test_timer_source_models_emit_separate_tick_isrs(self):
analysis = {
"protocol_semantics": [
{
"confidence": "medium",
"confidence_score": 0.6,
"timer_interrupt_model": {
"sources": [
{
"source": "FRT2 OCIA",
"handler_address_hex": "H'BF23",
"summary": "Candidate periodic tick ISR for idle heartbeat/report counters.",
"counters": [
{
"address": 0xF9C4,
"address_hex": "H'F9C4",
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
"role": "candidate idle/default report enqueue countdown.",
}
],
}
]
},
}
]
}
with patch("h8536.serial_pseudocode.analyze_serial_semantics", return_value=analysis):
text = generate_serial_pseudocode(candidate_payload())
self.assertIn("FRT2 OCIA H'BF23", text)
self.assertIn("H'F9C4 idle_heartbeat_gate_countdown_candidate", text)
self.assertIn("void frt2_ocia_candidate_tick_isr(void)", text)
self.assertIn("MEM8[0xF9C4u] = (u8)(MEM8[0xF9C4u] - 1u);", text)
def test_tx_only_option_omits_rx_functions(self):
text = generate_serial_pseudocode(
candidate_payload(),

View File

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

View File

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

View File

@@ -103,8 +103,30 @@ def planned_semantics_payload() -> dict:
instruction(0xBF14, "BEQ", "loc_C100", targets=[0xC100]),
instruction(0xBF18, "CMP:E.B", "#H'06, R0"),
instruction(0xBF1C, "BEQ", "loc_C600", targets=[0xC600]),
instruction(0xBF1E, "CMP:E.B", "#H'05, R0"),
instruction(0xBF1F, "BEQ", "loc_C500", targets=[0xC500]),
instruction(0xBF20, "CMP:E.B", "#H'07, R0"),
instruction(0xBF24, "BEQ", "loc_C700", targets=[0xC700]),
instruction(0x3FD3, "TST.B", "@H'FAA2", [0xFAA2]),
instruction(0x3FD9, "BTST.B", "#7, @H'FAA5", [0xFAA5]),
instruction(0x3FDF, "TST.B", "@H'F9C3", [0xF9C3]),
instruction(0x3FE5, "TST.B", "@H'F9C0", [0xF9C0]),
instruction(0x3FEB, "BSR", "loc_BAF2", targets=[0xBAF2]),
instruction(0x3FEF, "TST.B", "@H'F9C5", [0xF9C5]),
instruction(0x3FF5, "CLR.B", "@H'F9B5", [0xF9B5]),
instruction(0x3FF9, "CLR.B", "@H'F9B0", [0xF9B0]),
instruction(0x3FFD, "BCLR.B", "#7, @H'FAA5", [0xFAA5]),
instruction(0x4007, "BSET.B", "#7, @H'FAA5", [0xFAA5]),
instruction(0x4046, "TST.B", "@H'F9C4", [0xF9C4]),
instruction(0x404C, "BTST.B", "#7, @H'FAA5", [0xFAA5]),
instruction(0x4050, "BEQ", "loc_4059", targets=[0x4059]),
instruction(0x4052, "TST.B", "@H'F9C3", [0xF9C3]),
instruction(0x405F, "CMP:G.B", "@H'F9B5, R2", [0xF9B5]),
instruction(0x4067, "MOV:G.W", "#H'00, @(-H'0790,R2)"),
instruction(0x406C, "ADD:Q.B", "#1, @H'F9B0", [0xF9B0]),
instruction(0x4070, "BCLR.B", "#7, @H'F9B0", [0xF9B0]),
instruction(0x40E0, "MOV:G.B", "#H'14, @H'F9C4", [0xF9C4]),
instruction(0xBA31, "MOV:G.B", "#H'07, @H'F9C4", [0xF9C4]),
instruction(0xC000, "MOV:G.B", "@H'F861, R1", [0xF861]),
instruction(0xC004, "MOV:G.B", "@H'F862, R2", [0xF862]),
instruction(0xC008, "BSR", "loc_622B", targets=[0x622B]),
@@ -130,6 +152,9 @@ def planned_semantics_payload() -> dict:
instruction(0xC124, "MOV:G.W", "R3, @H'F853", [0xF853]),
instruction(0xC128, "MOV:G.B", "@H'F9B5, R6", [0xF9B5]),
instruction(0xC12C, "BSR", "loc_BA26", targets=[0xBA26]),
instruction(0xC500, "ADD:Q.B", "#1, @H'F9B5", [0xF9B5]),
instruction(0xC504, "BCLR.B", "#7, @H'F9B5", [0xF9B5]),
instruction(0xC508, "BCLR.B", "#7, @H'FAA3", [0xFAA3]),
instruction(0xC600, "MOV:G.B", "@H'F861, R1", [0xF861]),
instruction(0xC604, "MOV:G.B", "@H'F862, R2", [0xF862]),
instruction(0xC608, "BSR", "loc_622B", targets=[0x622B]),
@@ -143,6 +168,25 @@ def planned_semantics_payload() -> dict:
instruction(0xC70C, "BSR", "loc_BA26", targets=[0xBA26]),
instruction(0xC800, "CMP:E.B", "@H'F865, R7", [0xF865]),
instruction(0xC804, "BNE", "loc_C700", targets=[0xC700]),
instruction(0xBB20, "MOV:G.B", "#H'00, @H'F850", [0xF850]),
instruction(0xBB24, "MOV:G.B", "#H'00, @H'F851", [0xF851]),
instruction(0xBB28, "MOV:G.B", "#H'15, @H'F852", [0xF852]),
instruction(0xBB2C, "MOV:G.W", "@(-H'1800,R4), R0"),
instruction(0xBB30, "MOV:G.W", "R0, @H'F853", [0xF853]),
instruction(0xBB43, "BSR", "loc_BA26", targets=[0xBA26]),
instruction(0xBE90, "MOV:G.W", "#H'01F4, @H'F9C6", [0xF9C6]),
instruction(0xBE94, "MOV:G.B", "#H'14, @H'F9C8", [0xF9C8]),
instruction(0xBE98, "MOV:G.B", "#H'80, @H'FAA3", [0xFAA3]),
instruction(0xBE9C, "MOV:G.B", "#H'01, @H'FAA2", [0xFAA2]),
instruction(0xBEA0, "MOV:G.B", "@H'F9B5, R1", [0xF9B5]),
instruction(0xBEA4, "MOV:G.B", "#H'01, @H'F9C0", [0xF9C0]),
instruction(0xBE9E, "MOV:G.B", "@H'FAA5, R0", [0xFAA5]),
instruction(0xBEA5, "AND.B", "@H'FAA3, R0", [0xFAA3]),
instruction(0xBEB5, "TST.W", "@H'F9C6", [0xF9C6]),
instruction(0xBEBB, "TST.B", "@H'F9C8", [0xF9C8]),
instruction(0xBED5, "MOV:G.B", "@H'F858, R0", [0xF858]),
instruction(0xBED9, "MOV:G.B", "R0, @H'F850", [0xF850]),
instruction(0xBEE0, "BSR", "loc_BA26", targets=[0xBA26]),
]
)
@@ -285,6 +329,8 @@ class SerialSemanticsTest(unittest.TestCase):
self.assertIn("faa2", state_text)
self.assertIn("f9b5", state_text)
self.assertIn("f9c0", state_text)
self.assertIn("f9c4", state_text)
self.assertIn("idle_heartbeat_gate_countdown", state_text)
def test_planned_retry_error_model_identifies_retransmit_and_checksum_error(self):
semantics = only_semantics(self, planned_semantics_payload())
@@ -296,6 +342,81 @@ class SerialSemanticsTest(unittest.TestCase):
self.assertIn("0x07", retry_text)
self.assertIn("checksum_error_response", retry_text)
def test_tx_report_model_separates_autonomous_reports_from_rx_commands(self):
semantics = only_semantics(self, planned_semantics_payload())
report = semantics["tx_report_model"]
report_text = semantic_text(report)
self.assertEqual(report["direction"], "device_to_host_autonomous_report_candidate")
self.assertIn("bb43", report_text)
self.assertIn("ba26", report_text)
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):
semantics = only_semantics(self, planned_semantics_payload())
periodic = semantics["periodic_resend_model"]
periodic_text = semantic_text(periodic)
self.assertIn("f9c6", periodic_text)
self.assertIn("01f4", periodic_text)
self.assertIn("f9c8", periodic_text)
self.assertIn("14", periodic_text)
self.assertIn("faa3", periodic_text)
self.assertIn("80", periodic_text)
self.assertIn("bed5", periodic_text)
def test_gate_queue_model_surfaces_autonomous_tx_state_machine(self):
semantics = only_semantics(self, planned_semantics_payload())
gate = semantics["gate_queue_model"]
gate_text = semantic_text(gate)
self.assertIn("3fd3", gate_text)
self.assertIn("baf2", gate_text)
self.assertIn("faa2 == 0", gate_text)
self.assertIn("f9c0 == 0", gate_text)
self.assertIn("f9c4 == 0", gate_text)
self.assertIn("h'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)
self.assertIn("bed5", gate_text)
self.assertIn("f9c5", gate_text)
self.assertIn("commands 0x05", gate_text)
self.assertIn("0x06", gate_text)
self.assertIn("not rom constants", gate_text)
def test_timer_interrupt_model_surfaces_frt2_idle_heartbeat_counter(self):
semantics = only_semantics(
self,
base_payload(
[
instruction(0xBF23, "BCLR.B", "#5, @FRT2_TCSR"),
instruction(0xBF27, "TST.B", "@H'F9C4", [0xF9C4]),
instruction(0xBF2D, "ADD:Q.B", "#-1, @H'F9C4", [0xF9C4]),
instruction(0xBF31, "TST.B", "@H'F9C5", [0xF9C5]),
instruction(0xBF37, "ADD:Q.B", "#-1, @H'F9C5", [0xF9C5]),
]
),
)
timer = semantics["timer_interrupt_model"]
timer_text = semantic_text(timer)
self.assertIn("frt2 ocia", timer_text)
self.assertIn("f9c4", timer_text)
self.assertIn("idle_heartbeat_gate_countdown", timer_text)
self.assertIn("phi/32", timer_text)
def test_missing_serial_reconstruction_candidates_emit_no_protocol_semantics(self):
payload = {
"serial_reconstruction": {"candidates": []},

157
tests/test_table_xrefs.py Normal file
View File

@@ -0,0 +1,157 @@
import json
import tempfile
import unittest
from pathlib import Path
from h8536.table_xrefs import analyze_table_xrefs, generate_table_xref_report, write_table_xrefs
def reference(address: int) -> dict:
return {"address": address}
def instruction(
address: int,
mnemonic: str,
operands: str = "",
references: list[int] | None = None,
text: str | None = None,
) -> dict:
return {
"address": address,
"mnemonic": mnemonic,
"operands": operands,
"text": text or f"{mnemonic} {operands}".strip(),
"references": [reference(item) for item in (references or [])],
"targets": [],
}
def payload() -> dict:
return {
"call_graph": {
"nodes": [
{"start": 0xC000, "end": 0xC0FF, "label": "loc_C000"},
{"start": 0xD000, "end": 0xD0FF, "label": "loc_D000"},
],
},
"serial_semantics": {
"table_map_candidates": [
{
"kind": "logical_table_map_candidate",
"name_candidate": "primary_value_table_candidate",
"confidence": "candidate-medium",
"accesses": [{"instruction_address": 0xC004}],
}
],
},
"lcd_text": {
"strings": [
{
"address": 0x77F4,
"text": "COMM LINK ITEM-1",
"trimmed": "COMM LINK ITEM-1",
"confidence": "high",
"xrefs": [
{
"address": 0x7804,
"following_bsr": {"address": 0x7807, "target": 0x5A91},
}
],
"xref_count": 1,
}
],
},
"lcd_driver": {
"routines": [
{
"start": 0x3F40,
"end": 0x3F74,
"role_hint": "lcd_wait_and_transfer",
"roles": ["lcd_data_write"],
}
],
},
"instructions": [
instruction(0xC000, "MOV:G.W", "#H'0006, R3"),
instruction(0xC004, "MOV:G.W", "@(-H'2000,R3), R0"),
instruction(0xC008, "MOV:G.W", "R1, @(-H'1800,R4)"),
instruction(0xD000, "MOV:G.W", "@H'F900, R2", [0xF900]),
instruction(0xD004, "BSET.B", "#7, @(-H'1400,R5)"),
instruction(0xD008, "CMP:G.W", "@H'E124, R0", [0xE124]),
instruction(0xD00C, "MOV:G.W", "R3, @H'F922", [0xF922]),
],
}
class TableXrefsTest(unittest.TestCase):
def test_reports_logical_direct_static_and_dynamic_accesses(self):
analysis = analyze_table_xrefs(payload())
tables = {table["name"]: table for table in analysis["tables"]}
primary = tables["primary_value_table_candidate"]
self.assertEqual(primary["access_count"], 3)
self.assertEqual(primary["read_count"], 3)
self.assertEqual(primary["static_offsets"], [0, 6, 0x124])
static_access = primary["accesses"][0]
self.assertEqual(static_access["index"], 6)
self.assertEqual(static_access["logical_address"], 0xE006)
self.assertEqual(static_access["function_label"], "loc_C000")
self.assertEqual(static_access["semantic_candidates"][0]["confidence"], "candidate-medium")
self.assertEqual(primary["accesses"][2]["kind"], "direct_logical_address_access")
self.assertEqual(primary["accesses"][2]["logical_address"], 0xE124)
current = tables["current_value_table_candidate"]
self.assertEqual(current["access_count"], 2)
self.assertEqual(current["dynamic_index_count"], 1)
self.assertEqual(current["accesses"][0]["index"], "dynamic")
self.assertEqual(current["accesses"][0]["index_register"], "R4")
self.assertEqual(current["accesses"][1]["direct_address"], 0xF922)
self.assertEqual(current["accesses"][1]["offset"], 2)
self.assertEqual(current["write_count"], 2)
flags = tables["flag_table_candidate"]
self.assertEqual(flags["write_count"], 1)
self.assertEqual(flags["dynamic_index_count"], 1)
lcd_terms = {
item["term"]: item
for item in analysis["lcd_correlation"]["term_hits"]
}
self.assertEqual(lcd_terms["CONNECT"]["hit_count"], 0)
self.assertEqual(lcd_terms["COMM LINK"]["hit_count"], 1)
self.assertEqual(
analysis["lcd_correlation"]["display_builder_targets"][0]["target"],
0x5A91,
)
def test_text_report_names_dynamic_registers_and_functions(self):
text = generate_table_xref_report(payload(), source_name="sample.json")
self.assertIn("Table/Index Cross-Reference Report for sample.json", text)
self.assertIn("primary_value_table_candidate H'E000", text)
self.assertIn("offset H'0006 -> H'E006", text)
self.assertIn("offset H'0124 -> H'E124", text)
self.assertIn("offset H'0002 at H'F922", text)
self.assertIn("index dynamic via R4", text)
self.assertIn("term 'CONNECT': no LCD/text candidate hits", text)
self.assertIn("term 'COMM LINK': 1 candidate", text)
self.assertIn("display builder xrefs: H'5A91:1", text)
self.assertIn("loc_C000", text)
def test_write_json_output(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"
output_path = Path(tmp) / "xrefs.json"
input_path.write_text(json.dumps(payload()), encoding="utf-8")
write_table_xrefs(input_path, output_path, as_json=True)
written = json.loads(output_path.read_text(encoding="utf-8"))
self.assertEqual(written["kind"], "table_xrefs")
self.assertEqual(written["summary"]["access_count"], 6)
self.assertEqual(written["source"], str(input_path))
if __name__ == "__main__":
unittest.main()