1
0

Compare commits

..

3 Commits

Author SHA1 Message Date
Aiden
74a2e2fd2c testing around OK state 2026-05-26 14:01:10 +10:00
Aiden
4e0ef92e25 Communication 2026-05-26 13:16:50 +10:00
Aiden
85732f8754 RX side improvements 2026-05-26 12:33:51 +10:00
17 changed files with 1820 additions and 21 deletions

View File

@@ -46,7 +46,9 @@ To start the current emulator harness:
.\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 4000000 --stop-on-tx .\.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_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 h8536_emulator_rx_probe.py --preset connect-lcd
.\.venv\Scripts\python.exe h8536_emulator_rx_divergence.py --default-frames --uart-timing --wait-heartbeats 2 --summary-only
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen .\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite minimal --prompt-observation --result-json captures\connect-ok-minimal-result.json
.\.venv\Scripts\python.exe scripts\serial_ack_probe.py --ack-frame "05 00 40 00 00 1F" .\.venv\Scripts\python.exe scripts\serial_ack_probe.py --ack-frame "05 00 40 00 00 1F"
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json .\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\table-sweep-ack-000-07f.json --log captures\table-sweep-ack-000-07f.txt --result-json captures\table-sweep-ack-000-07f-result.json .\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\table-sweep-ack-000-07f.json --log captures\table-sweep-ack-000-07f.txt --result-json captures\table-sweep-ack-000-07f-result.json
@@ -59,6 +61,28 @@ To start the current emulator harness:
The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed. The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed.
The current PT2/protocol reconstruction is documented in [docs/pt2-protocol.md](docs/pt2-protocol.md).
## Real Bench Serial Format
The real RCP serial link is `38400 8E1`, not `38400 8N1`. This is backed by the ROM SCI1 init:
- `build/rom_decompiled.asm:437`: `SCI1_SMR = H'24`, async 8-bit, even parity, 1 stop.
- `build/rom_decompiled.asm:438`: `SCI1_SCR = H'3C`, RX/TX enabled.
- `build/rom_decompiled.asm:439`: `SCI1_BRR = H'07`.
The traced board path is H8/536 SCI1 through the MAX202: H8 pin 66 `P95/TXD` to MAX202 pin 11, and MAX202 pin 12 to H8 pin 67 `P96/RXD`.
Bench scripts default to even parity now. Keep `--parity E` explicit in important captures, and use `--parity N` only to reproduce older 8N1 captures. With the wrong 8N1 format, commands fall into the RX error/retry path instead of the normal command handlers; apparent `07...` frames from those captures should be treated as error/retry echoes until repeated under 8E1.
Confirmed bench result under 8E1: the CONNECT path can reach `CONNECT: OK`, the CAM POWER lamp illuminates, and the numeric readouts illuminate as `----`.
Minimal smoke-test shape:
```powershell
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --parity E --prompt-screen --log captures\8e1-connect-ok-smoke.txt
```
## What It Does ## What It Does
- Decodes the H8/500 instruction set used by the H8/536. - Decodes the H8/500 instruction set used by the H8/536.
@@ -101,8 +125,9 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\
- 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. - 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 timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, an X24164 two-wire EEPROM model on traced `P91/SCL` and `P97/SDA`, logical EEPROM image load/save/reporting, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path. - 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 timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, an X24164 two-wire EEPROM model on traced `P91/SCL` and `P97/SDA`, logical EEPROM image load/save/reporting, 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 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, can optionally schedule 38400 8N1 byte arrivals at real UART spacing, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects. - Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, can optionally schedule bench-style UART byte arrivals at real spacing, 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 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 CONNECT: OK bench matrix runner that power-cycles between cases and tests the known sequence, single frames, primer pairs, order permutations, inter-frame gaps, repeats, and hold time to separate magic-frame, primer, cadence, and latch behavior.
- Includes a bench ACK probe that reproduces the `01 00 00...` -> `01 00 01...` visible retry burst, waits for `07 80 40 20 90 2D`, then sends a candidate command-5 ACK and reports whether the target keeps repeating. - Includes a bench ACK probe that reproduces the `01 00 00...` -> `01 00 01...` visible retry burst, waits for `07 80 40 20 90 2D`, then sends a candidate command-5 ACK and reports whether the target keeps repeating.
- Includes a checksum-resynchronizing bench receiver that scans RX byte streams for valid six-byte frames, avoids common shifted-heartbeat false locks, and can fall back to the old fixed six-byte slicer with `--sync fixed`. - Includes a checksum-resynchronizing bench receiver that scans RX byte streams for valid six-byte frames, avoids common shifted-heartbeat false locks, and can fall back to the old fixed six-byte slicer with `--sync fixed`.
- Includes a JSON scenario bench runner for repeatable multi-step serial tests, including low-latency ACK-aware command-1 probes that can send the current command-5 ACK candidate immediately after the retry frame appears, with explicit max-ACK/max-target guardrails. - Includes a JSON scenario bench runner for repeatable multi-step serial tests, including low-latency ACK-aware command-1 probes that can send the current command-5 ACK candidate immediately after the retry frame appears, with explicit max-ACK/max-target guardrails.
@@ -120,6 +145,9 @@ Current serial observations:
- Emulator timing finding: the ROM initializes FRT2 with `TCR=H'02` and `OCRA=H'7A12`; using the manual's `phi/32` prescaler gives a 1,000,000-cycle OCIA period, so the default `--clock-hz 10000000` models that tick as 100 ms and the post-send `F9C4=H'07` heartbeat delay as about 700 ms. - Emulator timing finding: the ROM initializes FRT2 with `TCR=H'02` and `OCRA=H'7A12`; using the manual's `phi/32` prescaler gives a 1,000,000-cycle OCIA period, so the default `--clock-hz 10000000` models that tick as 100 ms and the post-send `F9C4=H'07` heartbeat delay as about 700 ms.
- 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`. - 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. - 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.
- Bench serial-format finding: real hardware talks `38400 8E1`. Earlier `8N1` captures primarily exercised SCI1 parity/error handling and retry echoes, not the normal command path. After switching bench scripts to even parity, the selector-zero CONNECT path can reach `CONNECT: OK`.
- Bench CONNECT recovery finding: `CONNECT:NOT ACT` is recoverable without a power cycle. This makes it a normal no-active-session/cleared-state display rather than a terminal latch; tests can now probe from the idle NOT ACT state directly, then separately check whether OK is held or needs periodic CCU-like refresh traffic.
- Bench CONNECT cadence finding: the `40 -> 80 -> C0` sequence stayed at `CONNECT:NOT ACT` with 10 ms, 50 ms, and 150 ms gaps, but produced `CONNECT: OK` then returned to `CONNECT:NOT ACT` with 700 ms and 1.5 s gaps. At 700 ms, single `40`/`80`/`C0` frames did not work, but all tested two-frame pairs did. Repeated `80 -> 80` at about 700 ms also worked, so the values do not need to differ. The no-power-cycle NOT ACT recovery capture produced repeated `02 00 02 00 00 5A` OK-path responses before heartbeat traffic resumed.
- Board/P9 finding: traced MCU pin 62 `P91` reaches X24164 pin 6 `SCL`, and MCU pin 68 `P97` reaches the shared X24164 pin 5 `SDA` node. The emulator now treats the ROM's `C121/C08B/C0DB/C10C/C142` P9 routines as an X24164-style two-wire EEPROM bus, with ROM logical addresses `0x000-0x7FF` on the `H'A0/H'A1` control-byte family and `0x800-0xFFF` on `H'E0/H'E1`. - Board/P9 finding: traced MCU pin 62 `P91` reaches X24164 pin 6 `SCL`, and MCU pin 68 `P97` reaches the shared X24164 pin 5 `SDA` node. The emulator now treats the ROM's `C121/C08B/C0DB/C10C/C142` P9 routines as an X24164-style two-wire EEPROM bus, with ROM logical addresses `0x000-0x7FF` on the `H'A0/H'A1` control-byte family and `0x800-0xFFF` on `H'E0/H'E1`.
- EEPROM role finding: `loc_40BB` checks `P7DR.7` and the `F402 == H'6B6F` signature before defaulting EEPROM/shadow tables; `loc_4103` writes ROM default words through `BFE0`, `loc_41D2` reads sixteen 8-byte records into `F7B0-F82F`, and the command-4 path at `BD2B-BD5F` can persist serial table writes when `F76E.7` is set. - EEPROM role finding: `loc_40BB` checks `P7DR.7` and the `F402 == H'6B6F` signature before defaulting EEPROM/shadow tables; `loc_4103` writes ROM default words through `BFE0`, `loc_41D2` reads sixteen 8-byte records into `F7B0-F82F`, and the command-4 path at `BD2B-BD5F` can persist serial table writes when `F76E.7` is set.
- EEPROM layout finding: `build\rom_eeprom_layout.txt` currently identifies the ROM factory table at `H'C964-H'CA63`, the F400 shadow defaults, page 0 offset `0x000-0x007` as the signature/options header (`00 00 6B 6F FE 00 00 00`), pages 1-F offset `0x00-0x07` as blank-by-default record slots, and 89 selector mappings from the `H'C564` table into F400/EEPROM offsets. `F404` defaults to `H'FE00` and is tested as option/feature bits, while `F76E` combines persistence enable, dispatch suppression, and low-nibble EEPROM page selection. - EEPROM layout finding: `build\rom_eeprom_layout.txt` currently identifies the ROM factory table at `H'C964-H'CA63`, the F400 shadow defaults, page 0 offset `0x000-0x007` as the signature/options header (`00 00 6B 6F FE 00 00 00`), pages 1-F offset `0x00-0x07` as blank-by-default record slots, and 89 selector mappings from the `H'C564` table into F400/EEPROM offsets. `F404` defaults to `H'FE00` and is tested as option/feature bits, while `F76E` combines persistence enable, dispatch suppression, and low-nibble EEPROM page selection.
@@ -237,6 +265,7 @@ For the emulator harness:
python h8536_emulator.py --help python h8536_emulator.py --help
python h8536_emulator_probe.py --help python h8536_emulator_probe.py --help
python h8536_emulator_rx_probe.py --help python h8536_emulator_rx_probe.py --help
python h8536_emulator_rx_divergence.py --help
``` ```
- `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`. - `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`.
@@ -256,9 +285,10 @@ python h8536_emulator_rx_probe.py --help
- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history. - `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history.
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame. - `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
- `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 "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
- `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with 8N1 wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`. - `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with bench-style wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`. The real bench link is `8E1`.
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates. - `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
- `scripts\serial_table_dump.py --port COM5 --relay-port COM6 --start 0x000 --count 0x200 --log captures\table-read.txt`: read-only command-1 sweep of the firmware-exposed serial table state for EEPROM/shadow inference. - `h8536_emulator_rx_divergence.py --default-frames --uart-timing --wait-heartbeats 2 --summary-only`: run the focused RX divergence trace for the bench mismatch. It flags whether a frame reached cmd0 `BC69`, cmd1 `BCD7`, retry echo, command-7 replay, autonomous `BAF2` report output, or the TX/RX overlap-collapse path.
- `scripts\serial_table_dump.py --port COM5 --relay-port COM6 --start 0x000 --count 0x200 --log captures\table-read.txt`: read-only command-1 sweep of the firmware-exposed serial table state for EEPROM/shadow inference. Bench serial scripts default to `8E1` because the ROM initializes SCI1 as async 8-bit even parity, 1 stop; pass `--parity N` only when reproducing older 8N1 captures.
- `scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json`: run the focused `0x000 -> 0x001` retry probe with immediate reactive ACK and a 2 ms poll interval, to test whether command 5 can arrive before the second `07 80 40 20 90 2D` retry. - `scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json`: run the focused `0x000 -> 0x001` retry probe with immediate reactive ACK and a 2 ms poll interval, to test whether command 5 can arrive before the second `07 80 40 20 90 2D` retry.
- `scripts\serial_scenario.py scenarios\early-ack-000-001.json --log captures\early-ack-000-001.txt --result-json captures\early-ack-000-001-result.json`: send the same command-1 pair, then send command-5 ACK immediately without waiting for the retry frame. - `scripts\serial_scenario.py scenarios\early-ack-000-001.json --log captures\early-ack-000-001.txt --result-json captures\early-ack-000-001-result.json`: send the same command-1 pair, then send command-5 ACK immediately without waiting for the retry frame.
- `scripts\serial_scenario.py scenarios\table-sweep-ack-000-07f.json --log captures\table-sweep-ack-000-07f.txt --result-json captures\table-sweep-ack-000-07f-result.json`: run a repeatable bench scenario that sweeps selectors `0x000-0x07F` and sends `05 00 40 00 00 1F` only after `07 80 40 20 90 2D` appears. The checked-in scenario stops if it reaches 8 ACKs or 32 target hits. Use `--sync fixed` only when comparing against the old non-resyncing receiver. - `scripts\serial_scenario.py scenarios\table-sweep-ack-000-07f.json --log captures\table-sweep-ack-000-07f.txt --result-json captures\table-sweep-ack-000-07f-result.json`: run a repeatable bench scenario that sweeps selectors `0x000-0x07F` and sends `05 00 40 00 00 1F` only after `07 80 40 20 90 2D` appears. The checked-in scenario stops if it reaches 8 ACKs or 32 target hits. Use `--sync fixed` only when comparing against the old non-resyncing receiver.
@@ -267,6 +297,10 @@ python h8536_emulator_rx_probe.py --help
- `h8536_emulator_state_search.py --preset connect-queue --target ok --first-hit --json-out build\connect-state-search-ok.json`: run the bounded emulator state search for the minimum selector-zero queue condition that reaches `CONNECT: OK`. The default matrix varies `E000[0]` and `F730`, seeds `F970[0]=0`, starts at `loc_2806`, and executes real ROM code into the LCD handler. - `h8536_emulator_state_search.py --preset connect-queue --target ok --first-hit --json-out build\connect-state-search-ok.json`: run the bounded emulator state search for the minimum selector-zero queue condition that reaches `CONNECT: OK`. The default matrix varies `E000[0]` and `F730`, seeds `F970[0]=0`, starts at `loc_2806`, and executes real ROM code into the LCD handler.
- `h8536_emulator_state_search.py --preset custom --pc 0x2CB9 --word E000=0x8080 --byte F730=0 --target ok`: directly test the CONNECT handler branch with explicit internal state patches. - `h8536_emulator_state_search.py --preset custom --pc 0x2CB9 --word E000=0x8080 --byte F730=0 --target ok`: directly test the CONNECT handler branch with explicit internal state patches.
- `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. - `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.
- `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --no-power-cycle --prompt-before-send --prompt-screen --post-sequence-read 10 --log captures\connect-notact-to-ok.txt`: prove the recoverable path by waiting for `CONNECT:NOT ACT`, then sending the CONNECT sequence without cycling power.
- `scripts\connect_ok_matrix.py --suite minimal --prompt-observation --result-json captures\connect-ok-minimal-result.json`: run the first reproducibility pass for the 8E1 CONNECT: OK discovery. It power-cycles between cases and tests the known sequence, each single frame, and the likely primer pairs.
- `scripts\connect_ok_matrix.py --suite gap --prompt-observation --result-json captures\connect-ok-gap-result.json`: rerun the known `40 -> 80 -> C0` order with varied inter-frame gaps to test whether cadence matters.
- `scripts\connect_ok_matrix.py --suite hold --prompt-observation --result-json captures\connect-ok-hold-result.json`: rerun the known order with longer post-send observation windows to test whether CONNECT: OK is latched or needs continued traffic.
- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode. - `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode.
- Current status: boots from `H'1000`, initializes SCI1, models the traced X24164 EEPROM bus on P9, captures P9 byte candidates, can optionally fast-path known P9 EEPROM routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`. - Current status: boots from `H'1000`, initializes SCI1, models the traced X24164 EEPROM bus on P9, captures P9 byte candidates, can optionally fast-path known P9 EEPROM routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`.
@@ -303,7 +337,7 @@ python h8536_emulator_rx_probe.py --help
- `h8536/ccu_seed_hints.py`: ROM miner for likely fake-CCU state seed selectors and candidate command/readback frames. - `h8536/ccu_seed_hints.py`: ROM miner for likely fake-CCU state seed selectors and candidate command/readback frames.
- `h8536/eeprom_layout.py`: ROM miner for X24164 EEPROM defaults, 8-byte record slots, and serial persistence mapping. - `h8536/eeprom_layout.py`: ROM miner for X24164 EEPROM defaults, 8-byte record slots, and serial persistence mapping.
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks. - `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, 38400 8N1 UART injection timing, P9/X24164 EEPROM bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding. - `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, bench-style UART injection timing, P9/X24164 EEPROM bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding.
- `h8536/emulator/eeprom_image.py`: logical EEPROM image dump/report helpers for emulator runs, including factory diffs and record-slot summaries. - `h8536/emulator/eeprom_image.py`: logical EEPROM image dump/report helpers for emulator runs, including factory diffs and record-slot summaries.
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments. - `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
- `h8536/emulator/state_search.py`: bounded internal-state search for CONNECT LCD outcomes using ROM execution plus explicit RAM/table patches. - `h8536/emulator/state_search.py`: bounded internal-state search for CONNECT LCD outcomes using ROM execution plus explicit RAM/table patches.
@@ -316,9 +350,10 @@ python h8536_emulator_rx_probe.py --help
- `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper. - `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper.
- `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers. - `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers.
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_eeprom_layout.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers. - `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_eeprom_layout.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. - `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_rx_divergence.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers.
- `h8536_emulator_state_search.py`: emulator CONNECT state-search CLI wrapper. - `h8536_emulator_state_search.py`: emulator CONNECT state-search CLI wrapper.
- `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence. - `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence.
- `scripts/serial_table_dump.py`: read-only COM5/COM6 command-1 table sweep for inferring live EEPROM-backed parameter state. - `scripts/connect_ok_matrix.py`: real-device COM5/COM6 CONNECT: OK reproducibility matrix runner for single-frame, pair, order, gap, repeat, and hold tests.
- `scripts/serial_table_dump.py`: read-only COM5/COM6 command-1 table sweep for inferring live EEPROM-backed parameter state. Bench scripts default to `38400 8E1`.
- `scripts/serial_scenario.py`: JSON-driven COM5/COM6 bench scenario runner for chained probes, waits, read sweeps, and ACK-on-target experiments. - `scripts/serial_scenario.py`: JSON-driven COM5/COM6 bench scenario runner for chained probes, waits, read sweeps, and ACK-on-target experiments.
- `scripts/state_map_runner.py`: COM5/COM6 PT2 state-map proof runner and offline bench-log analyzer. - `scripts/state_map_runner.py`: COM5/COM6 PT2 state-map proof runner and offline bench-log analyzer.

View File

@@ -0,0 +1,31 @@
Emulator EEPROM Snapshot
size=0x1000 sha256=4bed7704e1ea085487ca325c43bd60da75d37b6ae6f8292544e069a8825c64c6
writes: bytes=0 words=0 factory_diff_words=0
Persistent Records:
- page 0x0 EEPROM 0x000-0x007 bytes=00 00 6B 6F FE 00 00 00 text='..ko....'
- page 0x1 EEPROM 0x100-0x107 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x2 EEPROM 0x200-0x207 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x3 EEPROM 0x300-0x307 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x4 EEPROM 0x400-0x407 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x5 EEPROM 0x500-0x507 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x6 EEPROM 0x600-0x607 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x7 EEPROM 0x700-0x707 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x8 EEPROM 0x800-0x807 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x9 EEPROM 0x900-0x907 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xA EEPROM 0xA00-0xA07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xB EEPROM 0xB00-0xB07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xC EEPROM 0xC00-0xC07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xD EEPROM 0xD00-0xD07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xE EEPROM 0xE00-0xE07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xF EEPROM 0xF00-0xF07 bytes=20 20 20 20 20 20 20 20 text=' '
EEPROM Word Writes:
- none since EEPROM setup/load
Factory Diffs:
- current EEPROM image matches ROM factory/default image
F400 Shadow Diffs:
- H'F4AA offset=0xAA expected=0x8000 actual=0x5500 (factory_shadow_offset; selectors=0x112)

Binary file not shown.

View File

@@ -0,0 +1,31 @@
Emulator EEPROM Snapshot
size=0x1000 sha256=4bed7704e1ea085487ca325c43bd60da75d37b6ae6f8292544e069a8825c64c6
writes: bytes=0 words=0 factory_diff_words=0
Persistent Records:
- page 0x0 EEPROM 0x000-0x007 bytes=00 00 6B 6F FE 00 00 00 text='..ko....'
- page 0x1 EEPROM 0x100-0x107 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x2 EEPROM 0x200-0x207 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x3 EEPROM 0x300-0x307 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x4 EEPROM 0x400-0x407 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x5 EEPROM 0x500-0x507 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x6 EEPROM 0x600-0x607 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x7 EEPROM 0x700-0x707 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x8 EEPROM 0x800-0x807 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x9 EEPROM 0x900-0x907 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xA EEPROM 0xA00-0xA07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xB EEPROM 0xB00-0xB07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xC EEPROM 0xC00-0xC07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xD EEPROM 0xD00-0xD07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xE EEPROM 0xE00-0xE07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xF EEPROM 0xF00-0xF07 bytes=20 20 20 20 20 20 20 20 text=' '
EEPROM Word Writes:
- none since EEPROM setup/load
Factory Diffs:
- current EEPROM image matches ROM factory/default image
F400 Shadow Diffs:
- H'F4AA offset=0xAA expected=0x8000 actual=0x5500 (factory_shadow_offset; selectors=0x112)

Binary file not shown.

592
docs/pt2-protocol.md Normal file
View File

@@ -0,0 +1,592 @@
# PT2 Protocol Working Notes
This document is the current working model for the serial protocol spoken by the Sony RCP-TX7 panel ROM.
A later RCP manual mentions a "PT2 compatibility mode" for controlling the same CCU family this panel was made for. We are using "PT2" here as a practical label for this six-byte SCI1 protocol model. It is not yet a claim that every field name matches Sony's official PT2 terminology.
## Current High-Confidence Facts
- The real bench link is `38400 8E1`, not `38400 8N1`.
- The ROM uses H8/536 SCI1 through the MAX202 RS232 transceiver.
- Frames are six bytes long.
- The checksum is `0x5A XOR byte0 XOR byte1 XOR byte2 XOR byte3 XOR byte4`.
- The ROM validates the checksum before normal command dispatch.
- The first byte is decoded as `command = byte0 & 0x07`.
- Bytes 1 and 2 encode a logical selector.
- Bytes 3 and 4 are a 16-bit value for write-style commands.
- The protocol is stateful. Some commands only work while a continuation/session latch is live.
- `CONNECT:NOT ACT` is recoverable without a power cycle.
- `CONNECT: OK` can be reached on real hardware after using the correct `8E1` serial format.
## Hardware Path
SCI1 is the external serial path:
- H8/536 pin 66, `P95/TXD`, goes to MAX202 pin 11.
- MAX202 pin 12 goes to H8/536 pin 67, `P96/RXD`.
- The ROM initializes SCI1 as async 8-bit, even parity, 1 stop.
ROM evidence:
- `build/rom_decompiled.asm:437`: `SCI1_SMR = H'24`.
- `build/rom_decompiled.asm:438`: `SCI1_SCR = H'3C`.
- `build/rom_decompiled.asm:439`: `SCI1_BRR = H'07`.
Bench implication:
- Use `38400 8E1` for all real-device captures and probes.
- Old `38400 8N1` captures mostly exercised parity/error handling and retry echoes. Do not assign normal protocol meaning to old `8N1` `07...` frames until they are reproduced under `8E1`.
## Frame Format
Working host/RCP frame layout:
```text
byte0 command byte; ROM uses byte0 & 0x07
byte1 selector page/high bits; byte1.7 is rejected by normal handlers
byte2 selector low byte
byte3 value high byte
byte4 value low byte
byte5 checksum = 0x5A XOR byte0..byte4
```
Examples:
```text
00 00 00 80 80 5A ; command 0, selector 0x000, value 0x8080
01 00 00 00 00 5B ; command 1, read selector 0x000
04 00 00 80 00 DE ; command 4 shape, selector 0x000, value high 0x80
07 00 00 00 00 5D ; command 7, repeat previous finalized TX frame
```
## Selector Decode
The ROM builds a raw selector from bytes 1 and 2, then maps it through `loc_622B`.
Known decode:
| byte1 page | byte2 range | selector |
| --- | --- | --- |
| page 0, or pages 4-7 | `00-7F` | `0x000 + byte2` |
| page 1 | `00-FF` | `0x080 + byte2` |
| page 2 | `00-7F` | `0x180 + byte2` |
| page 3 | any | `0x1FF` fallback |
| invalid range | any | `0x1FF` fallback |
Important caveats:
- `byte1.7` is rejected before normal command handling.
- Frames like `01 80 40 ...` may look like a selector encoding, but the normal command path rejects `byte1.7`.
- Pages 4-7 appear to alias the page-0 path when the low byte is in range.
## Checksum
Checksum formula:
```python
checksum = 0x5A
for b in frame[:5]:
checksum ^= b
```
The ROM path:
- RXI captures bytes into `F868-F86D`.
- Main-loop processing copies them to `F860-F865`.
- Physical error latch `FAA4.7` is checked before checksum dispatch.
- Checksum mismatch enters the retry/error path.
- Valid checksum clears retry counter `FAA6`, decodes selector, and dispatches on `byte0 & 0x07`.
Key ROM areas:
- RXI/ERI capture: `BB57`, `BB67`.
- Validation and checksum: `BBAB-BBF0`.
- Selector decode call: `BC01 -> 622B`.
- Command dispatch: `BC08-BC67`.
## Command Model
The biggest protocol lesson is that the command set has two modes:
- Initial dispatcher: active while `FAA2 == 0`.
- Continuation dispatcher: active while `FAA2 != 0`.
That means command numbers are not globally meaningful. Command `4`, `5`, and `6` are continuation-path commands, not normal idle commands.
| Command | Path | Current meaning | Response |
| --- | --- | --- | --- |
| `0x00` | initial | Set primary/current value, queue selector processing | Immediate `0x04` echo-style response |
| `0x01` | initial | Read primary value table | Immediate `0x04` readback response |
| `0x02` | initial | Quiet clear/no-op style command | No immediate response seen in ROM |
| `0x04` | continuation | Set/update value without immediate response | Usually no immediate response |
| `0x05` | continuation | ACK/session-clear/pending handling | Usually no immediate response |
| `0x06` | continuation | Set secondary value table | Usually no immediate response |
| `0x07` | both | Retransmit previous finalized TX frame | Repeats last TX frame |
## Command Details
### Command 0: Initial Set Value
Path: `BC69`.
Conditions:
- Valid checksum.
- `FAA2 == 0`.
- `byte1.7 == 0`.
Effects:
- Writes value into `E000 + 2*selector`.
- Writes value into `E800 + 2*selector`.
- Sets dirty flag bit 7 in `EC00 + selector`.
- Calls `BE70`, which appends the selector into the `F970` selector-processing queue.
- Sends an immediate `0x04` response through `BA26`.
Selector-zero special case:
- For selector `0x000`, the low byte is forced to `0x80`.
- This makes selector-zero writes look like `xx80`, not arbitrary `xxLL`.
Important candidate:
```text
00 00 00 80 80 5A ; selector 0 = 0x8080, strongest CONNECT OK seed
```
### Command 1: Read Value
Path: `BCD7`.
Effects:
- Reads `E000 + 2*selector`.
- Stages a `0x04` response.
- Clears `FAA2.7`.
Useful readback examples:
```text
01 00 00 00 00 5B ; read selector 0x000
01 00 40 00 00 1B ; read selector 0x040
01 01 76 00 00 2C ; read selector 0x0F6
```
Bench implication:
- Command 1 verifies table state.
- It is not an ACK and does not enter continuation handling.
- Command 7 after command 1 can repeat the last finalized readback.
### Command 2: Initial Clear/No-Op Candidate
Path: `BD04`.
Effects:
- Clears `FAA2.7`.
- Returns without staging an obvious response.
Meaning is still unclear. Treat it as a quiet/session-control candidate, not as a data write.
### Command 4: Continuation Set Value
Path: `BD0E`.
Conditions:
- Valid checksum.
- `FAA2 != 0`.
- Command bit 2 set.
- `byte1.7 == 0`.
Effects:
- Writes value into `E000 + 2*selector`.
- Selector zero also updates `E800`.
- Nonzero selectors set dirty flag bit 7 in `EC00 + selector`.
- Can mirror/persist mapped nonzero selectors through the `F400`/EEPROM path when `F76E.7` allows it.
- If `FAA2.3` was set by a queued report, command 4 can advance `F9B5` to consume that report.
- Clears `FAA3` and `FAA2` before exit.
Known CONNECT test frames:
```text
04 00 00 40 00 1E
04 00 00 80 00 DE
04 00 00 C0 00 9E
```
ROM caveat:
- A standalone command 4 from a truly idle `FAA2 == 0` state should not reach `BD0E`.
- Bench evidence now proves the panel can still recover from `CONNECT:NOT ACT` without power cycling, so visible `NOT ACT` is not equivalent to "all serial continuation state is impossible".
### Command 5: Continuation ACK/Clear Candidate
Path: `BD80`.
Conditions:
- Continuation path only.
Effects:
- Usually no immediate response.
- Selectors `0x006C`, `0x006D`, and `0x006E` call `BE70`.
- If `F731.7` is set, selectors `0x006B`, `0x0096`, `0x0097`, `0x00C6`, and `0x00F8` clear `F731.7/F790.7`.
- If `FAA2.3` was set by a queued report, command 5 can advance `F9B5`.
- Clears `FAA3` and `FAA2` before exit.
Bench implication:
- Command 5 is not a generic always-live ACK.
- It only has ACK-like meaning when the continuation latch is live.
### Command 6: Continuation Secondary Set
Path: `BDDB`.
Effects:
- Writes value into `E400 + 2*selector`.
- Sets dirty flag bit 6 in `EC00 + selector`.
- Can advance queued-report state when `FAA2.3` is live.
- Clears `FAA3` and `FAA2`.
### Command 7: Repeat Previous TX
Path: `BE05`.
Effects:
- Copies the previous finalized TX frame back into staging.
- Sends it again through `BA26`.
- Works from initial and continuation paths.
Bench implication:
- Command 7 is useful as a "what did you last finalize?" probe.
- It does not prove a hidden continuation token by itself.
## RCP Transmit Frames
The TX side uses the same six-byte checksum model.
TX staging:
- `F850-F854`: staging bytes.
- `F858-F85C`: finalized bytes.
- `F85D`: computed checksum.
- `BA26`: finalizes and starts SCI1 TX.
- TXI sends bytes 1-5 after the first TDR write.
Known RCP-origin frames:
| Frame | Confidence | Current meaning |
| --- | --- | --- |
| `00 00 00 00 80 DA` | high | idle heartbeat / selector-zero report |
| `00 00 07 80 00 DD` | medium-high | observed CAM POWER button/report candidate |
| `00 00 15 80 00 CF` | medium-high | observed CALL on/report candidate |
| `00 00 15 00 00 4F` | medium-high | observed CALL off/report candidate |
| `02 00 02 00 00 5A` | medium | emulator CONNECT OK path response candidate |
Heartbeat:
- Idle frame: `00 00 00 00 80 DA`.
- Observed cadence: about 700 ms.
- ROM path: `loc_4067` enqueues selector 0, `loc_BAF2/BB08` dequeues it, `BB1C/BB20/BB2B` stages TX bytes, `BA26` emits the frame.
- FRT2 timing model: `TCR=H'02`, `OCRA=H'7A12`, modeled as a 100 ms tick at 10 MHz; `F9C4=0x07` gives about 700 ms post-send heartbeat delay.
## Retry/Error 07 Frames
`07...` frames are easy to misread.
The ROM can generate a `0x07` retry/error echo when:
- A physical RX error occurs, or
- A checksum mismatch occurs, and
- `FAA5.7` is set, and
- Retry count `FAA6` is below two.
Path:
- `BE29` retry gate.
- `BE4D` stages `F850=0x07`.
- `F851-F854` copy host `RX[1:4]`.
- `BA26` sends it.
Bench implication:
- A visible `07...` frame is not automatically a normal status report or ACK.
- Old `8N1` captures produced many misleading `07...` frames because parity errors exercised this path.
## Table Model
The ROM behaves like a selector-indexed state machine. The CCU likely seeds values, and the RCP updates LCD/lamp/control behavior from those values.
| Table | Range | Role |
| --- | --- | --- |
| Primary value table | `E000-E3FF` | Command 0/4 writes, command 1 reads |
| Secondary value table | `E400-E7FF` | Command 6 writes |
| Current/report table | `E800-EBFF` | Used when RCP builds outbound report frames |
| Dirty/flag table | `EC00-EFFF` | Per-selector flags, bit7 for primary writes, bit6 for secondary writes |
| EEPROM/shadow | `F400-F4FF` | Optional mapped persistence/config surface |
Important details:
- Selector zero is special in command 0 and command 4.
- Command 0 writes both `E000` and `E800`.
- Command 4 writes `E800` only for selector zero in the current ROM evidence.
- `BAF2` reads `E800 + 2*selector` when building autonomous RCP reports.
- `BE70/F970` is a selector-processing queue.
- `3E54/F870` is a separate serial-visible report queue.
Do not mix up:
- `F970`: "process this selector internally".
- `F870`: "send this selector/report over serial".
## State And Queues
Important RAM/state bytes:
| Address | Working name | Meaning |
| --- | --- | --- |
| `FAA2.7` | RX command in progress | Set on initial parse, cleared on exits |
| `FAA2.3` | queued report continuation needed | Set after autonomous report send |
| `FAA3.7` | pending resend mask | Set after queued report send |
| `FAA4.7` | RX physical error latch | Set by SCI1 ERI |
| `FAA5.7` | RX session gate | Set while `F9C5` is alive after complete RX |
| `FAA6` | retry counter | Limits retry/error echoes |
| `F9C1` | inter-byte timeout | Reloaded on RXI |
| `F9C3` | RX byte count | Counts up to six |
| `F9C4` | heartbeat/report cadence gate | Controls idle heartbeat enqueue |
| `F9C5` | RX/session timeout | Loaded with `0x14` after full RX frame |
| `F9B0/F9B5` | serial report queue cursors | Drive `F870`/`BAF2` |
| `F9B4/F9B9` | selector-processing queue cursors | Drive `F970`/`2806` |
Session expiry:
- A complete six-byte RX frame loads `F9C5=0x14`.
- FRT2 decrements `F9C5`.
- When `F9C5` reaches zero, `loc_3FEF` can clear queues/session state.
- If `FAA5.7` was set, expiry calls `loc_400C`.
- `loc_400C` clears connection/session RAM and refreshes inactive display state.
This explains why random traffic tends to settle back to `CONNECT:NOT ACT`.
## CONNECT State
`CONNECT` strings are built through the LCD driver, not received as literal serial text.
Known LCD path:
```text
FAF0-FAFF line buffer -> 3ECC -> 3F28 -> 3F40 -> F200/F201 LCD ports
```
ROM/emulator findings:
- Boot/no-active-session display can show `CONNECT:NOT ACT`.
- Direct emulator entry at `loc_2CB9` with `E000[0]=0x8080` and `F730=0` reaches `CONNECT: OK`.
- Queued selector-zero path reaches OK when:
- `F970[0]=0`
- `F9B9=0`
- `F9B4=1`
- `E000[0]=0x8080`
- `F730=0`
- Selector zero dispatches through the `28A6` jump table into the CONNECT handler window.
Bench findings:
- Correct `8E1` serial format made the CONNECT path work on real hardware.
- Real hardware can recover from `CONNECT:NOT ACT` to `CONNECT: OK` without a power cycle.
- Successful active-looking state included:
- `CONNECT: OK`
- CAM POWER lamp illuminated
- numeric readouts illuminated as `----`
- Matrix tests show that cadence matters:
- `40 -> 80 -> C0` with 10 ms, 50 ms, or 150 ms inter-frame gaps stayed at `CONNECT:NOT ACT` after a fresh power-cycle test.
- The same order with 700 ms and 1.5 s inter-frame gaps produced `CONNECT: OK` before falling back to `CONNECT:NOT ACT`.
- At 700 ms gaps, no single frame worked by itself.
- At 700 ms gaps, every tested two-frame pair worked: `40 -> 80`, `80 -> C0`, and `40 -> C0`.
- A repeated identical pair also worked: `80 -> 80` at about 700 ms produced eight `02 00 02 00 00 5A` OK-path responses, then heartbeat traffic resumed.
- The no-power-cycle recovery test from an already visible `CONNECT:NOT ACT` state produced repeated `02 00 02 00 00 5A` OK-path responses, then returned to heartbeat traffic.
Current interpretation:
- `CONNECT:NOT ACT` is a normal no-active-session/cleared-state display, not a terminal latch.
- `CONNECT: OK` is table/state driven, probably selector-zero active/connect state.
- `0x8080` at selector zero is the strongest known active/connect value.
- The panel likely expects the CCU to keep seeding or refreshing state after entering OK.
- The working fake-CCU sequence is probably not "three frames as fast as possible"; it appears to need CCU-like cadence or a live session window, roughly on the heartbeat/report timescale.
- A single selector-zero continuation-shaped frame is insufficient in the current tests; two selector-zero writes at the working cadence are enough. They do not need to carry different values, because `80 -> 80` also worked.
## Candidate CCU Seed Values
These are syntactically valid host frames produced from ROM table mining. Use them as candidate fake-CCU state seeds, not as final protocol truth.
| Selector | Candidate value | Frame | Why it matters |
| --- | --- | --- | --- |
| `0x000` | `0x8080` | `00 00 00 80 80 5A` | strongest CONNECT OK seed |
| `0x003` | `0x8000` | `00 00 03 80 00 D9` | ROM default enabled bit candidate |
| `0x040` | `0xFFFF` | `00 00 40 FF FF 1A` | ROM default all-ones/status block candidate |
| `0x040` | `0x4030` | `00 00 40 40 30 6A` | bench-touched 0x40 family value |
| `0x0F6` | `0x2000` | `00 01 76 20 00 0D` | `loc_48FA` tests `E1EC.13` and can enqueue report `0x00F6` |
Readbacks:
```text
01 00 00 00 00 5B ; selector 0x000
01 00 03 00 00 58 ; selector 0x003
01 00 40 00 00 1B ; selector 0x040
01 01 76 00 00 2C ; selector 0x0F6
```
## Observed Button/Panel Reports
Before the protocol format was corrected, the RCP appeared to emit only a few report families by itself:
```text
00 00 00 00 80 DA ; heartbeat
00 00 07 80 00 DD ; CAM POWER candidate
00 00 15 80 00 CF ; CALL on candidate
00 00 15 00 00 4F ; CALL off candidate
```
Current interpretation:
- The RCP can report some panel events.
- Many other controls probably need CCU-provided state before they become reportable or meaningful.
- The CCU likely streams display/lamp/readout state to the RCP, while the RCP reports operator changes back.
## EEPROM And Board Config
The P9 bus is not the external PT2 serial link. It is a bit-banged EEPROM/config path:
- H8 pin 62, `P91`, reaches X24164 pin 6 `SCL`.
- H8 pin 68, `P97`, reaches shared X24164 pin 5 `SDA`.
ROM findings:
- `loc_40BB` checks `P7DR.7` and `F402 == H'6B6F` before deciding whether to default EEPROM/shadow tables.
- `loc_4103` writes ROM default words through `BFE0`.
- `loc_41D2` reads sixteen 8-byte records into `F7B0-F82F`.
- Command 4 can persist mapped serial table writes when `F76E.7` is set.
Current interpretation:
- EEPROM stores panel/config/default state.
- It can affect startup and option behavior.
- After the `8E1` discovery, EEPROM is less likely to be the fundamental reason CONNECT failed, but it can still influence which selectors/features are active.
## Known Useful Bench Commands
Minimal CONNECT sequence runner:
```powershell
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --parity E --prompt-screen
```
Recover from `CONNECT:NOT ACT` without power cycling:
```powershell
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --no-power-cycle --parity E --prompt-before-send --prompt-screen --post-sequence-read 10 --log captures\connect-notact-to-ok.txt
```
Run the reproducibility/minimization matrix:
```powershell
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite minimal --parity E --prompt-observation --result-json captures\connect-ok-minimal-result.json
```
Test timing/cadence:
```powershell
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite gap --parity E --prompt-observation --result-json captures\connect-ok-gap-result.json
```
Test whether OK is held:
```powershell
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite hold --parity E --prompt-observation --result-json captures\connect-ok-hold-result.json
```
Current matrix result summary:
```text
minimal suite at 150 ms gaps: all cases stayed CONNECT NOT ACT
gap suite at 10/50/150 ms: stayed CONNECT NOT ACT
gap suite at 700 ms and 1.5 s: CONNECT OK, then CONNECT NOT ACT
minimal suite at 700 ms gaps: singles stayed CONNECT NOT ACT; all pairs reached CONNECT OK then CONNECT NOT ACT
repeated 80 -> 80 at about 700 ms: CONNECT OK responses, then CONNECT NOT ACT
hold suite at 150 ms gaps: stayed CONNECT NOT ACT
no-power-cycle NOT ACT recovery: CONNECT OK responses observed, then heartbeat resumes
```
Read table state:
```powershell
.\.venv\Scripts\python.exe scripts\serial_table_dump.py --port COM5 --relay-port COM6 --start 0x000 --count 0x200 --parity E --log captures\table-read-8e1.txt
```
## Current Best Model Of Normal CCU/RCP Communication
The protocol looks like a shared selector table with stateful reporting:
1. CCU sends initial state seeds into selector tables, especially selector zero and status/display selectors.
2. RCP updates LCD, lamps, and numeric readouts from selector dispatch handlers.
3. RCP emits heartbeat/report frames from `E800` via the `F870 -> BAF2` report queue.
4. Host/CCU uses continuation commands to consume/ACK/update live reports while `FAA2/FAA3` gates are active.
5. If the CCU stops talking or session state expires, RCP clears volatile session state and returns to `CONNECT:NOT ACT`.
This fits the real panel behavior:
- Idle panel emits heartbeat.
- Correct fake-CCU traffic can wake it to `CONNECT: OK`.
- Without richer CCU state, readouts illuminate but show placeholders like `----`.
## What Is Still Unknown
- The official PT2 names for commands and selectors.
- Which selectors drive every lamp and numeric display.
- Whether the CCU sends a periodic refresh stream after CONNECT OK.
- Exact hold time before OK falls back to NOT ACT, if no refresh traffic follows.
- Whether command 4 CONNECT success depends on an existing continuation latch, timing, or a side effect created by earlier frames.
- How EEPROM option bits change selector behavior.
- Whether all old visible `07...` families can be reproduced under `8E1`.
## Next Best Refinements
1. Finish the CONNECT matrix runs:
- rerun `hold` with 700 ms gaps to measure how long OK remains without refresh traffic.
2. Test whether periodic `80` refreshes hold CONNECT OK, and find the longest safe refresh interval.
3. Dump selector table state before and after CONNECT OK.
4. Seed selectors `0x003`, `0x040`, and `0x0F6` after selector-zero OK and watch lamps/readouts.
5. Mine selector dispatch handlers for known UI text terms: `IRIS`, `GAIN`, `SHUTTER`, `BARS`, `BLACK`, `CALL`, `AUTO`, `DIAG`.
6. Build a fake-CCU streamer that repeatedly writes a small selector set and logs which RCP reports appear.
## Source Files And Reports
Generated evidence:
- `build/rom_decompiled.asm`
- `build/rom_rx_branch_trace.txt`
- `build/rom_ccu_seed_hints.txt`
- `build/rom_eeprom_layout.txt`
- `build/rom_table_xrefs.txt`
- `build/connect-state-search-ok.json`
Useful tools:
- `h8536_protocol_trace.py`
- `h8536_protocol_capture.py`
- `h8536_rx_branch_trace.py`
- `h8536_ccu_seed_hints.py`
- `h8536_emulator_rx_probe.py`
- `h8536_emulator_state_search.py`
- `scripts/bench_connect_lcd_sequence.py`
- `scripts/connect_ok_matrix.py`
- `scripts/serial_table_dump.py`
- `scripts/serial_scenario.py`

View File

@@ -12,6 +12,7 @@ from typing import Iterable, TextIO
CHECKSUM_SEED = 0x5A CHECKSUM_SEED = 0x5A
FRAME_LENGTH = 6 FRAME_LENGTH = 6
SERIAL_PARITY_CHOICES = ("N", "E", "O")
CONNECT_LCD_SEQUENCE = ( CONNECT_LCD_SEQUENCE = (
bytes.fromhex("04000040001E"), bytes.fromhex("04000040001E"),
@@ -202,6 +203,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
) )
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP") 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("--baud", type=int, default=38400, help="RCP serial baud rate")
add_serial_format_args(parser)
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port") 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("--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("--no-power-cycle", action="store_true", help="do not send relay off/on before the test")
@@ -233,7 +235,7 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
log_path = args.log or default_log_path() log_path = args.log or default_log_path()
if args.dry_run: if args.dry_run:
print(f"device={args.port} {args.baud} 8N1", file=stdout) print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", 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) 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): for index, frame in enumerate(frames, start=1):
@@ -248,12 +250,15 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
detector = FrameDetector(sync_mode=args.sync) detector = FrameDetector(sync_mode=args.sync)
try: try:
logger.emit("CONNECT LCD bench sequence") 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"device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud}"
)
logger.emit(f"log={log_path}") logger.emit(f"log={log_path}")
for index, frame in enumerate(frames, start=1): for index, frame in enumerate(frames, start=1):
logger.emit(f"plan frame[{index}]={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}") 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: with open_device_serial(serial, args) as device:
relay = None relay = None
try: try:
if not args.no_power_cycle: if not args.no_power_cycle:
@@ -319,6 +324,23 @@ def _import_serial():
return serial return serial
def add_serial_format_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--parity",
choices=SERIAL_PARITY_CHOICES,
default="E",
help="serial parity for the RCP link; ROM SCI1 setup uses even parity",
)
def serial_format_label(args: argparse.Namespace) -> str:
return f"8{args.parity}1"
def open_device_serial(serial, args: argparse.Namespace):
return serial.Serial(args.port, args.baud, bytesize=8, parity=args.parity, stopbits=1, timeout=0.05)
def _send_frame(device, frame: bytes, logger: BenchLogger, label: str) -> None: def _send_frame(device, frame: bytes, logger: BenchLogger, label: str) -> None:
device.write(frame) device.write(frame)
device.flush() device.flush()

387
h8536/connect_ok_matrix.py Normal file
View File

@@ -0,0 +1,387 @@
from __future__ import annotations
import argparse
import json
import sys
import time
from dataclasses import dataclass
from datetime import datetime
from itertools import permutations
from pathlib import Path
from typing import Any, TextIO
from .bench_connect_lcd import (
BenchLogger,
COMMAND7_REPEAT_FRAME,
FrameDetector,
add_serial_format_args,
_import_serial,
open_device_serial,
_read_for,
_relay_command,
_relay_settle,
_send_frame,
_wait_for_ready,
format_frame,
frame_checksum_ok,
serial_format_label,
)
FRAME_40_DXC = bytes.fromhex("04000040001E")
FRAME_80_OK = bytes.fromhex("0400008000DE")
FRAME_C0_PRIORITY = bytes.fromhex("040000C0009E")
NAMED_FRAMES = {
"40": FRAME_40_DXC,
"80": FRAME_80_OK,
"c0": FRAME_C0_PRIORITY,
}
@dataclass(frozen=True)
class MatrixCase:
name: str
frames: tuple[bytes, ...]
gap: float = 0.150
repeat: int = 1
post_read: float = 3.0
note: str = ""
def default_log_path(suite: str) -> Path:
safe_suite = "".join(char if char.isalnum() or char in "-_" else "-" for char in suite)
return Path("captures") / f"connect-ok-matrix-{safe_suite}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run a reproducibility matrix for the CONNECT: OK bench behavior."
)
parser.add_argument("--suite", choices=("baseline", "minimal", "single", "pair", "order", "gap", "repeat", "hold", "all"), default="minimal")
parser.add_argument("--case", action="append", help="run only matching case names; repeatable")
parser.add_argument("--limit", type=int, help="run only the first N selected cases")
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")
add_serial_format_args(parser)
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 power-cycle between cases")
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 DUT power off between cases")
parser.add_argument("--relay-settle", type=float, default=2.0, help="seconds to wait after opening the relay port")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="seconds to wait for heartbeat after power-on")
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 a case if readiness heartbeats are not observed")
parser.add_argument("--pre-case-drain", type=float, default=0.250, help="seconds to drain/log RX before sending each case")
parser.add_argument("--post-case-read", type=float, default=3.0, help="default seconds to listen after each case")
parser.add_argument("--default-gap", type=float, default=0.150, help="default seconds to listen between frames")
parser.add_argument("--gaps", default="0.010,0.050,0.150,0.700,1.500", help="comma-separated gaps for --suite gap")
parser.add_argument("--repeat-counts", default="1,2,4", help="comma-separated repeat counts for --suite repeat")
parser.add_argument("--hold-seconds", default="3,8,20", help="comma-separated post-read times for --suite hold")
parser.add_argument("--command7-after", action="store_true", help="send command-7 previous-frame probe after each case")
parser.add_argument("--sync", choices=("checksum", "fixed"), default="checksum", help="RX frame sync strategy")
parser.add_argument("--prompt-observation", action="store_true", help="prompt for observed LCD/lamp state after each case")
parser.add_argument("--pause-between-cases", action="store_true", help="wait for Enter before starting the next case")
parser.add_argument("--log", type=Path, help="capture log path")
parser.add_argument("--result-json", type=Path, help="write machine-readable case summary")
parser.add_argument("--dry-run", action="store_true", help="print selected cases 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)
cases = select_cases(args)
log_path = args.log or default_log_path(args.suite)
if args.dry_run:
_print_dry_run(args, cases, log_path, stdout)
return 0
serial = _import_serial()
logger = BenchLogger(log_path, stdout=stdout)
results: list[dict[str, Any]] = []
try:
logger.emit("CONNECT: OK bench matrix")
logger.emit(
f"suite={args.suite} cases={len(cases)} device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud} sync={args.sync}"
)
logger.emit(f"log={log_path}")
with open_device_serial(serial, args) 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)
for index, case in enumerate(cases, start=1):
if args.pause_between_cases and index > 1:
input(f"Press Enter to start case {index}/{len(cases)}: {case.name}")
result = _run_case(args, device, relay, logger, case, index, len(cases))
results.append(result)
if args.require_ready and not result["ready"]:
logger.event("ABORT readiness was required")
break
finally:
if relay is not None:
relay.close()
_emit_matrix_summary(logger, results)
if args.result_json:
_write_result_json(args.result_json, log_path, args, results)
return 0
finally:
logger.close()
def select_cases(args: argparse.Namespace) -> list[MatrixCase]:
cases = build_cases(
args.suite,
default_gap=args.default_gap,
default_post_read=args.post_case_read,
gaps=_parse_float_csv(args.gaps),
repeat_counts=_parse_int_csv(args.repeat_counts),
hold_seconds=_parse_float_csv(args.hold_seconds),
)
if args.case:
filters = [item.lower() for item in args.case]
cases = [case for case in cases if any(fragment in case.name.lower() for fragment in filters)]
if args.limit is not None:
cases = cases[: max(0, args.limit)]
if not cases:
raise SystemExit("no matrix cases selected")
return cases
def build_cases(
suite: str,
*,
default_gap: float = 0.150,
default_post_read: float = 3.0,
gaps: list[float] | None = None,
repeat_counts: list[int] | None = None,
hold_seconds: list[float] | None = None,
) -> list[MatrixCase]:
gaps = gaps or [0.010, 0.050, 0.150, 0.700, 1.500]
repeat_counts = repeat_counts or [1, 2, 4]
hold_seconds = hold_seconds or [3.0, 8.0, 20.0]
def case(name: str, keys: tuple[str, ...], *, gap: float = default_gap, repeat: int = 1, post_read: float = default_post_read, note: str = "") -> MatrixCase:
return MatrixCase(name=name, frames=tuple(NAMED_FRAMES[key] for key in keys), gap=gap, repeat=repeat, post_read=post_read, note=note)
baseline = [
case("baseline-40-80-c0", ("40", "80", "c0"), note="known emulator-derived order"),
]
single = [
case("single-40", ("40",), note="tests whether the DXC/low path alone wakes the panel"),
case("single-80", ("80",), note="tests whether the OK/high path alone wakes the panel"),
case("single-c0", ("c0",), note="tests whether the priority-combined path alone wakes the panel"),
]
pair = [
case("pair-40-80", ("40", "80"), note="tests primer-then-OK without the C0 branch"),
case("pair-80-c0", ("80", "c0"), note="tests OK followed by priority branch"),
case("pair-40-c0", ("40", "c0"), note="tests DXC/low followed by priority branch"),
case("pair-80-40", ("80", "40"), note="tests reverse OK/DXC ordering"),
case("pair-c0-80", ("c0", "80"), note="tests C0 as primer for OK"),
case("pair-c0-40", ("c0", "40"), note="tests C0 as primer for DXC"),
]
order = [
case("order-" + "-".join(keys), keys, note="three-frame order/permutation test")
for keys in permutations(("40", "80", "c0"), 3)
]
gap_cases = [
case(f"gap-{_gap_name(gap)}-40-80-c0", ("40", "80", "c0"), gap=gap, note="same order with varied inter-frame delay")
for gap in gaps
]
repeat_cases = [
case(f"repeat-{count}x-40-80-c0", ("40", "80", "c0"), repeat=count, note="tests whether repeated cadence is required")
for count in repeat_counts
]
hold_cases = [
case(f"hold-{_gap_name(seconds)}s-40-80-c0", ("40", "80", "c0"), post_read=seconds, note="tests whether CONNECT OK persists without continued traffic")
for seconds in hold_seconds
]
suites = {
"baseline": baseline,
"minimal": baseline + [single[1], single[0], single[2]] + pair[:3],
"single": single,
"pair": pair,
"order": order,
"gap": gap_cases,
"repeat": repeat_cases,
"hold": hold_cases,
"all": baseline + single + pair + order + gap_cases + repeat_cases + hold_cases,
}
return _dedupe_cases(suites[suite])
def _run_case(
args: argparse.Namespace,
device: Any,
relay: Any | None,
logger: BenchLogger,
case: MatrixCase,
index: int,
total: int,
) -> dict[str, Any]:
detector = FrameDetector(sync_mode=args.sync)
logger.emit()
logger.emit(f"CASE {index}/{total} {case.name}")
logger.emit(f"note={case.note or '(none)'}")
logger.emit(f"gap={case.gap:.3f}s repeat={case.repeat} post_read={case.post_read:.3f}s")
for frame_index, frame in enumerate(case.frames, start=1):
logger.emit(f"case_frame[{frame_index}]={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}")
if args.no_power_cycle:
device.reset_input_buffer()
logger.event("POWER_CYCLE skipped")
else:
if relay is None:
raise SystemExit("relay was not opened")
_relay_command(relay, args.power_off_command, logger)
time.sleep(max(0.0, args.off_seconds))
device.reset_input_buffer()
_relay_command(relay, args.power_on_command, logger)
ready = _wait_for_ready(device, detector, logger, args.ready_timeout, args.ready_heartbeats)
if args.require_ready and not ready:
observation = _prompt_observation(args, logger, case)
return _case_result(case, detector, ready=ready, observation=observation)
if args.pre_case_drain > 0:
logger.event(f"DRAIN before case {args.pre_case_drain:.3f}s")
_read_for(device, detector, logger, args.pre_case_drain)
for repeat_index in range(max(1, case.repeat)):
if case.repeat > 1:
logger.event(f"BEGIN case_repeat {repeat_index + 1}/{case.repeat}")
for frame_index, frame in enumerate(case.frames, start=1):
_send_frame(device, frame, logger, f"{case.name}.r{repeat_index + 1}.f{frame_index}")
_read_for(device, detector, logger, case.gap)
if args.command7_after:
_send_frame(device, COMMAND7_REPEAT_FRAME, logger, f"{case.name}.command7_after")
_read_for(device, detector, logger, case.gap)
if case.post_read > 0:
logger.event(f"POST_READ {case.post_read:.3f}s")
_read_for(device, detector, logger, case.post_read)
observation = _prompt_observation(args, logger, case)
result = _case_result(case, detector, ready=ready, observation=observation)
logger.event(
f"CASE_RESULT {case.name} ready={int(ready)} rx_frames={result['rx_frames']} "
f"labels={json.dumps(result['labels'], sort_keys=True)}"
)
return result
def _prompt_observation(args: argparse.Namespace, logger: BenchLogger, case: MatrixCase) -> str:
if not args.prompt_observation:
return ""
prompt = f"{case.name}: LCD/lamps/readouts observation, or Enter to skip: "
observation = input(prompt).strip()
logger.event(f"OBSERVATION {case.name}: {observation or '(no note)'}")
return observation
def _case_result(case: MatrixCase, detector: FrameDetector, *, ready: bool, observation: str) -> dict[str, Any]:
return {
"name": case.name,
"note": case.note,
"frames": [format_frame(frame) for frame in case.frames],
"gap": case.gap,
"repeat": case.repeat,
"post_read": case.post_read,
"ready": ready,
"rx_frames": len(detector.frames),
"labels": dict(detector.labels),
"resync_events": detector.resync_events,
"dropped_bytes": detector.dropped_bytes,
"trailing_unframed_bytes": len(detector.buffer),
"observation": observation,
}
def _emit_matrix_summary(logger: BenchLogger, results: list[dict[str, Any]]) -> None:
logger.emit()
logger.emit("Matrix Summary")
logger.emit(f"cases={len(results)}")
for result in results:
labels = ", ".join(f"{key}={value}" for key, value in sorted(result["labels"].items())) or "no_rx_frames"
note = result["observation"] or "(no observation)"
logger.emit(
f"{result['name']}: ready={int(result['ready'])} rx_frames={result['rx_frames']} "
f"{labels} observation={note}"
)
def _write_result_json(path: Path, log_path: Path, args: argparse.Namespace, results: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"suite": args.suite,
"log": str(log_path),
"serial_format": f"{args.baud} {serial_format_label(args)}",
"cases": results,
}
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def _print_dry_run(args: argparse.Namespace, cases: list[MatrixCase], log_path: Path, stdout: TextIO) -> None:
print(f"suite={args.suite}", file=stdout)
print(f"cases={len(cases)}", file=stdout)
print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle_between_cases={int(not args.no_power_cycle)}", file=stdout)
print(f"log={log_path}", file=stdout)
for index, case in enumerate(cases, start=1):
print(
f"case[{index}]={case.name} gap={case.gap:.3f}s repeat={case.repeat} "
f"post_read={case.post_read:.3f}s",
file=stdout,
)
for frame in case.frames:
print(f" frame={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}", file=stdout)
if case.note:
print(f" note={case.note}", file=stdout)
if args.command7_after:
print(f"command7_after={format_frame(COMMAND7_REPEAT_FRAME)}", file=stdout)
def _dedupe_cases(cases: list[MatrixCase]) -> list[MatrixCase]:
seen: set[str] = set()
deduped: list[MatrixCase] = []
for case in cases:
if case.name in seen:
continue
seen.add(case.name)
deduped.append(case)
return deduped
def _parse_float_csv(text: str) -> list[float]:
return [float(part.strip()) for part in text.split(",") if part.strip()]
def _parse_int_csv(text: str) -> list[int]:
return [int(part.strip(), 0) for part in text.split(",") if part.strip()]
def _gap_name(value: float) -> str:
if value >= 1:
return f"{value:.1f}".rstrip("0").rstrip(".").replace(".", "p")
milliseconds = int(round(value * 1000))
return f"{milliseconds}ms"
__all__ = [
"FRAME_40_DXC",
"FRAME_80_OK",
"FRAME_C0_PRIORITY",
"MatrixCase",
"build_arg_parser",
"build_cases",
"main",
"select_cases",
]

View File

@@ -0,0 +1,529 @@
from __future__ import annotations
import argparse
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable
from ..formatting import h16, parse_int
from .cli import load_rom
from .constants import HEARTBEAT_FRAME
from .memory import MemoryAccess
from .runner import H8536Emulator
from .rx_probe import (
UartTiming,
_inject_frame_uart_timed,
_interrupt_mask,
_rx_byte_consumed,
_rx_ready,
_run_until,
_sci1_priority,
format_frame,
frame_checksum_ok,
parse_frame,
)
DEFAULT_EEPROM_LOAD = Path("build") / "bench-sync-after-dip-reset.bin"
DEFAULT_FRAMES = (
bytes.fromhex("0000008000DA"),
bytes.fromhex("01000000005B"),
bytes.fromhex("07000000005D"),
)
BENCH_SIGNATURES = {
bytes.fromhex("04000080805E"): "bench_cmd0_echo_80",
bytes.fromhex("02000200005A"): "bench_connect_ok",
bytes.fromhex("07804040A07D"): "bench_40a0_retry_echo_candidate_40",
bytes.fromhex("07808040A0BD"): "bench_40a0_retry_echo_candidate_80",
bytes.fromhex("0780C040A0FD"): "bench_40a0_retry_echo_candidate_c0",
}
WATCH_PCS = {
0x3FD3: "report_scheduler_gate",
0x3FEF: "report_scheduler_return",
0x4007: "resend_or_session_gate",
0xBBF0: "rx_checksum_compare",
0xBE29: "rx_checksum_retry_error",
0xBE4D: "retry_echo_stage",
0xBC0F: "faa2_split",
0xBC15: "initial_latch_faa2_bit7",
0xBC33: "initial_dispatch_fallthrough",
0xBC3A: "continuation_dispatch",
0xBC5C: "continuation_clear_queued_ack",
0xBC67: "continuation_reenter_initial",
0xBC69: "cmd0_set_value",
0xBCD7: "cmd1_read_value",
0xBE05: "cmd7_copy_or_retry",
0xBE70: "selector_queue_be70",
0xBA26: "tx_builder_ba26",
0xBA72: "tx_first_byte",
0xBA84: "txi_entry_ba84",
0xBA8A: "txi_faa2_bit3_test",
0xBA90: "txi_rx_index_test",
0xBA96: "txi_overlap_collapse",
0xBA9A: "txi_clear_pending_mask",
0xBAA2: "txi_disable_tie",
0xBAB5: "txi_next_byte",
0xBAF2: "report_dequeue_baf2",
0xBB00: "report_session_latch",
0xBB08: "report_selector_read",
0xBB1C: "report_selector_encode",
0xBB20: "report_selector_encode_hi",
0xBB2B: "report_payload_read",
0xBB35: "report_value_stage",
0xBB43: "report_send",
0xBE9E: "session_resend_gate",
0xBED5: "session_resend_send",
}
WATCH_GROUPS = {
"cmd0_reached_BC69": (0xBC69,),
"cmd1_reached_BCD7": (0xBCD7,),
"retry_echo": (0xBE29, 0xBE4D),
"cmd7_replay": (0xBE05,),
"autonomous_report": (0xBAF2, 0xBB00, 0xBB35, 0xBB43),
"tx_rx_overlap_collapse": (0xBA96, 0xBA9A, 0xBAA2),
}
WATCH_WRITE_RANGES = (
(0xF860, 0xF865, "rx_validation_F860_F865"),
(0xF868, 0xF86D, "rx_capture_F868_F86D"),
(0xF850, 0xF85D, "tx_staging_F850_F85D"),
(0xF870, 0xF96F, "report_queue_F870_F96F"),
(0xF970, 0xF9AF, "selector_queue_F970_F9AF"),
(0xF9B0, 0xF9B0, "report_head_F9B0"),
(0xF9B4, 0xF9B4, "selector_tail_F9B4"),
(0xF9B5, 0xF9B5, "report_tail_F9B5"),
(0xF9C0, 0xF9C0, "tx_gate_F9C0"),
(0xF9C1, 0xF9C1, "rx_interbyte_timeout_F9C1"),
(0xF9C3, 0xF9C3, "rx_index_F9C3"),
(0xF9C5, 0xF9C5, "rx_session_timeout_F9C5"),
(0xFAA2, 0xFAA6, "serial_latches_FAA2_FAA6"),
(0xE000, 0xE001, "primary_table_E000"),
(0xE800, 0xE801, "current_table_E800"),
(0xEC00, 0xEC01, "flag_table_EC00"),
)
STATE_BYTES = {
0xF9B0: "F9B0_report_head",
0xF9B4: "F9B4_selector_tail",
0xF9B5: "F9B5_report_tail",
0xF9C0: "F9C0_tx_gate",
0xF9C1: "F9C1_rx_interbyte_timeout",
0xF9C3: "F9C3_rx_index",
0xF9C5: "F9C5_rx_session_timeout",
0xFAA2: "FAA2_session_flags",
0xFAA3: "FAA3_pending_mask",
0xFAA4: "FAA4_rx_error_latch",
0xFAA5: "FAA5_retry_gate_flags",
0xFAA6: "FAA6_retry_counter",
}
STATE_BUFFERS = {
"F850_F85D_tx_staging": (0xF850, 14),
"F860_F865_rx_validation": (0xF860, 6),
"F868_F86D_rx_capture": (0xF868, 6),
"F870_F87F_report_queue_head": (0xF870, 16),
"F970_F97F_selector_queue_head": (0xF970, 16),
"E000_E001_primary_0000": (0xE000, 2),
"E800_E801_current_0000": (0xE800, 2),
"EC00_EC01_flags_0000": (0xEC00, 2),
"E880_E881_current_0040": (0xE880, 2),
"E900_E901_current_0080": (0xE900, 2),
"E980_E981_current_00C0": (0xE980, 2),
}
@dataclass
class DivergenceContext:
pc_hits: Counter[int] = field(default_factory=Counter)
first_pcs: list[int] = field(default_factory=list)
unsupported: str | None = None
def record_pc(self, pc: int) -> None:
if pc not in WATCH_PCS:
return
self.pc_hits[pc] += 1
if len(self.first_pcs) < 32:
self.first_pcs.append(pc)
@dataclass(frozen=True)
class RxDivergenceConfig:
boot_steps: int = 250_000
wait_heartbeats: int = 0
wait_heartbeat_steps: int = 500_000
post_frame_steps: int = 80_000
per_byte_steps: int = 5_000
interval_steps: int = 512
frt1_ocia_steps: int | None = None
frt2_ocia_steps: int | None = None
clock_hz: int = 10_000_000
uart_timing: bool = False
uart_baud: int = 38_400
p7_input: int = 0xFF
p9_fast_path: bool = True
p9_fast_input: int = 0xFF
p9_fast_optimistic_wrapper: bool = False
eeprom_seed: str = "blank"
eeprom_load: Path | None = DEFAULT_EEPROM_LOAD
stop_after_tx_frame: bool = False
@dataclass(frozen=True)
class FrameTrace:
frame: bytes
checksum_ok: bool
rx_injection: str
steps: int
stopped_reason: str
tx_frames: tuple[bytes, ...]
state_changes: tuple[str, ...]
writes: tuple[str, ...]
context: DivergenceContext
def outcome(self) -> str:
flags = classify_hits(self.context.pc_hits)
return " ".join(f"{name}={int(value)}" for name, value in flags.items())
def lines(self, index: int, *, summary_only: bool = False) -> list[str]:
lines = [
(
f"frame[{index}] host={format_frame(self.frame)} checksum_ok={int(self.checksum_ok)} "
f"steps={self.steps} stopped={self.stopped_reason} {self.outcome()}"
)
]
if self.tx_frames:
lines.append(" tx=" + " | ".join(label_frame(frame) for frame in self.tx_frames))
else:
lines.append(" tx=none")
if summary_only:
return lines
lines.append(f" rx_injection={self.rx_injection}")
hit_lines = format_pc_hits(self.context)
if hit_lines:
lines.append(" pcs=" + " ".join(hit_lines))
if self.state_changes:
lines.append(" state=" + "; ".join(self.state_changes))
if self.writes:
lines.append(" writes:")
lines.extend(f" {line}" for line in self.writes)
if self.context.unsupported:
lines.append(f" unsupported={self.context.unsupported}")
return lines
@dataclass(frozen=True)
class RxDivergenceResult:
rom_path: Path
eeprom_load: Path | None
reset_vector: int
boot_summary: str
heartbeat_summary: str | None
traces: tuple[FrameTrace, ...]
all_tx_frames: tuple[bytes, ...]
def lines(self, *, summary_only: bool = False) -> list[str]:
lines = [
f"rom={self.rom_path}",
f"eeprom_loaded={self.eeprom_load if self.eeprom_load else 'none'}",
f"reset_vector={h16(self.reset_vector)}",
self.boot_summary,
bench_signature_line(),
]
if self.heartbeat_summary is not None:
lines.append(self.heartbeat_summary)
for index, trace in enumerate(self.traces):
lines.extend(trace.lines(index, summary_only=summary_only))
lines.append("all_tx=" + _format_frame_list(self.all_tx_frames))
return lines
def run_rx_divergence(
frames: Iterable[bytes],
*,
rom_path: Path | None = None,
config: RxDivergenceConfig = RxDivergenceConfig(),
) -> RxDivergenceResult:
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,
clock_hz=config.clock_hz,
p9_fast_path_enabled=config.p9_fast_path,
p9_fast_default_input_byte=config.p9_fast_input,
p9_fast_default_wrapper_success=config.p9_fast_optimistic_wrapper,
p7_input=config.p7_input,
eeprom_seed=config.eeprom_seed,
)
eeprom_load = config.eeprom_load
if eeprom_load is not None and eeprom_load.is_file():
emulator.memory.load_eeprom_image(eeprom_load.read_bytes())
boot_context = DivergenceContext()
boot_steps_used, boot_reason = _run_until(emulator, config.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"sci1_priority={_sci1_priority(emulator)} interrupt_mask={_interrupt_mask(emulator)} "
f"clock_hz={emulator.clock_hz} p7_input={config.p7_input:#04x}"
)
heartbeat_summary = None
if config.wait_heartbeats:
heartbeat_summary = _wait_for_heartbeats(emulator, config.wait_heartbeats, config.wait_heartbeat_steps)
traces = tuple(_trace_frame(emulator, frame, config) for frame in frames)
return RxDivergenceResult(
rom_path=discovered_rom_path,
eeprom_load=eeprom_load if eeprom_load is not None and eeprom_load.is_file() else None,
reset_vector=emulator.reset_vector(),
boot_summary=boot_summary,
heartbeat_summary=heartbeat_summary,
traces=traces,
all_tx_frames=tuple(emulator.sci1.tx_frames),
)
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Focused H8/536 RX divergence trace around command dispatch and serial state.")
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")
parser.add_argument("--default-frames", action="store_true", help="append the known bench-divergence frame trio")
parser.add_argument("--boot-steps", type=int, default=RxDivergenceConfig.boot_steps)
parser.add_argument("--eeprom-load", type=Path, default=DEFAULT_EEPROM_LOAD, help="logical EEPROM image loaded before boot")
parser.add_argument("--no-eeprom-load", action="store_true", help="boot without loading the default bench EEPROM image")
parser.add_argument("--p7-input", type=parse_int, default=RxDivergenceConfig.p7_input)
parser.add_argument("--wait-heartbeats", type=int, default=RxDivergenceConfig.wait_heartbeats)
parser.add_argument("--wait-heartbeat-steps", type=int, default=RxDivergenceConfig.wait_heartbeat_steps)
parser.add_argument("--uart-timing", action="store_true", help="inject bytes at UART character timing instead of waiting for RDRF clear")
parser.add_argument("--uart-baud", type=parse_int, default=RxDivergenceConfig.uart_baud)
parser.add_argument("--post-frame-steps", type=int, default=RxDivergenceConfig.post_frame_steps)
parser.add_argument("--per-byte-steps", type=int, default=RxDivergenceConfig.per_byte_steps)
parser.add_argument("--clock-hz", type=parse_int, default=RxDivergenceConfig.clock_hz)
parser.add_argument("--interval-steps", type=int, default=RxDivergenceConfig.interval_steps)
parser.add_argument("--frt1-ocia-steps", type=int, default=RxDivergenceConfig.frt1_ocia_steps)
parser.add_argument("--frt2-ocia-steps", type=int, default=RxDivergenceConfig.frt2_ocia_steps)
parser.add_argument("--no-p9-fast-path", action="store_true")
parser.add_argument("--p9-fast-input", type=parse_int, default=RxDivergenceConfig.p9_fast_input)
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true")
parser.add_argument("--eeprom-seed", choices=("blank", "factory"), default=RxDivergenceConfig.eeprom_seed)
parser.add_argument("--stop-after-tx-frame", action="store_true", help="stop each post-frame run after the first new TX frame")
parser.add_argument("--summary-only", action="store_true", help="omit detailed state/write trace lines")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_arg_parser().parse_args(argv)
frames = list(args.frames)
if args.default_frames:
frames.extend(DEFAULT_FRAMES)
if not frames:
raise SystemExit("pass at least one frame, or use --default-frames")
result = run_rx_divergence(
frames,
rom_path=args.rom,
config=RxDivergenceConfig(
boot_steps=args.boot_steps,
wait_heartbeats=args.wait_heartbeats,
wait_heartbeat_steps=args.wait_heartbeat_steps,
post_frame_steps=args.post_frame_steps,
per_byte_steps=args.per_byte_steps,
interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps,
clock_hz=args.clock_hz,
uart_timing=args.uart_timing,
uart_baud=args.uart_baud,
p7_input=args.p7_input,
p9_fast_path=not args.no_p9_fast_path,
p9_fast_input=args.p9_fast_input,
p9_fast_optimistic_wrapper=args.p9_fast_optimistic_wrapper,
eeprom_seed=args.eeprom_seed,
eeprom_load=None if args.no_eeprom_load else args.eeprom_load,
stop_after_tx_frame=args.stop_after_tx_frame,
),
)
for line in result.lines(summary_only=args.summary_only):
print(line)
return 0
def _trace_frame(emulator: H8536Emulator, frame: bytes, config: RxDivergenceConfig) -> FrameTrace:
state_before = snapshot_state(emulator)
log_start = len(emulator.memory.access_log)
tx_frame_start = len(emulator.sci1.tx_frames)
context = DivergenceContext()
steps_total = 0
if config.uart_timing:
timing = UartTiming(baud=config.uart_baud)
steps_total, stopped_reason = _inject_frame_uart_timed(
emulator,
frame,
timing=timing,
max_steps_per_gap=config.per_byte_steps,
context=context,
)
rx_injection = timing.summary(emulator.clock_hz)
injected_all_bytes = stopped_reason == "frame_injected_uart_timing"
else:
rx_injection = "polite_wait_for_rdrf_clear"
injected_all_bytes = True
stopped_reason = "post_frame_steps"
for offset, value in enumerate(frame):
emulator.inject_sci1_rx_byte(value)
steps, reason = _run_until(emulator, config.per_byte_steps, _rx_byte_consumed, context)
steps_total += steps
if reason != "predicate":
stopped_reason = f"rx_byte_{offset}_{reason}"
injected_all_bytes = False
break
if injected_all_bytes:
target_frame_count = tx_frame_start + 1
def stop_predicate(inner: H8536Emulator) -> bool:
return config.stop_after_tx_frame and len(inner.sci1.tx_frames) >= target_frame_count
steps, reason = _run_until(emulator, config.post_frame_steps, stop_predicate, context)
steps_total += steps
stopped_reason = "tx_frame" if reason == "predicate" and config.stop_after_tx_frame else reason
log_end = len(emulator.memory.access_log)
state_after = snapshot_state(emulator)
return FrameTrace(
frame=frame,
checksum_ok=frame_checksum_ok(frame),
rx_injection=rx_injection,
steps=steps_total,
stopped_reason=stopped_reason,
tx_frames=tuple(emulator.sci1.tx_frames[tx_frame_start:]),
state_changes=tuple(format_state_changes(state_before, state_after)),
writes=tuple(format_interesting_writes(emulator.memory.access_log[log_start:log_end])),
context=context,
)
def _wait_for_heartbeats(emulator: H8536Emulator, target_count: int, max_steps: int) -> str:
start_count = _heartbeat_count(emulator.sci1.tx_frames)
def predicate(inner: H8536Emulator) -> bool:
return _heartbeat_count(inner.sci1.tx_frames) - start_count >= target_count
context = DivergenceContext()
steps, reason = _run_until(emulator, max_steps, predicate, context)
seen = _heartbeat_count(emulator.sci1.tx_frames) - start_count
return f"wait_heartbeats target={target_count} seen={seen} steps={steps} stopped={reason}"
def snapshot_state(emulator: H8536Emulator) -> dict[str, int | bytes]:
state: dict[str, int | bytes] = {}
for address, name in STATE_BYTES.items():
state[name] = emulator.memory.read8(address)
for name, (address, length) in STATE_BUFFERS.items():
state[name] = bytes(emulator.memory.read8(address + offset) for offset in range(length))
return state
def format_state_changes(before: dict[str, int | bytes], after: dict[str, int | bytes]) -> list[str]:
lines = []
for key in sorted(after):
if before.get(key) == after[key]:
continue
lines.append(f"{key}:{_state_value(before.get(key))}->{_state_value(after[key])}")
return lines
def format_interesting_writes(accesses: Iterable[MemoryAccess], *, limit: int = 96) -> list[str]:
lines = []
total = 0
for access in accesses:
if access.kind != "write":
continue
label = write_label(access.address)
if label is None:
continue
total += 1
if len(lines) < limit:
lines.append(f"{h16(access.address)}={access.value:02X} {label}")
if total > limit:
lines.append(f"... {total - limit} more watched writes")
return lines
def write_label(address: int) -> str | None:
for start, end, label in WATCH_WRITE_RANGES:
if start <= address <= end:
return label
return None
def classify_hits(hits: Counter[int]) -> dict[str, bool]:
return {name: any(hits.get(pc, 0) for pc in pcs) for name, pcs in WATCH_GROUPS.items()}
def format_pc_hits(context: DivergenceContext) -> list[str]:
lines = [f"{h16(pc)}:{WATCH_PCS[pc]}={context.pc_hits[pc]}" for pc in sorted(context.pc_hits)]
if context.first_pcs:
first = ",".join(h16(pc) for pc in context.first_pcs[:16])
lines.append(f"first={first}")
return lines
def label_frame(frame: bytes) -> str:
signature = BENCH_SIGNATURES.get(frame)
if signature:
return f"{format_frame(frame)} ({signature})"
if frame == HEARTBEAT_FRAME:
return f"{format_frame(frame)} (heartbeat)"
return format_frame(frame)
def bench_signature_line() -> str:
return "bench_signatures=" + " | ".join(label_frame(frame) for frame in BENCH_SIGNATURES)
def _heartbeat_count(frames: Iterable[bytes]) -> int:
return sum(1 for frame in frames if frame == HEARTBEAT_FRAME)
def _state_value(value: int | bytes | None) -> str:
if isinstance(value, bytes):
return format_frame(value)
if isinstance(value, int):
return f"{value:02X}"
return "none"
def _format_frame_list(frames: Iterable[bytes]) -> str:
items = [label_frame(frame) for frame in frames]
return " | ".join(items) if items else "none"
__all__ = [
"BENCH_SIGNATURES",
"DEFAULT_EEPROM_LOAD",
"DEFAULT_FRAMES",
"DivergenceContext",
"FrameTrace",
"RxDivergenceConfig",
"RxDivergenceResult",
"bench_signature_line",
"build_arg_parser",
"classify_hits",
"format_interesting_writes",
"format_pc_hits",
"format_state_changes",
"label_frame",
"main",
"parse_frame",
"run_rx_divergence",
"write_label",
]

View File

@@ -11,7 +11,9 @@ from typing import TextIO
from .bench_connect_lcd import ( from .bench_connect_lcd import (
BenchLogger, BenchLogger,
FrameDetector, FrameDetector,
add_serial_format_args,
_import_serial, _import_serial,
open_device_serial,
_read_for, _read_for,
_relay_command, _relay_command,
_relay_settle, _relay_settle,
@@ -20,6 +22,7 @@ from .bench_connect_lcd import (
format_frame, format_frame,
frame_checksum_ok, frame_checksum_ok,
parse_frame, parse_frame,
serial_format_label,
) )
@@ -42,6 +45,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
) )
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP") 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("--baud", type=int, default=38400, help="RCP serial baud rate")
add_serial_format_args(parser)
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port") 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("--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("--no-power-cycle", action="store_true", help="do not send relay off/on before the test")
@@ -79,11 +83,14 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
detector = FrameDetector() detector = FrameDetector()
try: try:
logger.emit("Serial ACK probe") logger.emit("Serial ACK probe")
logger.emit(f"device={args.port} {args.baud} 8N1 relay={args.relay_port} {args.relay_baud}") logger.emit(
f"device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud}"
)
logger.emit(f"log={log_path}") logger.emit(f"log={log_path}")
_emit_plan(args, logger) _emit_plan(args, logger)
with serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0.05) as device: with open_device_serial(serial, args) as device:
relay = None relay = None
try: try:
if not args.no_power_cycle: if not args.no_power_cycle:
@@ -169,7 +176,7 @@ def _emit_plan(args: argparse.Namespace, logger: BenchLogger) -> None:
def _print_plan(args: argparse.Namespace, log_path: Path, stdout: TextIO) -> None: def _print_plan(args: argparse.Namespace, log_path: Path, stdout: TextIO) -> None:
print(f"device={args.port} {args.baud} 8N1", file=stdout) print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout) print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout) print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout)
print(f"prime={format_frame(args.prime_frame)} checksum_ok={int(frame_checksum_ok(args.prime_frame))}", file=stdout) print(f"prime={format_frame(args.prime_frame)} checksum_ok={int(frame_checksum_ok(args.prime_frame))}", file=stdout)

View File

@@ -12,7 +12,9 @@ from typing import Any, TextIO
from .bench_connect_lcd import ( from .bench_connect_lcd import (
BenchLogger, BenchLogger,
FrameDetector, FrameDetector,
add_serial_format_args,
_import_serial, _import_serial,
open_device_serial,
_read_for, _read_for,
_relay_command, _relay_command,
_relay_settle, _relay_settle,
@@ -21,6 +23,7 @@ from .bench_connect_lcd import (
format_frame, format_frame,
frame_checksum_ok, frame_checksum_ok,
parse_frame, parse_frame,
serial_format_label,
) )
from .serial_table_dump import build_read_frame, decode_table_read_response from .serial_table_dump import build_read_frame, decode_table_read_response
@@ -56,6 +59,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("scenario", type=Path, help="JSON scenario file") parser.add_argument("scenario", type=Path, help="JSON scenario file")
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP") 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("--baud", type=int, default=38400, help="RCP serial baud rate")
add_serial_format_args(parser)
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port") 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("--relay-baud", type=int, default=115200, help="Pico relay serial baud rate")
parser.add_argument("--no-power-cycle", action="store_true", help="skip power_cycle actions") parser.add_argument("--no-power-cycle", action="store_true", help="skip power_cycle actions")
@@ -84,9 +88,12 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
try: try:
logger.emit("Serial bench scenario") logger.emit("Serial bench scenario")
logger.emit(f"name={scenario.get('name', args.scenario.stem)}") logger.emit(f"name={scenario.get('name', args.scenario.stem)}")
logger.emit(f"device={args.port} {args.baud} 8N1 relay={args.relay_port} {args.relay_baud} sync={args.sync}") logger.emit(
f"device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud} sync={args.sync}"
)
logger.emit(f"log={log_path}") logger.emit(f"log={log_path}")
with serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0.05) as device: with open_device_serial(serial, args) as device:
ctx = ScenarioContext(args=args, logger=logger, detector=detector, device=device) ctx = ScenarioContext(args=args, logger=logger, detector=detector, device=device)
try: try:
for index, step in enumerate(_scenario_steps(scenario), start=1): for index, step in enumerate(_scenario_steps(scenario), start=1):
@@ -438,7 +445,7 @@ def _ack_limit_reached(ctx: ScenarioContext, ack: dict[str, Any]) -> bool:
def _print_dry_run(args: argparse.Namespace, scenario: dict[str, Any], log_path: Path, stdout: TextIO) -> None: def _print_dry_run(args: argparse.Namespace, scenario: dict[str, Any], log_path: Path, stdout: TextIO) -> None:
print(f"scenario={scenario.get('name', args.scenario.stem)}", file=stdout) print(f"scenario={scenario.get('name', args.scenario.stem)}", file=stdout)
print(f"device={args.port} {args.baud} 8N1", file=stdout) print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout) print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"sync={args.sync}", file=stdout) print(f"sync={args.sync}", file=stdout)
print(f"log={log_path}", file=stdout) print(f"log={log_path}", file=stdout)

View File

@@ -11,7 +11,9 @@ from typing import TextIO
from .bench_connect_lcd import ( from .bench_connect_lcd import (
BenchLogger, BenchLogger,
FrameDetector, FrameDetector,
add_serial_format_args,
_import_serial, _import_serial,
open_device_serial,
_read_for, _read_for,
_relay_command, _relay_command,
_relay_settle, _relay_settle,
@@ -19,6 +21,7 @@ from .bench_connect_lcd import (
format_frame, format_frame,
frame_checksum, frame_checksum,
frame_checksum_ok, frame_checksum_ok,
serial_format_label,
) )
@@ -68,6 +71,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
) )
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP") 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("--baud", type=int, default=38400, help="RCP serial baud rate")
add_serial_format_args(parser)
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port") 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("--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 sweep") parser.add_argument("--no-power-cycle", action="store_true", help="do not send relay off/on before the sweep")
@@ -95,7 +99,7 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
log_path = args.log or default_log_path() log_path = args.log or default_log_path()
if args.dry_run: if args.dry_run:
print(f"device={args.port} {args.baud} 8N1", file=stdout) print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout) print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout) print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout)
for selector in selectors: for selector in selectors:
@@ -115,11 +119,14 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
response_rows: list[tuple[int, bytes, tuple[int, int] | None]] = [] response_rows: list[tuple[int, bytes, tuple[int, int] | None]] = []
try: try:
logger.emit("Read-only serial table sweep") logger.emit("Read-only serial table sweep")
logger.emit(f"device={args.port} {args.baud} 8N1 relay={args.relay_port} {args.relay_baud}") logger.emit(
f"device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud}"
)
logger.emit(f"log={log_path}") logger.emit(f"log={log_path}")
logger.emit(f"selectors={len(selectors)} command=01 write_frames=0") logger.emit(f"selectors={len(selectors)} command=01 write_frames=0")
with serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0.05) as device: with open_device_serial(serial, args) as device:
relay = None relay = None
try: try:
if not args.no_power_cycle: if not args.no_power_cycle:

View File

@@ -13,7 +13,9 @@ from typing import Any, Iterable, TextIO
from .bench_connect_lcd import ( from .bench_connect_lcd import (
BenchLogger, BenchLogger,
FrameDetector, FrameDetector,
add_serial_format_args,
_import_serial, _import_serial,
open_device_serial,
_relay_command, _relay_command,
_relay_settle, _relay_settle,
_wait_for_ready, _wait_for_ready,
@@ -22,6 +24,7 @@ from .bench_connect_lcd import (
frame_checksum_ok, frame_checksum_ok,
label_frame, label_frame,
parse_frame, parse_frame,
serial_format_label,
) )
@@ -78,6 +81,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--expected-word", type=_int_arg, help="expected E000[0] readback word; default follows --preset") parser.add_argument("--expected-word", type=_int_arg, help="expected E000[0] readback word; default follows --preset")
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP") 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("--baud", type=int, default=38400, help="RCP serial baud rate")
add_serial_format_args(parser)
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port") 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("--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("--no-power-cycle", action="store_true", help="do not send relay off/on before the test")
@@ -143,11 +147,14 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
detector = FrameDetector(sync_mode=args.sync) detector = FrameDetector(sync_mode=args.sync)
try: try:
logger.emit("PT2 state-map proof runner") logger.emit("PT2 state-map proof runner")
logger.emit(f"device={args.port} {args.baud} 8N1 relay={args.relay_port} {args.relay_baud} sync={args.sync}") logger.emit(
f"device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud} sync={args.sync}"
)
logger.emit(f"log={log_path}") logger.emit(f"log={log_path}")
_emit_plan(logger, args, force_frame, expected_word, preset_note) _emit_plan(logger, args, force_frame, expected_word, preset_note)
with serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0.05) as device: with open_device_serial(serial, args) as device:
ctx = StateMapRunContext(args=args, logger=logger, detector=detector, device=device) ctx = StateMapRunContext(args=args, logger=logger, detector=detector, device=device)
try: try:
_prepare_device(ctx) _prepare_device(ctx)
@@ -639,7 +646,7 @@ def _print_dry_run(
stdout: TextIO, stdout: TextIO,
) -> None: ) -> None:
print("PT2 state-map proof runner", file=stdout) print("PT2 state-map proof runner", file=stdout)
print(f"device={args.port} {args.baud} 8N1", file=stdout) print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout) print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout) print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout)
print(f"preset={args.preset} note={preset_note}", file=stdout) print(f"preset={args.preset} note={preset_note}", file=stdout)

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
import io
import unittest
from h8536.connect_ok_matrix import build_cases, main
class ConnectOkMatrixTest(unittest.TestCase):
def test_minimal_suite_starts_with_known_sequence_and_single_ok(self):
cases = build_cases("minimal")
self.assertEqual(cases[0].name, "baseline-40-80-c0")
self.assertEqual(cases[1].name, "single-80")
self.assertIn("pair-40-80", [case.name for case in cases])
def test_gap_suite_uses_requested_inter_frame_gaps(self):
cases = build_cases("gap", gaps=[0.01, 0.7])
self.assertEqual([case.name for case in cases], ["gap-10ms-40-80-c0", "gap-700ms-40-80-c0"])
self.assertEqual([case.gap for case in cases], [0.01, 0.7])
def test_dry_run_defaults_to_even_parity_and_lists_frames(self):
stdout = io.StringIO()
exit_code = main(["--dry-run", "--suite", "minimal", "--limit", "2"], stdout=stdout)
self.assertEqual(exit_code, 0)
output = stdout.getvalue()
self.assertIn("device=COM5 38400 8E1", output)
self.assertIn("case[1]=baseline-40-80-c0", output)
self.assertIn("case[2]=single-80", output)
self.assertIn("frame=04 00 00 80 00 DE checksum_ok=1", output)
def test_case_filter_selects_named_subset(self):
stdout = io.StringIO()
exit_code = main(["--dry-run", "--suite", "all", "--case", "pair-c0-80"], stdout=stdout)
self.assertEqual(exit_code, 0)
output = stdout.getvalue()
self.assertIn("cases=1", output)
self.assertIn("case[1]=pair-c0-80", output)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,80 @@
import unittest
from collections import Counter
from h8536.emulator.memory import MemoryAccess
from h8536.emulator.rx_divergence import (
DivergenceContext,
STATE_BUFFERS,
bench_signature_line,
classify_hits,
format_interesting_writes,
format_pc_hits,
format_state_changes,
label_frame,
parse_frame,
write_label,
)
class EmulatorRxDivergenceTest(unittest.TestCase):
def test_reuses_probe_frame_parser_and_labels_bench_signature(self):
frame = parse_frame("04 00 00 80 80")
self.assertEqual(frame, bytes.fromhex("04000080805E"))
self.assertIn("bench_cmd0_echo_80", label_frame(frame))
self.assertIn("bench_connect_ok", bench_signature_line())
def test_pc_hit_summary_makes_dispatch_outcome_visible(self):
hits = Counter({0xBC69: 1, 0xBA26: 2})
summary = classify_hits(hits)
self.assertTrue(summary["cmd0_reached_BC69"])
self.assertFalse(summary["cmd1_reached_BCD7"])
self.assertFalse(summary["retry_echo"])
self.assertFalse(summary["cmd7_replay"])
def test_formats_watched_pc_hits_in_trace_order(self):
context = DivergenceContext()
for pc in (0xBC0F, 0xBC69, 0xBC69):
context.record_pc(pc)
lines = format_pc_hits(context)
self.assertIn("H'BC0F:faa2_split=1", lines)
self.assertIn("H'BC69:cmd0_set_value=2", lines)
self.assertIn("first=H'BC0F,H'BC69,H'BC69", lines)
def test_filters_required_write_addresses(self):
accesses = [
MemoryAccess(0xF860, 1, 0x12, "write", "on_chip_ram"),
MemoryAccess(0xE001, 1, 0x34, "write", "external"),
MemoryAccess(0xE002, 1, 0x56, "write", "external"),
MemoryAccess(0xFEDD, 1, 0x78, "write", "register"),
]
lines = format_interesting_writes(accesses)
self.assertEqual(len(lines), 2)
self.assertIn("rx_validation_F860_F865", lines[0])
self.assertIn("primary_table_E000", lines[1])
self.assertEqual(write_label(0xFAA6), "serial_latches_FAA2_FAA6")
self.assertIsNone(write_label(0xE002))
def test_state_changes_formats_bytes_and_ints_compactly(self):
lines = format_state_changes(
{"FAA2_session_flags": 0, "F860_F865_rx_validation": b"\x00" * 6},
{"FAA2_session_flags": 0x80, "F860_F865_rx_validation": bytes.fromhex("010203040506")},
)
self.assertIn("FAA2_session_flags:00->80", lines)
self.assertIn("F860_F865_rx_validation:00 00 00 00 00 00->01 02 03 04 05 06", lines)
def test_snapshots_visible_40a0_candidate_table_slots(self):
self.assertEqual(STATE_BUFFERS["E880_E881_current_0040"], (0xE880, 2))
self.assertEqual(STATE_BUFFERS["E900_E901_current_0080"], (0xE900, 2))
self.assertEqual(STATE_BUFFERS["E980_E981_current_00C0"], (0xE980, 2))
if __name__ == "__main__":
unittest.main()