Compare commits
3 Commits
6d68a87e4e
...
74a2e2fd2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74a2e2fd2c | ||
|
|
4e0ef92e25 | ||
|
|
85732f8754 |
47
README.md
47
README.md
@@ -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.
|
||||||
|
|||||||
31
build/bench-sync-cmd0-readback-zero-eeprom.txt
Normal file
31
build/bench-sync-cmd0-readback-zero-eeprom.txt
Normal 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)
|
||||||
BIN
build/bench-sync-cmd0-readback-zero.bin
Normal file
BIN
build/bench-sync-cmd0-readback-zero.bin
Normal file
Binary file not shown.
31
build/bench-sync-cmd0-seed-zero-eeprom.txt
Normal file
31
build/bench-sync-cmd0-seed-zero-eeprom.txt
Normal 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)
|
||||||
BIN
build/bench-sync-cmd0-seed-zero.bin
Normal file
BIN
build/bench-sync-cmd0-seed-zero.bin
Normal file
Binary file not shown.
592
docs/pt2-protocol.md
Normal file
592
docs/pt2-protocol.md
Normal 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`
|
||||||
@@ -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
387
h8536/connect_ok_matrix.py
Normal 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",
|
||||||
|
]
|
||||||
529
h8536/emulator/rx_divergence.py
Normal file
529
h8536/emulator/rx_divergence.py
Normal 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",
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
5
h8536_emulator_rx_divergence.py
Normal file
5
h8536_emulator_rx_divergence.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from h8536.emulator.rx_divergence import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
14
scripts/connect_ok_matrix.py
Normal file
14
scripts/connect_ok_matrix.py
Normal 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())
|
||||||
45
tests/test_connect_ok_matrix.py
Normal file
45
tests/test_connect_ok_matrix.py
Normal 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()
|
||||||
80
tests/test_emulator_rx_divergence.py
Normal file
80
tests/test_emulator_rx_divergence.py
Normal 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()
|
||||||
Reference in New Issue
Block a user