Compare commits
12 Commits
56829b6e0b
...
6d4d9f0027
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4d9f0027 | ||
|
|
191b72d418 | ||
|
|
e141f3b30d | ||
|
|
752148c585 | ||
|
|
3ab79648ff | ||
|
|
d2e7609bbf | ||
|
|
1fabf6587d | ||
|
|
05e1237acc | ||
|
|
81f5d7a150 | ||
|
|
9d93d88840 | ||
|
|
b264037e82 | ||
|
|
07f48c76e0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*.pyc
|
||||
captures/
|
||||
|
||||
120
README.md
120
README.md
@@ -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
54
ROM/rcp-txd-idle-only.txt
Normal 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
|
||||
9
build/rom_consistency.txt
Normal file
9
build/rom_consistency.txt
Normal 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.
|
||||
@@ -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
@@ -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 */
|
||||
}
|
||||
|
||||
256
build/rom_report_sources.json
Normal file
256
build/rom_report_sources.json
Normal 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"
|
||||
}
|
||||
}
|
||||
32
build/rom_report_sources.txt
Normal file
32
build/rom_report_sources.txt
Normal 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
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
145
build/rom_serial_gate.txt
Normal 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.
|
||||
@@ -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
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
93
build/rom_table_xrefs.txt
Normal 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
348
h8536/bench_connect_lcd.py
Normal 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
211
h8536/consistency.py
Normal 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
111
h8536/emulator/__init__.py
Normal 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",
|
||||
]
|
||||
7
h8536/emulator/__main__.py
Normal file
7
h8536/emulator/__main__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
451
h8536/emulator/bench_replay.py
Normal file
451
h8536/emulator/bench_replay.py
Normal 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
77
h8536/emulator/cli.py
Normal 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
|
||||
48
h8536/emulator/constants.py
Normal file
48
h8536/emulator/constants.py
Normal 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
34
h8536/emulator/cpu.py
Normal 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
16
h8536/emulator/errors.py
Normal 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
|
||||
152
h8536/emulator/fast_paths.py
Normal file
152
h8536/emulator/fast_paths.py
Normal 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
136
h8536/emulator/memory.py
Normal 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)
|
||||
15
h8536/emulator/peripherals/__init__.py
Normal file
15
h8536/emulator/peripherals/__init__.py
Normal 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",
|
||||
]
|
||||
69
h8536/emulator/peripherals/lcd.py
Normal file
69
h8536/emulator/peripherals/lcd.py
Normal 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",
|
||||
]
|
||||
80
h8536/emulator/peripherals/p9_bus.py
Normal file
80
h8536/emulator/peripherals/p9_bus.py
Normal 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
1265
h8536/emulator/probe.py
Normal file
File diff suppressed because it is too large
Load Diff
681
h8536/emulator/runner.py
Normal file
681
h8536/emulator/runner.py
Normal 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
475
h8536/emulator/rx_probe.py
Normal 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
119
h8536/emulator/sci.py
Normal 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
535
h8536/protocol_capture.py
Normal 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
504
h8536/protocol_trace.py
Normal 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",
|
||||
]
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
548
h8536/report_source_trace.py
Normal file
548
h8536/report_source_trace.py
Normal 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())
|
||||
@@ -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
552
h8536/serial_gate.py
Normal 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())
|
||||
@@ -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);",
|
||||
|
||||
@@ -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",),
|
||||
|
||||
@@ -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
830
h8536/table_xrefs.py
Normal 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
5
h8536_consistency.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from h8536.consistency import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
8
h8536_emulator.py
Normal file
8
h8536_emulator.py
Normal 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())
|
||||
5
h8536_emulator_bench_replay.py
Normal file
5
h8536_emulator_bench_replay.py
Normal 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
8
h8536_emulator_probe.py
Normal 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())
|
||||
5
h8536_emulator_rx_probe.py
Normal file
5
h8536_emulator_rx_probe.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from h8536.emulator.rx_probe import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
8
h8536_protocol_capture.py
Normal file
8
h8536_protocol_capture.py
Normal 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
8
h8536_protocol_trace.py
Normal 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())
|
||||
8
h8536_report_source_trace.py
Normal file
8
h8536_report_source_trace.py
Normal 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
8
h8536_serial_gate.py
Normal 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
5
h8536_table_xrefs.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from h8536.table_xrefs import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyserial==3.5
|
||||
14
scripts/bench_connect_lcd_sequence.py
Normal file
14
scripts/bench_connect_lcd_sequence.py
Normal 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())
|
||||
54
tests/test_bench_connect_lcd.py
Normal file
54
tests/test_bench_connect_lcd.py
Normal 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
36
tests/test_consistency.py
Normal 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
181
tests/test_emulator.py
Normal 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()
|
||||
74
tests/test_emulator_addressing.py
Normal file
74
tests/test_emulator_addressing.py
Normal 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()
|
||||
62
tests/test_emulator_bench_replay.py
Normal file
62
tests/test_emulator_bench_replay.py
Normal 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()
|
||||
110
tests/test_emulator_fast_paths.py
Normal file
110
tests/test_emulator_fast_paths.py
Normal 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()
|
||||
116
tests/test_emulator_flags.py
Normal file
116
tests/test_emulator_flags.py
Normal 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()
|
||||
48
tests/test_emulator_lcd.py
Normal file
48
tests/test_emulator_lcd.py
Normal 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()
|
||||
360
tests/test_emulator_probe.py
Normal file
360
tests/test_emulator_probe.py
Normal 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()
|
||||
104
tests/test_emulator_probe_sci.py
Normal file
104
tests/test_emulator_probe_sci.py
Normal 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()
|
||||
27
tests/test_emulator_rx_probe.py
Normal file
27
tests/test_emulator_rx_probe.py
Normal 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()
|
||||
71
tests/test_emulator_sci_timing.py
Normal file
71
tests/test_emulator_sci_timing.py
Normal 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()
|
||||
131
tests/test_emulator_timers.py
Normal file
131
tests/test_emulator_timers.py
Normal 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
42
tests/test_p9_bus.py
Normal 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()
|
||||
172
tests/test_protocol_capture.py
Normal file
172
tests/test_protocol_capture.py
Normal 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()
|
||||
154
tests/test_protocol_trace.py
Normal file
154
tests/test_protocol_trace.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
135
tests/test_report_source_trace.py
Normal file
135
tests/test_report_source_trace.py
Normal 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()
|
||||
@@ -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
205
tests/test_serial_gate.py
Normal 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()
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
157
tests/test_table_xrefs.py
Normal 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()
|
||||
Reference in New Issue
Block a user