EMualtor adjustments
This commit is contained in:
@@ -62,7 +62,7 @@ To start the current emulator harness:
|
|||||||
- Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates.
|
- Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates.
|
||||||
- Decodes observed serial byte captures into six-byte frames, validates checksums, labels capture-observed heartbeat/call/camera-power candidates, and summarizes heartbeat cadence.
|
- Decodes observed serial byte captures into six-byte frames, validates checksums, labels capture-observed heartbeat/call/camera-power candidates, and summarizes heartbeat cadence.
|
||||||
- Accepts both analyzer-style lines such as `RX 006 bytes ...` and the idle reference `frame 006 ...` format in `ROM/rcp-txd-idle-only.txt`.
|
- Accepts both analyzer-style lines such as `RX 006 bytes ...` and the idle reference `frame 006 ...` format in `ROM/rcp-txd-idle-only.txt`.
|
||||||
- Reconstructs the autonomous serial gate/queue state-machine around `loc_3FD3`, `loc_BAF2`, `F9B0/F9B5`, `FAA2/FAA3/FAA5`, and the resend path through `BE9E/BED5`.
|
- Reconstructs the autonomous serial gate/queue state-machine around `loc_3FD3`, `loc_BAF2`, `F9B0/F9B5`, `FAA2/FAA3/FAA5`, the `F9C4`/FRT2 idle heartbeat gate at `loc_4046`, and the resend path through `BE9E/BED5`.
|
||||||
- Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations.
|
- Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations.
|
||||||
- Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations.
|
- Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations.
|
||||||
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
|
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
|
||||||
@@ -79,7 +79,7 @@ To start the current emulator harness:
|
|||||||
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
|
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
|
||||||
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
|
- 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, scaffolded SCI1 TXI/interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, 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, scaffolded SCI1 TXI/interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path.
|
||||||
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, 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.
|
||||||
|
|
||||||
Current serial observations:
|
Current serial observations:
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ Current serial observations:
|
|||||||
- Idle frame: `00 00 00 00 80 DA`.
|
- Idle frame: `00 00 00 00 80 DA`.
|
||||||
- Capture-side label: `heartbeat_alive_candidate`.
|
- Capture-side label: `heartbeat_alive_candidate`.
|
||||||
- Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms.
|
- Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms.
|
||||||
|
- Static/runtime finding: `F9C4` is a candidate idle heartbeat/report countdown. Init loads `H'14`, `loc_BA26` reloads `H'07` after a send, FRT2 OCIA decrements it, and `loc_4046` can enqueue report `H'00FF` when it reaches zero and the queue is empty.
|
||||||
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
|
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
|
||||||
|
|
||||||
The generated listing is written to:
|
The generated listing is written to:
|
||||||
@@ -191,6 +192,8 @@ python h8536_emulator_probe.py --help
|
|||||||
- `--interval-steps N`: tune the scaffolded interval timer cadence.
|
- `--interval-steps N`: tune the scaffolded interval timer cadence.
|
||||||
- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: tune rough FRT compare-interrupt cadence.
|
- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: tune rough FRT compare-interrupt cadence.
|
||||||
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration.
|
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration.
|
||||||
|
- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history.
|
||||||
|
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
|
||||||
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT1/FRT2 OCIA. The probe now reaches the SCI1 transmit path, but still needs more timing/peripheral fidelity before it emits the full observed heartbeat frame.
|
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT1/FRT2 OCIA. The probe now reaches the SCI1 transmit path, but still needs more timing/peripheral fidelity before it emits the full observed heartbeat frame.
|
||||||
|
|
||||||
## Code Layout
|
## Code Layout
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,185 @@
|
|||||||
"Branch predicates are summarized from local instruction order and targets; this is not an emulator trace."
|
"Branch predicates are summarized from local instruction order and targets; this is not an emulator trace."
|
||||||
],
|
],
|
||||||
"evidence": {
|
"evidence": {
|
||||||
|
"idle_heartbeat_gate_loc_4046": {
|
||||||
|
"candidate_timer_roles": [
|
||||||
|
{
|
||||||
|
"address": 63940,
|
||||||
|
"address_hex": "H'F9C4",
|
||||||
|
"evidence_address_hex": "H'BF2D",
|
||||||
|
"role": "candidate idle heartbeat/report gate countdown"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decrement_address_hex": "H'BF2D",
|
||||||
|
"gate_address_hex": "H'4046",
|
||||||
|
"initial_reload_address_hex": "H'40E0",
|
||||||
|
"initial_reload_value_hex": "H'14",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"address": 16454,
|
||||||
|
"address_hex": "H'4046",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "TST.B @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16458,
|
||||||
|
"address_hex": "H'404A",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [
|
||||||
|
"H'4058"
|
||||||
|
],
|
||||||
|
"text": "BNE loc_4058"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16460,
|
||||||
|
"address_hex": "H'404C",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "BTST.B #7, @H'FAA5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16464,
|
||||||
|
"address_hex": "H'4050",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [
|
||||||
|
"H'4059"
|
||||||
|
],
|
||||||
|
"text": "BEQ loc_4059"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16466,
|
||||||
|
"address_hex": "H'4052",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "TST.B @H'F9C3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16470,
|
||||||
|
"address_hex": "H'4056",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [
|
||||||
|
"H'4059"
|
||||||
|
],
|
||||||
|
"text": "BEQ loc_4059"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16472,
|
||||||
|
"address_hex": "H'4058",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "RTS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16473,
|
||||||
|
"address_hex": "H'4059",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "MOV:G.B @H'F9B0, R2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16479,
|
||||||
|
"address_hex": "H'405F",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "CMP:G.B @H'F9B5, R2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16483,
|
||||||
|
"address_hex": "H'4063",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [
|
||||||
|
"H'4074"
|
||||||
|
],
|
||||||
|
"text": "BNE loc_4074"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16487,
|
||||||
|
"address_hex": "H'4067",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "MOV:G.W #H'00, @(-H'0790,R2)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16492,
|
||||||
|
"address_hex": "H'406C",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "ADD:Q.B #1, @H'F9B0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16496,
|
||||||
|
"address_hex": "H'4070",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "BCLR.B #7, @H'F9B0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 16608,
|
||||||
|
"address_hex": "H'40E0",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "MOV:G.B #H'14, @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 47665,
|
||||||
|
"address_hex": "H'BA31",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "MOV:G.B #H'07, @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 48931,
|
||||||
|
"address_hex": "H'BF23",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "BCLR.B #5, @FRT2_TCSR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 48935,
|
||||||
|
"address_hex": "H'BF27",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "TST.B @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": 48941,
|
||||||
|
"address_hex": "H'BF2D",
|
||||||
|
"present": true,
|
||||||
|
"targets_hex": [],
|
||||||
|
"text": "ADD:Q.B #-1, @H'F9C4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post_tx_reload_address_hex": "H'BA31",
|
||||||
|
"post_tx_reload_value_hex": "H'07",
|
||||||
|
"present": true,
|
||||||
|
"queue_write_address_hex": "H'4067",
|
||||||
|
"required_addresses_hex": [
|
||||||
|
"H'4046",
|
||||||
|
"H'4050",
|
||||||
|
"H'4067",
|
||||||
|
"H'40E0",
|
||||||
|
"H'BA31",
|
||||||
|
"H'BF23",
|
||||||
|
"H'BF2D"
|
||||||
|
],
|
||||||
|
"summary": "F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 can enqueue H'00FF if the queue is empty and the FAA5/F9C3 RX gate permits it. With FRT2 OCRA H'7A12 and CKS=phi/32, a phi near 10 MHz gives about 0.7s for H'07, matching the observed heartbeat cadence.",
|
||||||
|
"tick_handler_address_hex": "H'BF23",
|
||||||
|
"timer": {
|
||||||
|
"clock_select": "CKS1=1 CKS0=0 => phi/32",
|
||||||
|
"handler_address_hex": "H'BF23",
|
||||||
|
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
|
||||||
|
"observed_period_ms_candidate": 700,
|
||||||
|
"ocra_address_hex": "H'FEA4",
|
||||||
|
"ocra_value_hex": "H'7A12",
|
||||||
|
"source": "FRT2 OCIA",
|
||||||
|
"tcr_address_hex": "H'FEA0",
|
||||||
|
"tcsr_address_hex": "H'FEA1",
|
||||||
|
"vector_address_hex": "H'006A",
|
||||||
|
"vector_target_label": "vec_frt2_ocia_BF23"
|
||||||
|
},
|
||||||
|
"title": "loc_4046 idle heartbeat/report gate"
|
||||||
|
},
|
||||||
"queue_send_gate_loc_BAF2": {
|
"queue_send_gate_loc_BAF2": {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
@@ -1330,6 +1509,87 @@
|
|||||||
],
|
],
|
||||||
"write_count": 4
|
"write_count": 4
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"accesses": [
|
||||||
|
{
|
||||||
|
"access": "read",
|
||||||
|
"address": 16454,
|
||||||
|
"address_hex": "H'4046",
|
||||||
|
"function": "loc_4046",
|
||||||
|
"text": "TST.B @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "write",
|
||||||
|
"address": 16608,
|
||||||
|
"address_hex": "H'40E0",
|
||||||
|
"function": "loc_40BB",
|
||||||
|
"text": "MOV:G.B #H'14, @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "write",
|
||||||
|
"address": 47665,
|
||||||
|
"address_hex": "H'BA31",
|
||||||
|
"function": "loc_BA26",
|
||||||
|
"text": "MOV:G.B #H'07, @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "read",
|
||||||
|
"address": 48935,
|
||||||
|
"address_hex": "H'BF27",
|
||||||
|
"function": "vec_frt2_ocia_BF23",
|
||||||
|
"text": "TST.B @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "read_write",
|
||||||
|
"address": 48941,
|
||||||
|
"address_hex": "H'BF2D",
|
||||||
|
"function": "vec_frt2_ocia_BF23",
|
||||||
|
"text": "ADD:Q.B #-1, @H'F9C4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"address": 63940,
|
||||||
|
"address_hex": "H'F9C4",
|
||||||
|
"read_count": 2,
|
||||||
|
"read_write_count": 1,
|
||||||
|
"sample_accesses": [
|
||||||
|
{
|
||||||
|
"access": "read",
|
||||||
|
"address": 16454,
|
||||||
|
"address_hex": "H'4046",
|
||||||
|
"function": "loc_4046",
|
||||||
|
"text": "TST.B @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "write",
|
||||||
|
"address": 16608,
|
||||||
|
"address_hex": "H'40E0",
|
||||||
|
"function": "loc_40BB",
|
||||||
|
"text": "MOV:G.B #H'14, @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "write",
|
||||||
|
"address": 47665,
|
||||||
|
"address_hex": "H'BA31",
|
||||||
|
"function": "loc_BA26",
|
||||||
|
"text": "MOV:G.B #H'07, @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "read",
|
||||||
|
"address": 48935,
|
||||||
|
"address_hex": "H'BF27",
|
||||||
|
"function": "vec_frt2_ocia_BF23",
|
||||||
|
"text": "TST.B @H'F9C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access": "read_write",
|
||||||
|
"address": 48941,
|
||||||
|
"address_hex": "H'BF2D",
|
||||||
|
"function": "vec_frt2_ocia_BF23",
|
||||||
|
"text": "ADD:Q.B #-1, @H'F9C4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"write_count": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"accesses": [
|
"accesses": [
|
||||||
{
|
{
|
||||||
@@ -1993,6 +2253,11 @@
|
|||||||
"address_hex": "H'F9C3",
|
"address_hex": "H'F9C3",
|
||||||
"symbol": "ram_F9C3"
|
"symbol": "ram_F9C3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"address": 63940,
|
||||||
|
"address_hex": "H'F9C4",
|
||||||
|
"symbol": "ram_F9C4"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"address": 63941,
|
"address": 63941,
|
||||||
"address_hex": "H'F9C5",
|
"address_hex": "H'F9C5",
|
||||||
|
|||||||
@@ -72,6 +72,29 @@ Evidence:
|
|||||||
- H'BDF7: BCLR.B #7, @H'F9B5
|
- H'BDF7: BCLR.B #7, @H'F9B5
|
||||||
- H'BDFB: CLR.B @H'FAA3
|
- H'BDFB: CLR.B @H'FAA3
|
||||||
- H'BDFF: CLR.B @H'FAA2
|
- H'BDFF: CLR.B @H'FAA2
|
||||||
|
- loc_4046 idle heartbeat/report gate: present
|
||||||
|
F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 can enqueue H'00FF if the queue is empty and the FAA5/F9C3 RX gate permits it. With FRT2 OCRA H'7A12 and CKS=phi/32, a phi near 10 MHz gives about 0.7s for H'07, matching the observed heartbeat cadence.
|
||||||
|
- H'4046: TST.B @H'F9C4
|
||||||
|
- H'404A: BNE loc_4058
|
||||||
|
- H'404C: BTST.B #7, @H'FAA5
|
||||||
|
- H'4050: BEQ loc_4059
|
||||||
|
- H'4052: TST.B @H'F9C3
|
||||||
|
- H'4056: BEQ loc_4059
|
||||||
|
- H'4058: RTS
|
||||||
|
- H'4059: MOV:G.B @H'F9B0, R2
|
||||||
|
- H'405F: CMP:G.B @H'F9B5, R2
|
||||||
|
- H'4063: BNE loc_4074
|
||||||
|
- H'4067: MOV:G.W #H'00, @(-H'0790,R2)
|
||||||
|
- H'406C: ADD:Q.B #1, @H'F9B0
|
||||||
|
- H'4070: BCLR.B #7, @H'F9B0
|
||||||
|
- H'40E0: MOV:G.B #H'14, @H'F9C4
|
||||||
|
- H'BA31: MOV:G.B #H'07, @H'F9C4
|
||||||
|
- H'BF23: BCLR.B #5, @FRT2_TCSR
|
||||||
|
- H'BF27: TST.B @H'F9C4
|
||||||
|
- H'BF2D: ADD:Q.B #-1, @H'F9C4
|
||||||
|
Candidate timer roles:
|
||||||
|
- H'F9C4: candidate idle heartbeat/report gate countdown
|
||||||
|
Timer: FRT2 OCIA, H'BF23, OCRA=H'7A12, observed period ~= 700ms
|
||||||
- FRT1 OCIA periodic tick countdowns: present
|
- FRT1 OCIA periodic tick countdowns: present
|
||||||
Static evidence links vector H'0062 to the FRT1 OCIA handler at H'BEEA; the handler clears FRT1_TCSR.OCFA and conditionally decrements H'F9C0, H'F9C1, and H'F9C6.
|
Static evidence links vector H'0062 to the FRT1 OCIA handler at H'BEEA; the handler clears FRT1_TCSR.OCFA and conditionally decrements H'F9C0, H'F9C1, and H'F9C6.
|
||||||
- H'BEEA: BCLR.B #5, @FRT1_TCSR
|
- H'BEEA: BCLR.B #5, @FRT1_TCSR
|
||||||
@@ -101,6 +124,8 @@ State address readers/writers:
|
|||||||
H'BAED write CLR.B @H'F9C1; H'BB71 read TST.B @H'F9C1; H'BBA3 write MOV:G.B #H'05, @H'F9C1; H'BEF8 read TST.B @H'F9C1; H'BEFE read_write ADD:Q.B #-1, @H'F9C1
|
H'BAED write CLR.B @H'F9C1; H'BB71 read TST.B @H'F9C1; H'BBA3 write MOV:G.B #H'05, @H'F9C1; H'BEF8 read TST.B @H'F9C1; H'BEFE read_write ADD:Q.B #-1, @H'F9C1
|
||||||
- H'F9C3: reads=6 writes=4 read/write=0
|
- H'F9C3: reads=6 writes=4 read/write=0
|
||||||
H'3FDF read TST.B @H'F9C3; H'4052 read TST.B @H'F9C3; H'BA90 read TST.B @H'F9C3; H'BB77 write CLR.B @H'F9C3; H'BB7D read CMP:G.B #H'05, @H'F9C3; H'BB8A read MOV:G.B @H'F9C3, R1
|
H'3FDF read TST.B @H'F9C3; H'4052 read TST.B @H'F9C3; H'BA90 read TST.B @H'F9C3; H'BB77 write CLR.B @H'F9C3; H'BB7D read CMP:G.B #H'05, @H'F9C3; H'BB8A read MOV:G.B @H'F9C3, R1
|
||||||
|
- H'F9C4: reads=2 writes=2 read/write=1
|
||||||
|
H'4046 read TST.B @H'F9C4; H'40E0 write MOV:G.B #H'14, @H'F9C4; H'BA31 write MOV:G.B #H'07, @H'F9C4; H'BF27 read TST.B @H'F9C4; H'BF2D read_write ADD:Q.B #-1, @H'F9C4
|
||||||
- H'F9C5: reads=2 writes=2 read/write=1
|
- H'F9C5: reads=2 writes=2 read/write=1
|
||||||
H'3FEF read TST.B @H'F9C5; H'BB9E write MOV:G.B #H'14, @H'F9C5; H'BEE4 write CLR.B @H'F9C5; H'BF31 read TST.B @H'F9C5; H'BF37 read_write ADD:Q.B #-1, @H'F9C5
|
H'3FEF read TST.B @H'F9C5; H'BB9E write MOV:G.B #H'14, @H'F9C5; H'BEE4 write CLR.B @H'F9C5; H'BF31 read TST.B @H'F9C5; H'BF37 read_write ADD:Q.B #-1, @H'F9C5
|
||||||
- H'F9C6: reads=2 writes=2 read/write=1
|
- H'F9C6: reads=2 writes=2 read/write=1
|
||||||
|
|||||||
@@ -173,17 +173,17 @@ extern volatile u8 MEM8[0x10000];
|
|||||||
* state variable candidates:
|
* state variable candidates:
|
||||||
* - event_queue_read_cursor_candidate H'F9B4: reads 1, writes 2; bits 5
|
* - event_queue_read_cursor_candidate H'F9B4: reads 1, writes 2; bits 5
|
||||||
* evidence: H'BE78, H'BE95, H'BE99
|
* evidence: H'BE78, H'BE95, H'BE99
|
||||||
* - event_queue_write_or_pending_cursor_candidate H'F9B5: reads 1, writes 6; bits 7
|
* - event_queue_write_or_pending_cursor_candidate H'F9B5: reads 2, writes 6; bits 7
|
||||||
* evidence: H'BAF2, H'BD6D, H'BD71, H'BDC8, H'BDCC, H'BDF3, H'BDF7
|
* evidence: H'405F, H'BAF2, H'BD6D, H'BD71, H'BDC8, H'BDCC, H'BDF3, H'BDF7
|
||||||
* - event_queue_base_or_current_slot_candidate H'F9B9: reads 1, writes 0
|
* - event_queue_base_or_current_slot_candidate H'F9B9: reads 1, writes 0
|
||||||
* evidence: H'BE70
|
* evidence: H'BE70
|
||||||
* - serial_tx_busy_timer_candidate H'F9C0: reads 2, writes 8
|
* - serial_tx_busy_timer_candidate H'F9C0: reads 2, writes 8
|
||||||
* evidence: H'BA26, H'BA2C, H'BAA2, H'BADA, H'BAE1, H'BAE8, H'BE1D, H'BE3E, H'BEEE, H'BEF4
|
* evidence: H'BA26, H'BA2C, H'BAA2, H'BADA, H'BAE1, H'BAE8, H'BE1D, H'BE3E, H'BEEE, H'BEF4
|
||||||
* - autonomous_report_period_timer_candidate H'F9C6: reads 1, writes 2
|
* - idle_heartbeat_gate_countdown_candidate H'F9C4: reads 2, writes 3
|
||||||
* evidence: H'BB46, H'BEB5, H'BEC5
|
* evidence: H'4046, H'40E0, H'BA31, H'BF27, H'BF2D
|
||||||
* - autonomous_report_resend_countdown_candidate H'F9C8: reads 1, writes 2
|
* - rx_session_timeout_candidate H'F9C5: reads 1, writes 3
|
||||||
* evidence: H'BB4C, H'BEBB, H'BEC1
|
* evidence: H'BB9E, H'BEE4, H'BF31, H'BF37
|
||||||
* - ... 5 more state-variable candidates
|
* - ... 7 more state-variable candidates
|
||||||
* retry/error model candidate:
|
* retry/error model candidate:
|
||||||
* - checksum path: 0x5A-seeded XOR over RX[0..4] differs from RX[5] -> loc_BE29
|
* - checksum path: 0x5A-seeded XOR over RX[0..4] differs from RX[5] -> loc_BE29
|
||||||
* - retry path: counter H'FAA6, threshold 2; Candidate retry path clears/consults serial flags, increments FAA6, compares it with 2, and when still below the apparent limit stages a command 0x07 response.
|
* - retry path: counter H'FAA6, threshold 2; Candidate retry path clears/consults serial flags, increments FAA6, compares it with 2, and when still below the apparent limit stages a command 0x07 response.
|
||||||
@@ -191,13 +191,16 @@ extern volatile u8 MEM8[0x10000];
|
|||||||
* - evidence: H'BBD8, H'BBDC, H'BBE0, H'BBE4, H'BBE8, H'BBEC, H'BBF0, H'BE4D, H'BE56, H'BE5E, H'BE66, H'BE52, H'BE5A, H'BE62, H'BE6A, H'BE29, H'BE2D, H'BE33, H'BE37, H'BE43, H'BE47, H'BE05, H'BE0D, H'BE15, H'BE09, H'BE11, H'BE19, H'BE22
|
* - evidence: H'BBD8, H'BBDC, H'BBE0, H'BBE4, H'BBE8, H'BBEC, H'BBF0, H'BE4D, H'BE56, H'BE5E, H'BE66, H'BE52, H'BE5A, H'BE62, H'BE6A, H'BE29, H'BE2D, H'BE33, H'BE37, H'BE43, H'BE47, H'BE05, H'BE0D, H'BE15, H'BE09, H'BE11, H'BE19, H'BE22
|
||||||
* gate/queue state machine candidate:
|
* gate/queue state machine candidate:
|
||||||
* - main_loop_may_enter_report_builder: FAA2 == 0 && F9C0 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)); Main-loop report gate; session must be idle, TX busy timer clear, and RX gate open.
|
* - main_loop_may_enter_report_builder: FAA2 == 0 && F9C0 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)); Main-loop report gate; session must be idle, TX busy timer clear, and RX gate open.
|
||||||
|
* - idle_heartbeat_report_may_enqueue: F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5; Idle/default report gate; when the FRT2 countdown clears and the queue is empty, loc_4046 can enqueue H'00FF for the later loc_BAF2 -> loc_BA26 send path.
|
||||||
* - queue_has_pending_report: F9B5 != F9B0; Queue/pending cursor gate; non-empty state stages through BB43 before loc_BA26.
|
* - queue_has_pending_report: F9B5 != F9B0; Queue/pending cursor gate; non-empty state stages through BB43 before loc_BA26.
|
||||||
* - periodic_resend_may_fire: (FAA5 & FAA3 & 0x80) != 0 && F9C6 == 0 && F9C8 != 0 after countdown; Resend gate masks pending state with FAA5, checks F9C6/F9C8, then calls BA26 at BED5.
|
* - periodic_resend_may_fire: (FAA5 & FAA3 & 0x80) != 0 && F9C6 == 0 && F9C8 != 0 after countdown; Resend gate masks pending state with FAA5, checks F9C6/F9C8, then calls BA26 at BED5.
|
||||||
* - rx_completion_sets_session_timer: RX completion sets F9C5 (observed reload H'14) after the sixth byte is captured.
|
* - rx_completion_sets_session_timer: RX completion sets F9C5 (observed reload H'14) after the sixth byte is captured.
|
||||||
* - session_timeout_clears_gate_and_queue: When F9C5 is clear, loc_3FEF clears F9B5/F9B0 and clears FAA5.bit7; when nonzero, it sets FAA5.bit7.
|
* - session_timeout_clears_gate_and_queue: When F9C5 is clear, loc_3FEF clears F9B5/F9B0 and clears FAA5.bit7; when nonzero, it sets FAA5.bit7.
|
||||||
|
* - idle_heartbeat_gate_initial_delay_loaded: Startup/init loads F9C4 with H'14 before the first idle/default report can be queued.
|
||||||
|
* - idle_heartbeat_gate_post_send_delay_loaded: loc_BA26 reloads F9C4 with H'07 after each send, matching the observed heartbeat spacing.
|
||||||
* - host_ack_can_advance_queue: Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.; commands H'05, H'06
|
* - host_ack_can_advance_queue: Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.; commands H'05, H'06
|
||||||
* - caveat: Many panel controls may require host/session traffic before reporting. Observed autonomous call/camera-power indexes are runtime/capture overlays, not ROM constants.
|
* - caveat: Many panel controls may require host/session traffic before reporting. Observed autonomous call/camera-power indexes are runtime/capture overlays, not ROM constants.
|
||||||
* - evidence: H'3FD3, H'3FD7, H'3FD9, H'3FDD, H'3FDF, H'3FE3, H'3FE5, H'3FE9, H'3FEB, H'3FEF, H'3FF3, H'3FF5, H'3FF9, H'3FFD, H'4001, H'4003, H'4005, H'4007, H'BAF2, H'BAF6, H'BAF8, H'BAFC, H'BAFE, H'BB00, H'BB04, H'BB06, H'BB08, H'BB0C, H'BB0E, H'BB11, H'BB13, H'BB15, H'BB17, H'BB19, H'BB1C, H'BB20, H'BB24, H'BB26, H'BB29, H'BB2B, H'BB2F, H'BB33, H'BB35, H'BB39, H'BB3D, H'BB3F, H'BB43, H'BE9E, H'BEA2, H'BEA5, H'BEA9, H'BEAD, H'BEAF, H'BEB3, H'BEB5, H'BEB9, H'BEBB, H'BEBF, H'BEC1, H'BEC5, H'BECB, H'BECF, H'BED1, H'BED5
|
* - evidence: H'3FD3, H'3FD7, H'3FD9, H'3FDD, H'3FDF, H'3FE3, H'3FE5, H'3FE9, H'3FEB, H'3FEF, H'3FF3, H'3FF5, H'3FF9, H'3FFD, H'4001, H'4003, H'4005, H'4007, H'4046, H'404A, H'404C, H'4050, H'4052, H'4056, H'4058, H'4059, H'405D, H'405F, H'4063, H'4065, H'4067, H'406C, H'4070, H'BAF2, H'BAF6, H'BAF8, H'BAFC, H'BAFE, H'BB00, H'BB04, H'BB06, H'BB08, H'BB0C, H'BB0E, H'BB11, H'BB13, H'BB15, H'BB17, H'BB19, H'BB1C, H'BB20, H'BB24, H'BB26, H'BB29, H'BB2B, H'BB2F, H'BB33, H'BB35, H'BB39, H'BB3D, H'BB3F, H'BB43, H'BE9E, H'BEA2, H'BEA5, H'BEA9, H'BEAD, H'BEAF, H'BEB3, H'BEB5, H'BEB9, H'BEBB, H'BEBF, H'BEC1, H'BEC5, H'BECB, H'BECF, H'BED1, H'BED5
|
||||||
* TX/autonomous report model candidate:
|
* TX/autonomous report model candidate:
|
||||||
* - loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate; byte5 is 0x5A XOR checksum
|
* - loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate; byte5 is 0x5A XOR checksum
|
||||||
* - observed overlay candidates: heartbeat_or_idle_report_candidate: 00 00 00 00 80 DA; call_button_report_candidate: 00 00 15 80 00 CF, 00 00 15 00 00 4F; camera_power_report_candidate: 00 00 07 80 00 DD
|
* - observed overlay candidates: heartbeat_or_idle_report_candidate: 00 00 00 00 80 DA; call_button_report_candidate: 00 00 15 80 00 CF, 00 00 15 00 00 4F; camera_power_report_candidate: 00 00 07 80 00 DD
|
||||||
@@ -210,10 +213,14 @@ extern volatile u8 MEM8[0x10000];
|
|||||||
* - BED5 resend path: Candidate periodic resend path feeding the TX staging/send-builder flow.
|
* - BED5 resend path: Candidate periodic resend path feeding the TX staging/send-builder flow.
|
||||||
* - evidence: H'BB46, H'BEC5, H'BB4C, H'BB51, H'BECB, H'BED5
|
* - evidence: H'BB46, H'BEC5, H'BB4C, H'BB51, H'BECB, H'BED5
|
||||||
* interrupt/timer architecture candidate:
|
* interrupt/timer architecture candidate:
|
||||||
* - FRT1 OCIA H'BEEA appears to be a periodic tick ISR for serial gate/cadence counters.
|
* - FRT1 OCIA H'BEEA: Candidate periodic tick ISR for serial busy, interbyte, and resend counters.
|
||||||
|
* - FRT2 OCIA H'BF23: Candidate periodic tick ISR for idle heartbeat/report and RX session counters.
|
||||||
* - H'F9C0 tx_report_gate_counter_candidate: candidate gate counter used before entering the report builder.
|
* - H'F9C0 tx_report_gate_counter_candidate: candidate gate counter used before entering the report builder.
|
||||||
* - H'F9C1 rx_interbyte_timeout_candidate: candidate RX interbyte timeout counter.
|
* - H'F9C1 rx_interbyte_timeout_candidate: candidate RX interbyte timeout counter.
|
||||||
* - H'F9C6 periodic_resend_cadence_counter_candidate: candidate periodic resend/heartbeat cadence counter.
|
* - H'F9C6 periodic_resend_cadence_counter_candidate: candidate periodic resend/heartbeat cadence counter.
|
||||||
|
* - H'F9C4 idle_heartbeat_gate_countdown_candidate: candidate idle/default report enqueue countdown.
|
||||||
|
* - H'F9C5 rx_session_timeout_candidate: candidate RX/session maintenance timeout counter.
|
||||||
|
* - evidence: H'BEEA, H'BEEE, H'BEF2, H'BEF4, H'BEF8, H'BEFC, H'BEFE, H'BF02, H'BF06, H'BF08, H'BF23, H'BF27, H'BF2B, H'BF2D, H'BF31, H'BF35, H'BF37
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static u8 sci1_rx_candidate_command(void)
|
static u8 sci1_rx_candidate_command(void)
|
||||||
@@ -257,6 +264,15 @@ static bool sci1_candidate_report_queue_nonempty(void)
|
|||||||
return MEM8[0xF9B5u] != MEM8[0xF9B0u];
|
return MEM8[0xF9B5u] != MEM8[0xF9B0u];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)
|
||||||
|
{
|
||||||
|
bool idle_timer_clear = MEM8[0xF9C4u] == 0u;
|
||||||
|
bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;
|
||||||
|
bool queue_empty = MEM8[0xF9B0u] == MEM8[0xF9B5u];
|
||||||
|
|
||||||
|
return idle_timer_clear && rx_gate_open && queue_empty;
|
||||||
|
}
|
||||||
|
|
||||||
static bool sci1_candidate_periodic_resend_gate_open(void)
|
static bool sci1_candidate_periodic_resend_gate_open(void)
|
||||||
{
|
{
|
||||||
bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;
|
bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;
|
||||||
@@ -268,7 +284,7 @@ static bool sci1_candidate_periodic_resend_gate_open(void)
|
|||||||
|
|
||||||
void frt1_ocia_candidate_tick_isr(void)
|
void frt1_ocia_candidate_tick_isr(void)
|
||||||
{
|
{
|
||||||
/* Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters. */
|
/* Candidate periodic tick at H'BEEA: decrement nonzero serial counters. */
|
||||||
/* TX_REPORT_GATE_COUNTER_CANDIDATE: candidate gate counter used before entering the report builder. */
|
/* TX_REPORT_GATE_COUNTER_CANDIDATE: candidate gate counter used before entering the report builder. */
|
||||||
if (MEM8[0xF9C0u] != 0u) {
|
if (MEM8[0xF9C0u] != 0u) {
|
||||||
MEM8[0xF9C0u] = (u8)(MEM8[0xF9C0u] - 1u);
|
MEM8[0xF9C0u] = (u8)(MEM8[0xF9C0u] - 1u);
|
||||||
@@ -286,6 +302,21 @@ void frt1_ocia_candidate_tick_isr(void)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void frt2_ocia_candidate_tick_isr(void)
|
||||||
|
{
|
||||||
|
/* Candidate periodic tick at H'BF23: decrement nonzero serial counters. */
|
||||||
|
/* IDLE_HEARTBEAT_GATE_COUNTDOWN_CANDIDATE: candidate idle/default report enqueue countdown. */
|
||||||
|
if (MEM8[0xF9C4u] != 0u) {
|
||||||
|
MEM8[0xF9C4u] = (u8)(MEM8[0xF9C4u] - 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RX_SESSION_TIMEOUT_CANDIDATE: candidate RX/session maintenance timeout counter. */
|
||||||
|
if (MEM8[0xF9C5u] != 0u) {
|
||||||
|
MEM8[0xF9C5u] = (u8)(MEM8[0xF9C5u] - 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void sci1_process_candidate_protocol_command(void)
|
void sci1_process_candidate_protocol_command(void)
|
||||||
{
|
{
|
||||||
u8 command = sci1_rx_candidate_command();
|
u8 command = sci1_rx_candidate_command();
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ class P9FastPathEvent:
|
|||||||
kind: str
|
kind: str
|
||||||
pc: int
|
pc: int
|
||||||
value: int | None = None
|
value: int | None = None
|
||||||
|
source: str | None = None
|
||||||
|
queue_depth: int | None = None
|
||||||
|
|
||||||
|
def line(self) -> str:
|
||||||
|
value = "" if self.value is None else f" value={self.value:02X}"
|
||||||
|
source = "" if self.source is None else f" source={self.source}"
|
||||||
|
queue_depth = "" if self.queue_depth is None else f" queued={self.queue_depth}"
|
||||||
|
return f"{self.kind} pc={self.pc:04X}{value}{source}{queue_depth}"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -59,11 +67,26 @@ class P9FastPath:
|
|||||||
|
|
||||||
config: P9FastPathConfig = field(default_factory=P9FastPathConfig)
|
config: P9FastPathConfig = field(default_factory=P9FastPathConfig)
|
||||||
input_bytes: list[int] = field(default_factory=list)
|
input_bytes: list[int] = field(default_factory=list)
|
||||||
|
input_sources: list[str] = field(default_factory=list)
|
||||||
output_bytes: list[int] = field(default_factory=list)
|
output_bytes: list[int] = field(default_factory=list)
|
||||||
events: list[P9FastPathEvent] = field(default_factory=list)
|
events: list[P9FastPathEvent] = field(default_factory=list)
|
||||||
|
|
||||||
def queue_input(self, *values: int) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
if len(self.input_sources) < len(self.input_bytes):
|
||||||
|
self.input_sources.extend(["initial"] * (len(self.input_bytes) - len(self.input_sources)))
|
||||||
|
elif len(self.input_sources) > len(self.input_bytes):
|
||||||
|
del self.input_sources[len(self.input_bytes) :]
|
||||||
|
|
||||||
|
def queue_input(self, *values: int, source: str = "queued") -> None:
|
||||||
self.input_bytes.extend(value & 0xFF for value in values)
|
self.input_bytes.extend(value & 0xFF for value in values)
|
||||||
|
self.input_sources.extend(source for _ in values)
|
||||||
|
|
||||||
|
def queue_input_script(self, name: str, values: list[int] | tuple[int, ...]) -> None:
|
||||||
|
self.queue_input(*values, source=f"script:{name}")
|
||||||
|
|
||||||
|
def trace_lines(self, limit: int | None = None) -> list[str]:
|
||||||
|
events = self.events if limit is None else self.events[-limit:]
|
||||||
|
return [event.line() for event in events]
|
||||||
|
|
||||||
def try_handle(self, emulator: Any) -> bool:
|
def try_handle(self, emulator: Any) -> bool:
|
||||||
if not self.config.enabled:
|
if not self.config.enabled:
|
||||||
@@ -102,9 +125,14 @@ class P9FastPath:
|
|||||||
|
|
||||||
def _handle_read_byte(self, emulator: Any) -> None:
|
def _handle_read_byte(self, emulator: Any) -> None:
|
||||||
pc = emulator.cpu.pc & 0xFFFF
|
pc = emulator.cpu.pc & 0xFFFF
|
||||||
value = self.input_bytes.pop(0) if self.input_bytes else self.config.default_input_byte
|
if self.input_bytes:
|
||||||
|
value = self.input_bytes.pop(0)
|
||||||
|
source = self.input_sources.pop(0) if self.input_sources else "queued"
|
||||||
|
else:
|
||||||
|
value = self.config.default_input_byte
|
||||||
|
source = "default_input_byte"
|
||||||
value &= 0xFF
|
value &= 0xFF
|
||||||
self.events.append(P9FastPathEvent("read_byte", pc, value))
|
self.events.append(P9FastPathEvent("read_byte", pc, value, source, len(self.input_bytes)))
|
||||||
|
|
||||||
# The ROM-side read routine yields a byte in R5. Model that as a byte
|
# The ROM-side read routine yields a byte in R5. Model that as a byte
|
||||||
# register write so the existing high byte is not accidentally clobbered.
|
# register write so the existing high byte is not accidentally clobbered.
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ from pathlib import Path
|
|||||||
from ..formatting import h16, parse_int
|
from ..formatting import h16, parse_int
|
||||||
from .cli import load_rom
|
from .cli import load_rom
|
||||||
from .constants import (
|
from .constants import (
|
||||||
|
ON_CHIP_RAM_END,
|
||||||
|
ON_CHIP_RAM_START,
|
||||||
P9DDR,
|
P9DDR,
|
||||||
P9DR,
|
P9DR,
|
||||||
|
REGISTER_FIELD_END,
|
||||||
|
REGISTER_FIELD_START,
|
||||||
SCI1_BRR,
|
SCI1_BRR,
|
||||||
SCI1_RDR,
|
SCI1_RDR,
|
||||||
SCI1_SCR,
|
SCI1_SCR,
|
||||||
@@ -26,6 +30,56 @@ from .runner import H8536Emulator
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059)
|
DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059)
|
||||||
|
TX_FRAME_WATCH_PCS = {
|
||||||
|
0xBA26: "builder_entry",
|
||||||
|
0xBA4E: "checksum_seed",
|
||||||
|
0xBA64: "checksum_store",
|
||||||
|
0xBA72: "first_tdr",
|
||||||
|
0xBAB5: "txi_tdr",
|
||||||
|
}
|
||||||
|
TX_FRAME_SNAPSHOT_START = 0xF850
|
||||||
|
TX_FRAME_START = 0xF858
|
||||||
|
TX_FRAME_LENGTH = 6
|
||||||
|
TX_FRAME_TRACE_START = TX_FRAME_SNAPSHOT_START
|
||||||
|
TX_FRAME_TRACE_END = TX_FRAME_START + TX_FRAME_LENGTH - 1
|
||||||
|
REPORT_QUEUE_START = 0xF870
|
||||||
|
REPORT_QUEUE_SLOTS = 0x80
|
||||||
|
REPORT_QUEUE_END = REPORT_QUEUE_START + (REPORT_QUEUE_SLOTS * 2) - 1
|
||||||
|
REPORT_QUEUE_HEAD = 0xF9B0
|
||||||
|
REPORT_QUEUE_TAIL = 0xF9B5
|
||||||
|
REPORT_QUEUE_ENTRY_PCS = {
|
||||||
|
0x3E54: "enqueue_entry",
|
||||||
|
0xBAF2: "dequeue_entry",
|
||||||
|
}
|
||||||
|
REPORT_QUEUE_FIRST_NONZERO_DISPLAY_LIMIT = 16
|
||||||
|
REPORT_GATE_PCS = {
|
||||||
|
0x4046: "gate_entry",
|
||||||
|
0x404A: "f9c4_nonzero_return_branch",
|
||||||
|
0x404C: "faa5_bit7_test",
|
||||||
|
0x4050: "faa5_clear_enqueue_branch",
|
||||||
|
0x4052: "f9c3_test",
|
||||||
|
0x4056: "f9c3_zero_enqueue_branch",
|
||||||
|
0x4058: "gate_return_no_enqueue",
|
||||||
|
0x4059: "queue_empty_check_start",
|
||||||
|
0x405F: "queue_empty_compare",
|
||||||
|
0x4063: "queue_not_empty_return_branch",
|
||||||
|
0x4067: "enqueue_report_zero",
|
||||||
|
0x406C: "advance_head",
|
||||||
|
0x4070: "mask_head",
|
||||||
|
0x4074: "gate_return",
|
||||||
|
}
|
||||||
|
REPORT_GATE_F9C4 = 0xF9C4
|
||||||
|
REPORT_GATE_F9C3 = 0xF9C3
|
||||||
|
REPORT_GATE_FAA5 = 0xFAA5
|
||||||
|
RAM_LIFECYCLE_DEFAULT_WATCHES = {
|
||||||
|
REPORT_GATE_F9C4: "F9C4_report_gate_timer",
|
||||||
|
REPORT_GATE_F9C3: "F9C3_rx_or_activity_gate",
|
||||||
|
REPORT_GATE_FAA5: "FAA5_report_flags",
|
||||||
|
0xF9C0: "F9C0_tx_or_report_gate",
|
||||||
|
0xF9C5: "F9C5_queue_reset_gate",
|
||||||
|
REPORT_QUEUE_HEAD: "F9B0_report_queue_head",
|
||||||
|
REPORT_QUEUE_TAIL: "F9B5_report_queue_tail",
|
||||||
|
}
|
||||||
SCI1_PROBE_REGISTERS = {
|
SCI1_PROBE_REGISTERS = {
|
||||||
SCI1_SCR: "SCR",
|
SCI1_SCR: "SCR",
|
||||||
SCI1_TDR: "TDR",
|
SCI1_TDR: "TDR",
|
||||||
@@ -50,6 +104,72 @@ def _format_six_byte_frames(data: bytes, *, limit: int = 8) -> str:
|
|||||||
return " | ".join(frame.hex(" ").upper() for frame in recent)
|
return " | ".join(frame.hex(" ").upper() for frame in recent)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tx_frame(text: str) -> bytes:
|
||||||
|
normalized = (
|
||||||
|
text.strip()
|
||||||
|
.replace(",", " ")
|
||||||
|
.replace(":", " ")
|
||||||
|
.replace("-", " ")
|
||||||
|
.replace("_", " ")
|
||||||
|
)
|
||||||
|
parts = normalized.split()
|
||||||
|
if len(parts) == 1:
|
||||||
|
compact = parts[0]
|
||||||
|
if compact.lower().startswith("0x"):
|
||||||
|
compact = compact[2:]
|
||||||
|
if compact.upper().startswith("H'"):
|
||||||
|
compact = compact[2:]
|
||||||
|
if len(compact) % 2:
|
||||||
|
raise argparse.ArgumentTypeError("target frame compact hex must have an even number of digits")
|
||||||
|
parts = [compact[index : index + 2] for index in range(0, len(compact), 2)]
|
||||||
|
|
||||||
|
values: list[int] = []
|
||||||
|
for part in parts:
|
||||||
|
token = part
|
||||||
|
if token.lower().startswith("0x"):
|
||||||
|
token = token[2:]
|
||||||
|
if token.upper().startswith("H'"):
|
||||||
|
token = token[2:]
|
||||||
|
if not token or len(token) > 2:
|
||||||
|
raise argparse.ArgumentTypeError(f"invalid byte token {part!r}")
|
||||||
|
try:
|
||||||
|
value = int(token, 16)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise argparse.ArgumentTypeError(f"invalid byte token {part!r}") from exc
|
||||||
|
if not 0 <= value <= 0xFF:
|
||||||
|
raise argparse.ArgumentTypeError(f"byte out of range {part!r}")
|
||||||
|
values.append(value)
|
||||||
|
|
||||||
|
if len(values) != TX_FRAME_LENGTH:
|
||||||
|
raise argparse.ArgumentTypeError(f"target frame must contain exactly {TX_FRAME_LENGTH} bytes")
|
||||||
|
return bytes(values)
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_diff_text(current: bytes, target: bytes) -> str:
|
||||||
|
diffs = [
|
||||||
|
f"{index}:{current[index]:02X}!={target[index]:02X}"
|
||||||
|
for index in range(min(len(current), len(target)))
|
||||||
|
if current[index] != target[index]
|
||||||
|
]
|
||||||
|
if len(current) != len(target):
|
||||||
|
diffs.append(f"length:{len(current)}!={len(target)}")
|
||||||
|
return "none" if not diffs else " ".join(diffs)
|
||||||
|
|
||||||
|
|
||||||
|
def _target_comparison_line(current: bytes, target: bytes) -> str:
|
||||||
|
pre_checksum_diffs = [
|
||||||
|
str(index)
|
||||||
|
for index in range(TX_FRAME_LENGTH - 1)
|
||||||
|
if index < len(current) and index < len(target) and current[index] != target[index]
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
"target_frame="
|
||||||
|
f"target={target.hex(' ').upper()} current={current.hex(' ').upper()} "
|
||||||
|
f"diffs={_frame_diff_text(current, target)} "
|
||||||
|
f"pre_checksum_diffs={','.join(pre_checksum_diffs) if pre_checksum_diffs else 'none'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SCI1Snapshot:
|
class SCI1Snapshot:
|
||||||
smr: int
|
smr: int
|
||||||
@@ -118,6 +238,187 @@ class WatchSnapshot:
|
|||||||
return f"step={self.step} pc={h16(self.pc)} sp={h16(self.sp)} {regs} stack=[{stack}] callers=[{callers}]"
|
return f"step={self.step} pc={h16(self.pc)} sp={h16(self.sp)} {regs} stack=[{stack}] callers=[{callers}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TXFrameSnapshot:
|
||||||
|
step: int
|
||||||
|
pc: int
|
||||||
|
label: str
|
||||||
|
bytes_f850_f85d: bytes
|
||||||
|
computed_checksum: int
|
||||||
|
stored_checksum: int
|
||||||
|
checksum_ok: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frame_bytes(self) -> bytes:
|
||||||
|
return self.bytes_f850_f85d[TX_FRAME_START - TX_FRAME_SNAPSHOT_START :]
|
||||||
|
|
||||||
|
def line(self) -> str:
|
||||||
|
return (
|
||||||
|
f"step={self.step} pc={h16(self.pc)} {self.label} "
|
||||||
|
f"F850-F85D={self.bytes_f850_f85d.hex(' ').upper()} "
|
||||||
|
f"TX={self.frame_bytes.hex(' ').upper()} "
|
||||||
|
f"computed={self.computed_checksum:02X} stored={self.stored_checksum:02X} "
|
||||||
|
f"checksum_ok={int(self.checksum_ok)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TXFrameWriteTrace:
|
||||||
|
step: int
|
||||||
|
pc: int
|
||||||
|
address: int
|
||||||
|
old_value: int
|
||||||
|
new_value: int
|
||||||
|
frame_after: bytes
|
||||||
|
instruction: str
|
||||||
|
regs: tuple[int, ...]
|
||||||
|
target_value: int | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def area(self) -> str:
|
||||||
|
if TX_FRAME_START <= self.address < TX_FRAME_START + TX_FRAME_LENGTH:
|
||||||
|
return f"TX[{self.address - TX_FRAME_START}]"
|
||||||
|
return f"stage[{self.address - TX_FRAME_SNAPSHOT_START}]"
|
||||||
|
|
||||||
|
def line(self) -> str:
|
||||||
|
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
|
||||||
|
target = ""
|
||||||
|
if self.target_value is not None:
|
||||||
|
verdict = "match" if self.new_value == self.target_value else "DIFF"
|
||||||
|
target = f" target={self.target_value:02X} {verdict}"
|
||||||
|
return (
|
||||||
|
f"step={self.step} pc={h16(self.pc)} {self.area} {h16(self.address)} "
|
||||||
|
f"{self.old_value:02X}->{self.new_value:02X}{target} "
|
||||||
|
f"frame_after={self.frame_after.hex(' ').upper()} regs=[{regs}] "
|
||||||
|
f"instruction={self.instruction}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReportQueueTrace:
|
||||||
|
step: int
|
||||||
|
pc: int
|
||||||
|
kind: str
|
||||||
|
head: int
|
||||||
|
tail: int
|
||||||
|
instruction: str
|
||||||
|
regs: tuple[int, ...]
|
||||||
|
address: int | None = None
|
||||||
|
queue_index: int | None = None
|
||||||
|
old_value: int | None = None
|
||||||
|
new_value: int | None = None
|
||||||
|
old_word: int | None = None
|
||||||
|
new_word: int | None = None
|
||||||
|
candidate_word: int | None = None
|
||||||
|
|
||||||
|
def line(self) -> str:
|
||||||
|
parts = [
|
||||||
|
f"step={self.step}",
|
||||||
|
f"pc={h16(self.pc)}",
|
||||||
|
self.kind,
|
||||||
|
f"head={self.head:02X}",
|
||||||
|
f"tail={self.tail:02X}",
|
||||||
|
f"depth={_report_queue_depth(self.head, self.tail):02X}",
|
||||||
|
]
|
||||||
|
if self.queue_index is not None:
|
||||||
|
parts.append(f"slot={self.queue_index:02X}")
|
||||||
|
if self.address is not None:
|
||||||
|
parts.append(f"addr={h16(self.address)}")
|
||||||
|
if self.old_value is not None and self.new_value is not None:
|
||||||
|
parts.append(f"byte={self.old_value:02X}->{self.new_value:02X}")
|
||||||
|
if self.old_word is not None:
|
||||||
|
if self.new_word is None:
|
||||||
|
parts.append(f"word={h16(self.old_word)}")
|
||||||
|
else:
|
||||||
|
parts.append(f"word={h16(self.old_word)}->{h16(self.new_word)}")
|
||||||
|
if self.candidate_word is not None:
|
||||||
|
parts.append(f"candidate={h16(self.candidate_word)}")
|
||||||
|
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
|
||||||
|
parts.append(f"regs=[{regs}]")
|
||||||
|
parts.append(f"instruction={self.instruction}")
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RAMLifecycleTrace:
|
||||||
|
step: int
|
||||||
|
pc: int
|
||||||
|
address: int
|
||||||
|
name: str
|
||||||
|
old_value: int
|
||||||
|
new_value: int
|
||||||
|
instruction: str
|
||||||
|
regs: tuple[int, ...]
|
||||||
|
|
||||||
|
def line(self) -> str:
|
||||||
|
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
|
||||||
|
return (
|
||||||
|
f"step={self.step} pc={h16(self.pc)} {self.name} {h16(self.address)} "
|
||||||
|
f"{self.old_value:02X}->{self.new_value:02X} regs=[{regs}] "
|
||||||
|
f"instruction={self.instruction}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReportGateTrace:
|
||||||
|
step: int
|
||||||
|
pc: int
|
||||||
|
label: str
|
||||||
|
f9c4: int
|
||||||
|
faa5: int
|
||||||
|
f9c3: int
|
||||||
|
head: int
|
||||||
|
tail: int
|
||||||
|
regs: tuple[int, ...]
|
||||||
|
z: bool
|
||||||
|
c: bool
|
||||||
|
n: bool
|
||||||
|
decision: str
|
||||||
|
f9c4_last_write_step: int | None = None
|
||||||
|
f9c4_last_write_pc: int | None = None
|
||||||
|
f9c4_last_write_value: int | None = None
|
||||||
|
f9c4_last_write_age: int | None = None
|
||||||
|
f9c4_last_nonzero_step: int | None = None
|
||||||
|
f9c4_last_nonzero_pc: int | None = None
|
||||||
|
f9c4_last_nonzero_value: int | None = None
|
||||||
|
f9c4_last_nonzero_age: int | None = None
|
||||||
|
|
||||||
|
def line(self) -> str:
|
||||||
|
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
|
||||||
|
lifecycle_parts = []
|
||||||
|
if (
|
||||||
|
self.f9c4_last_write_step is not None
|
||||||
|
and self.f9c4_last_write_pc is not None
|
||||||
|
and self.f9c4_last_write_value is not None
|
||||||
|
):
|
||||||
|
lifecycle_parts.append(
|
||||||
|
"F9C4_last="
|
||||||
|
f"step={self.f9c4_last_write_step}@{h16(self.f9c4_last_write_pc)} "
|
||||||
|
f"value={self.f9c4_last_write_value:02X} age={self.f9c4_last_write_age}"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.f9c4_last_nonzero_step is not None
|
||||||
|
and self.f9c4_last_nonzero_pc is not None
|
||||||
|
and self.f9c4_last_nonzero_value is not None
|
||||||
|
):
|
||||||
|
lifecycle_parts.append(
|
||||||
|
"F9C4_last_nonzero="
|
||||||
|
f"step={self.f9c4_last_nonzero_step}@{h16(self.f9c4_last_nonzero_pc)} "
|
||||||
|
f"value={self.f9c4_last_nonzero_value:02X} age={self.f9c4_last_nonzero_age}"
|
||||||
|
)
|
||||||
|
lifecycle = ""
|
||||||
|
if lifecycle_parts:
|
||||||
|
lifecycle = " " + " ".join(lifecycle_parts)
|
||||||
|
return (
|
||||||
|
f"step={self.step} pc={h16(self.pc)} {self.label} "
|
||||||
|
f"F9C4={self.f9c4:02X} FAA5={self.faa5:02X}/bit7={(self.faa5 >> 7) & 1} "
|
||||||
|
f"F9C3={self.f9c3:02X} head={self.head:02X} tail={self.tail:02X} "
|
||||||
|
f"depth={_report_queue_depth(self.head, self.tail):02X} "
|
||||||
|
f"Z={int(self.z)} C={int(self.c)} N={int(self.n)} "
|
||||||
|
f"decision={self.decision}{lifecycle} regs=[{regs}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProbeReport:
|
class ProbeReport:
|
||||||
steps: int
|
steps: int
|
||||||
@@ -133,6 +434,19 @@ class ProbeReport:
|
|||||||
sci1: SCI1Snapshot | None = None
|
sci1: SCI1Snapshot | None = None
|
||||||
sci1_txi: SCI1TXISummary | None = None
|
sci1_txi: SCI1TXISummary | None = None
|
||||||
watch_snapshots: list[WatchSnapshot] = field(default_factory=list)
|
watch_snapshots: list[WatchSnapshot] = field(default_factory=list)
|
||||||
|
tx_frame_snapshots: list[TXFrameSnapshot] = field(default_factory=list)
|
||||||
|
tx_frame_write_traces: list[TXFrameWriteTrace] = field(default_factory=list)
|
||||||
|
tx_target_divergences: list[TXFrameWriteTrace] = field(default_factory=list)
|
||||||
|
report_queue_traces: list[ReportQueueTrace] = field(default_factory=list)
|
||||||
|
report_queue_first_writes: list[ReportQueueTrace] = field(default_factory=list)
|
||||||
|
report_queue_first_nonzero_writes: list[ReportQueueTrace] = field(default_factory=list)
|
||||||
|
report_queue_watch_hits: list[ReportQueueTrace] = field(default_factory=list)
|
||||||
|
report_gate_traces: list[ReportGateTrace] = field(default_factory=list)
|
||||||
|
ram_lifecycle_traces: list[RAMLifecycleTrace] = field(default_factory=list)
|
||||||
|
ram_lifecycle_last_writes: list[RAMLifecycleTrace] = field(default_factory=list)
|
||||||
|
ram_lifecycle_last_nonzero_writes: list[RAMLifecycleTrace] = field(default_factory=list)
|
||||||
|
final_tx_frame: bytes = b""
|
||||||
|
target_frame: bytes | None = None
|
||||||
unsupported: str | None = None
|
unsupported: str | None = None
|
||||||
|
|
||||||
def lines(self, hot_limit: int = 12) -> list[str]:
|
def lines(self, hot_limit: int = 12) -> list[str]:
|
||||||
@@ -153,12 +467,51 @@ class ProbeReport:
|
|||||||
lines.append(self.sci1_txi.line())
|
lines.append(self.sci1_txi.line())
|
||||||
if self.unsupported:
|
if self.unsupported:
|
||||||
lines.append(f"unsupported={self.unsupported}")
|
lines.append(f"unsupported={self.unsupported}")
|
||||||
|
if self.target_frame is not None:
|
||||||
|
current = self.final_tx_frame
|
||||||
|
if not current and self.tx_frame_snapshots:
|
||||||
|
current = self.tx_frame_snapshots[-1].frame_bytes
|
||||||
|
lines.append(_target_comparison_line(current, self.target_frame))
|
||||||
if self.p9_accesses:
|
if self.p9_accesses:
|
||||||
lines.append("recent_p9:")
|
lines.append("recent_p9:")
|
||||||
lines.extend(" " + line for line in self.p9_accesses[-24:])
|
lines.extend(" " + line for line in self.p9_accesses[-24:])
|
||||||
if self.sci_accesses:
|
if self.sci_accesses:
|
||||||
lines.append("recent_sci:")
|
lines.append("recent_sci:")
|
||||||
lines.extend(" " + line for line in self.sci_accesses[-16:])
|
lines.extend(" " + line for line in self.sci_accesses[-16:])
|
||||||
|
if self.tx_frame_snapshots:
|
||||||
|
lines.append("recent_tx_frame_snapshots:")
|
||||||
|
lines.extend(" " + snapshot.line() for snapshot in self.tx_frame_snapshots)
|
||||||
|
if self.tx_frame_write_traces:
|
||||||
|
lines.append("recent_tx_frame_writes:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.tx_frame_write_traces)
|
||||||
|
if self.tx_target_divergences:
|
||||||
|
lines.append("first_target_divergences:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.tx_target_divergences)
|
||||||
|
if self.report_queue_traces:
|
||||||
|
lines.append("recent_report_queue:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.report_queue_traces)
|
||||||
|
if self.report_queue_first_nonzero_writes:
|
||||||
|
lines.append("first_report_queue_nonzero_writes:")
|
||||||
|
shown = self.report_queue_first_nonzero_writes[:REPORT_QUEUE_FIRST_NONZERO_DISPLAY_LIMIT]
|
||||||
|
lines.extend(" " + trace.line() for trace in shown)
|
||||||
|
remaining = len(self.report_queue_first_nonzero_writes) - len(shown)
|
||||||
|
if remaining > 0:
|
||||||
|
lines.append(f" ... {remaining} more first nonzero queue writes")
|
||||||
|
if self.report_queue_watch_hits:
|
||||||
|
lines.append("report_queue_watch_hits:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.report_queue_watch_hits)
|
||||||
|
if self.report_gate_traces:
|
||||||
|
lines.append("recent_report_gates:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.report_gate_traces)
|
||||||
|
if self.ram_lifecycle_traces:
|
||||||
|
lines.append("recent_ram_lifecycle:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.ram_lifecycle_traces)
|
||||||
|
if self.ram_lifecycle_last_writes:
|
||||||
|
lines.append("ram_lifecycle_last_writes:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.ram_lifecycle_last_writes)
|
||||||
|
if self.ram_lifecycle_last_nonzero_writes:
|
||||||
|
lines.append("ram_lifecycle_last_nonzero_writes:")
|
||||||
|
lines.extend(" " + trace.line() for trace in self.ram_lifecycle_last_nonzero_writes)
|
||||||
if self.watch_snapshots:
|
if self.watch_snapshots:
|
||||||
lines.append("recent_watch_snapshots:")
|
lines.append("recent_watch_snapshots:")
|
||||||
lines.extend(" " + snapshot.line() for snapshot in self.watch_snapshots)
|
lines.extend(" " + snapshot.line() for snapshot in self.watch_snapshots)
|
||||||
@@ -238,6 +591,308 @@ def _watch_snapshot(emulator: H8536Emulator, *, stack_words: int = 6) -> WatchSn
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_window(emulator: H8536Emulator, start: int, length: int) -> bytes:
|
||||||
|
offset = start - ON_CHIP_RAM_START
|
||||||
|
return bytes(emulator.memory.ram[offset : offset + length])
|
||||||
|
|
||||||
|
|
||||||
|
def _tx_frame_checksum(frame: bytes) -> int:
|
||||||
|
checksum = 0x5A
|
||||||
|
for byte in frame[: TX_FRAME_LENGTH - 1]:
|
||||||
|
checksum ^= byte
|
||||||
|
return checksum & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def _tx_frame_snapshot(emulator: H8536Emulator, label: str) -> TXFrameSnapshot:
|
||||||
|
data = _ram_window(emulator, TX_FRAME_SNAPSHOT_START, 0x0E)
|
||||||
|
frame = data[TX_FRAME_START - TX_FRAME_SNAPSHOT_START :]
|
||||||
|
computed = _tx_frame_checksum(frame)
|
||||||
|
stored = frame[TX_FRAME_LENGTH - 1]
|
||||||
|
return TXFrameSnapshot(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=emulator.cpu.pc & 0xFFFF,
|
||||||
|
label=label,
|
||||||
|
bytes_f850_f85d=data,
|
||||||
|
computed_checksum=computed,
|
||||||
|
stored_checksum=stored,
|
||||||
|
checksum_ok=computed == stored,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _watched_frame_values(emulator: H8536Emulator) -> dict[int, int]:
|
||||||
|
data = _ram_window(emulator, TX_FRAME_TRACE_START, TX_FRAME_TRACE_END - TX_FRAME_TRACE_START + 1)
|
||||||
|
return {TX_FRAME_TRACE_START + index: value for index, value in enumerate(data)}
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_from_watched_values(values: dict[int, int]) -> bytes:
|
||||||
|
return bytes(values.get(TX_FRAME_START + index, 0) & 0xFF for index in range(TX_FRAME_LENGTH))
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_depth(head: int, tail: int) -> int:
|
||||||
|
return (head - tail) & 0x7F
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_index(address: int) -> int | None:
|
||||||
|
if not REPORT_QUEUE_START <= address <= REPORT_QUEUE_END:
|
||||||
|
return None
|
||||||
|
return (address - REPORT_QUEUE_START) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def _watched_report_queue_values(emulator: H8536Emulator) -> dict[int, int]:
|
||||||
|
data = _ram_window(emulator, REPORT_QUEUE_START, REPORT_QUEUE_END - REPORT_QUEUE_START + 1)
|
||||||
|
values = {REPORT_QUEUE_START + index: value for index, value in enumerate(data)}
|
||||||
|
values[REPORT_QUEUE_HEAD] = _ram_window(emulator, REPORT_QUEUE_HEAD, 1)[0]
|
||||||
|
values[REPORT_QUEUE_TAIL] = _ram_window(emulator, REPORT_QUEUE_TAIL, 1)[0]
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_word(values: dict[int, int], queue_index: int) -> int:
|
||||||
|
address = REPORT_QUEUE_START + ((queue_index & 0x7F) * 2)
|
||||||
|
return ((values.get(address, 0) & 0xFF) << 8) | (values.get(address + 1, 0) & 0xFF)
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_state(values: dict[int, int]) -> tuple[int, int]:
|
||||||
|
return values.get(REPORT_QUEUE_HEAD, 0) & 0x7F, values.get(REPORT_QUEUE_TAIL, 0) & 0x7F
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_entry_trace(emulator: H8536Emulator, values: dict[int, int], label: str) -> ReportQueueTrace:
|
||||||
|
head, tail = _report_queue_state(values)
|
||||||
|
queue_index = head if label == "enqueue_entry" else tail
|
||||||
|
candidate = emulator.cpu.regs[3] if label == "enqueue_entry" else _report_queue_word(values, queue_index)
|
||||||
|
return ReportQueueTrace(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=emulator.cpu.pc & 0xFFFF,
|
||||||
|
kind=label,
|
||||||
|
head=head,
|
||||||
|
tail=tail,
|
||||||
|
queue_index=queue_index,
|
||||||
|
old_word=_report_queue_word(values, queue_index),
|
||||||
|
candidate_word=candidate & 0xFFFF,
|
||||||
|
instruction="<entry>",
|
||||||
|
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_access_traces(
|
||||||
|
emulator: H8536Emulator,
|
||||||
|
*,
|
||||||
|
pc: int,
|
||||||
|
instruction: str,
|
||||||
|
accesses: list,
|
||||||
|
values: dict[int, int],
|
||||||
|
) -> list[ReportQueueTrace]:
|
||||||
|
queue_accesses: dict[int, dict[str, object]] = {}
|
||||||
|
traces: list[ReportQueueTrace] = []
|
||||||
|
regs = tuple(register & 0xFFFF for register in emulator.cpu.regs)
|
||||||
|
|
||||||
|
for access in accesses:
|
||||||
|
queue_index = _report_queue_index(access.address)
|
||||||
|
if queue_index is not None:
|
||||||
|
group = queue_accesses.setdefault(
|
||||||
|
queue_index,
|
||||||
|
{
|
||||||
|
"address": REPORT_QUEUE_START + queue_index * 2,
|
||||||
|
"old_word": _report_queue_word(values, queue_index),
|
||||||
|
"read": False,
|
||||||
|
"write": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
group[access.kind] = True
|
||||||
|
if access.kind == "write":
|
||||||
|
values[access.address] = access.value & 0xFF
|
||||||
|
continue
|
||||||
|
|
||||||
|
if access.kind != "write" or access.address not in (REPORT_QUEUE_HEAD, REPORT_QUEUE_TAIL):
|
||||||
|
continue
|
||||||
|
|
||||||
|
old_value = values.get(access.address, 0) & 0xFF
|
||||||
|
values[access.address] = access.value & 0xFF
|
||||||
|
head, tail = _report_queue_state(values)
|
||||||
|
traces.append(
|
||||||
|
ReportQueueTrace(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=pc,
|
||||||
|
kind="cursor_head_write" if access.address == REPORT_QUEUE_HEAD else "cursor_tail_write",
|
||||||
|
address=access.address,
|
||||||
|
old_value=old_value,
|
||||||
|
new_value=access.value & 0xFF,
|
||||||
|
head=head,
|
||||||
|
tail=tail,
|
||||||
|
instruction=instruction,
|
||||||
|
regs=regs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for queue_index, group in sorted(queue_accesses.items()):
|
||||||
|
head, tail = _report_queue_state(values)
|
||||||
|
write = bool(group["write"])
|
||||||
|
traces.append(
|
||||||
|
ReportQueueTrace(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=pc,
|
||||||
|
kind="queue_write" if write else "queue_read",
|
||||||
|
address=int(group["address"]),
|
||||||
|
queue_index=queue_index,
|
||||||
|
old_word=int(group["old_word"]),
|
||||||
|
new_word=_report_queue_word(values, queue_index) if write else None,
|
||||||
|
head=head,
|
||||||
|
tail=tail,
|
||||||
|
instruction=instruction,
|
||||||
|
regs=regs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return traces
|
||||||
|
|
||||||
|
|
||||||
|
def _report_queue_trace_matches_watch(trace: ReportQueueTrace, watch_ids: set[int]) -> bool:
|
||||||
|
if not watch_ids:
|
||||||
|
return False
|
||||||
|
candidates = (trace.old_word, trace.new_word, trace.candidate_word)
|
||||||
|
return any(value is not None and (value & 0xFFFF) in watch_ids for value in candidates)
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_byte(emulator: H8536Emulator, address: int) -> int:
|
||||||
|
return _ram_window(emulator, address, 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_memory_byte(emulator: H8536Emulator, address: int) -> int:
|
||||||
|
address &= 0xFFFF
|
||||||
|
if ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
|
||||||
|
return emulator.memory.ram[address - ON_CHIP_RAM_START]
|
||||||
|
if REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
|
||||||
|
return emulator.memory.registers[address - REGISTER_FIELD_START]
|
||||||
|
if address in emulator.memory.external:
|
||||||
|
return emulator.memory.external[address]
|
||||||
|
if emulator.memory.rom.contains(address):
|
||||||
|
return emulator.memory.rom.u8(address)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_lifecycle_watch_names(extra_addresses: list[int] | tuple[int, ...]) -> dict[int, str]:
|
||||||
|
names = dict(RAM_LIFECYCLE_DEFAULT_WATCHES)
|
||||||
|
for address in extra_addresses:
|
||||||
|
normalized = address & 0xFFFF
|
||||||
|
names.setdefault(normalized, f"ram_{normalized:04X}")
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_lifecycle_access_traces(
|
||||||
|
emulator: H8536Emulator,
|
||||||
|
*,
|
||||||
|
pc: int,
|
||||||
|
instruction: str,
|
||||||
|
accesses: list,
|
||||||
|
values: dict[int, int],
|
||||||
|
watch_names: dict[int, str],
|
||||||
|
) -> list[RAMLifecycleTrace]:
|
||||||
|
traces: list[RAMLifecycleTrace] = []
|
||||||
|
regs = tuple(register & 0xFFFF for register in emulator.cpu.regs)
|
||||||
|
for access in accesses:
|
||||||
|
address = access.address & 0xFFFF
|
||||||
|
if access.kind != "write" or address not in watch_names:
|
||||||
|
continue
|
||||||
|
old_value = values.get(address, 0) & 0xFF
|
||||||
|
new_value = access.value & 0xFF
|
||||||
|
values[address] = new_value
|
||||||
|
traces.append(
|
||||||
|
RAMLifecycleTrace(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=pc,
|
||||||
|
address=address,
|
||||||
|
name=watch_names[address],
|
||||||
|
old_value=old_value,
|
||||||
|
new_value=new_value,
|
||||||
|
instruction=instruction,
|
||||||
|
regs=regs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return traces
|
||||||
|
|
||||||
|
|
||||||
|
def _report_gate_decision(pc: int, *, f9c4: int, faa5: int, f9c3: int, head: int, tail: int, z: bool) -> str:
|
||||||
|
if pc == 0x4046:
|
||||||
|
return "f9c4_zero_continue" if f9c4 == 0 else "f9c4_nonzero_return"
|
||||||
|
if pc == 0x404A:
|
||||||
|
return "return_f9c4_nonzero" if not z else "continue_f9c4_zero"
|
||||||
|
if pc == 0x404C:
|
||||||
|
return "faa5_bit7_clear_skips_f9c3" if not (faa5 & 0x80) else "faa5_bit7_set_check_f9c3"
|
||||||
|
if pc == 0x4050:
|
||||||
|
return "enqueue_candidate_faa5_clear" if z else "check_f9c3_faa5_set"
|
||||||
|
if pc == 0x4052:
|
||||||
|
return "f9c3_zero_enqueue" if f9c3 == 0 else "f9c3_nonzero_return"
|
||||||
|
if pc == 0x4056:
|
||||||
|
return "enqueue_candidate_f9c3_zero" if z else "return_f9c3_nonzero"
|
||||||
|
if pc == 0x4059:
|
||||||
|
return "check_empty_queue"
|
||||||
|
if pc == 0x405F:
|
||||||
|
return "queue_empty" if head == tail else "queue_not_empty"
|
||||||
|
if pc == 0x4063:
|
||||||
|
return "return_queue_not_empty" if not z else "enqueue_zero_report"
|
||||||
|
if pc == 0x4067:
|
||||||
|
return "write_report_00ff_to_queue_slot"
|
||||||
|
if pc == 0x406C:
|
||||||
|
return "advance_report_queue_head"
|
||||||
|
if pc == 0x4070:
|
||||||
|
return "mask_report_queue_head_bit7"
|
||||||
|
if pc in (0x4058, 0x4074):
|
||||||
|
return "return"
|
||||||
|
return "snapshot"
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_lifecycle_age(emulator: H8536Emulator, trace: RAMLifecycleTrace | None) -> int | None:
|
||||||
|
if trace is None:
|
||||||
|
return None
|
||||||
|
return emulator.cpu.steps - trace.step
|
||||||
|
|
||||||
|
|
||||||
|
def _report_gate_trace(
|
||||||
|
emulator: H8536Emulator,
|
||||||
|
label: str,
|
||||||
|
*,
|
||||||
|
last_ram_lifecycle_writes: dict[int, RAMLifecycleTrace] | None = None,
|
||||||
|
last_ram_lifecycle_nonzero_writes: dict[int, RAMLifecycleTrace] | None = None,
|
||||||
|
) -> ReportGateTrace:
|
||||||
|
f9c4 = _ram_byte(emulator, REPORT_GATE_F9C4)
|
||||||
|
faa5 = _ram_byte(emulator, REPORT_GATE_FAA5)
|
||||||
|
f9c3 = _ram_byte(emulator, REPORT_GATE_F9C3)
|
||||||
|
head = _ram_byte(emulator, REPORT_QUEUE_HEAD) & 0x7F
|
||||||
|
tail = _ram_byte(emulator, REPORT_QUEUE_TAIL) & 0x7F
|
||||||
|
pc = emulator.cpu.pc & 0xFFFF
|
||||||
|
last_f9c4 = (last_ram_lifecycle_writes or {}).get(REPORT_GATE_F9C4)
|
||||||
|
last_nonzero_f9c4 = (last_ram_lifecycle_nonzero_writes or {}).get(REPORT_GATE_F9C4)
|
||||||
|
return ReportGateTrace(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=pc,
|
||||||
|
label=label,
|
||||||
|
f9c4=f9c4,
|
||||||
|
faa5=faa5,
|
||||||
|
f9c3=f9c3,
|
||||||
|
head=head,
|
||||||
|
tail=tail,
|
||||||
|
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
|
||||||
|
z=emulator.cpu.z,
|
||||||
|
c=emulator.cpu.c,
|
||||||
|
n=emulator.cpu.n,
|
||||||
|
decision=_report_gate_decision(
|
||||||
|
pc,
|
||||||
|
f9c4=f9c4,
|
||||||
|
faa5=faa5,
|
||||||
|
f9c3=f9c3,
|
||||||
|
head=head,
|
||||||
|
tail=tail,
|
||||||
|
z=emulator.cpu.z,
|
||||||
|
),
|
||||||
|
f9c4_last_write_step=last_f9c4.step if last_f9c4 is not None else None,
|
||||||
|
f9c4_last_write_pc=last_f9c4.pc if last_f9c4 is not None else None,
|
||||||
|
f9c4_last_write_value=last_f9c4.new_value if last_f9c4 is not None else None,
|
||||||
|
f9c4_last_write_age=_ram_lifecycle_age(emulator, last_f9c4),
|
||||||
|
f9c4_last_nonzero_step=last_nonzero_f9c4.step if last_nonzero_f9c4 is not None else None,
|
||||||
|
f9c4_last_nonzero_pc=last_nonzero_f9c4.pc if last_nonzero_f9c4 is not None else None,
|
||||||
|
f9c4_last_nonzero_value=last_nonzero_f9c4.new_value if last_nonzero_f9c4 is not None else None,
|
||||||
|
f9c4_last_nonzero_age=_ram_lifecycle_age(emulator, last_nonzero_f9c4),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_probe(
|
def run_probe(
|
||||||
rom_bytes: bytes,
|
rom_bytes: bytes,
|
||||||
*,
|
*,
|
||||||
@@ -254,6 +909,20 @@ def run_probe(
|
|||||||
watch_snapshot_limit: int = 32,
|
watch_snapshot_limit: int = 32,
|
||||||
watch_pc_limit: int = 8,
|
watch_pc_limit: int = 8,
|
||||||
watch_min_interval: int = 1024,
|
watch_min_interval: int = 1024,
|
||||||
|
tx_frame_watch: bool = True,
|
||||||
|
tx_frame_snapshot_limit: int = 32,
|
||||||
|
trace_frame_sources: bool = False,
|
||||||
|
frame_write_trace_limit: int = 64,
|
||||||
|
target_frame: bytes | None = None,
|
||||||
|
trace_report_queue: bool = False,
|
||||||
|
report_queue_trace_limit: int = 64,
|
||||||
|
watch_report_ids: list[int] | tuple[int, ...] = (),
|
||||||
|
report_queue_watch_hit_limit: int = 32,
|
||||||
|
trace_report_gates: bool = False,
|
||||||
|
report_gate_trace_limit: int = 64,
|
||||||
|
trace_ram_lifecycle: bool = False,
|
||||||
|
ram_watch_addresses: list[int] | tuple[int, ...] = (),
|
||||||
|
ram_lifecycle_trace_limit: int = 64,
|
||||||
) -> ProbeReport:
|
) -> ProbeReport:
|
||||||
emulator = H8536Emulator(
|
emulator = H8536Emulator(
|
||||||
rom_bytes,
|
rom_bytes,
|
||||||
@@ -267,6 +936,25 @@ def run_probe(
|
|||||||
p9_accesses: list[str] = []
|
p9_accesses: list[str] = []
|
||||||
sci_accesses: list[str] = []
|
sci_accesses: list[str] = []
|
||||||
snapshots: list[WatchSnapshot] = []
|
snapshots: list[WatchSnapshot] = []
|
||||||
|
tx_frame_snapshots: list[TXFrameSnapshot] = []
|
||||||
|
tx_frame_write_traces: list[TXFrameWriteTrace] = []
|
||||||
|
first_target_divergences: dict[int, TXFrameWriteTrace] = {}
|
||||||
|
watched_frame_values = _watched_frame_values(emulator)
|
||||||
|
report_queue_traces: list[ReportQueueTrace] = []
|
||||||
|
first_report_queue_writes: dict[int, ReportQueueTrace] = {}
|
||||||
|
first_report_queue_nonzero_writes: dict[int, ReportQueueTrace] = {}
|
||||||
|
report_queue_watch_hits: list[ReportQueueTrace] = []
|
||||||
|
report_gate_traces: list[ReportGateTrace] = []
|
||||||
|
ram_lifecycle_traces: list[RAMLifecycleTrace] = []
|
||||||
|
last_ram_lifecycle_writes: dict[int, RAMLifecycleTrace] = {}
|
||||||
|
last_ram_lifecycle_nonzero_writes: dict[int, RAMLifecycleTrace] = {}
|
||||||
|
ram_lifecycle_watch_names = _ram_lifecycle_watch_names(ram_watch_addresses)
|
||||||
|
ram_lifecycle_values = {
|
||||||
|
address: _raw_memory_byte(emulator, address) for address in ram_lifecycle_watch_names
|
||||||
|
}
|
||||||
|
watch_report_id_set = {value & 0xFFFF for value in watch_report_ids}
|
||||||
|
watched_report_queue_values = _watched_report_queue_values(emulator)
|
||||||
|
tx_builder_seen = False
|
||||||
watch_set = set(DEFAULT_WATCH_PCS if watch_pcs is None else watch_pcs)
|
watch_set = set(DEFAULT_WATCH_PCS if watch_pcs is None else watch_pcs)
|
||||||
watch_counts: Counter[int] = Counter()
|
watch_counts: Counter[int] = Counter()
|
||||||
watch_last_step: dict[int, int] = {}
|
watch_last_step: dict[int, int] = {}
|
||||||
@@ -277,6 +965,34 @@ def run_probe(
|
|||||||
for _ in range(max_steps):
|
for _ in range(max_steps):
|
||||||
pc = emulator.cpu.pc
|
pc = emulator.cpu.pc
|
||||||
hot_pcs[pc] += 1
|
hot_pcs[pc] += 1
|
||||||
|
if pc == 0xBA26:
|
||||||
|
tx_builder_seen = True
|
||||||
|
if trace_report_queue and pc in REPORT_QUEUE_ENTRY_PCS:
|
||||||
|
report_queue_traces.append(
|
||||||
|
_report_queue_entry_trace(emulator, watched_report_queue_values, REPORT_QUEUE_ENTRY_PCS[pc])
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
_report_queue_trace_matches_watch(report_queue_traces[-1], watch_report_id_set)
|
||||||
|
and len(report_queue_watch_hits) < report_queue_watch_hit_limit
|
||||||
|
):
|
||||||
|
report_queue_watch_hits.append(report_queue_traces[-1])
|
||||||
|
if len(report_queue_traces) > report_queue_trace_limit:
|
||||||
|
del report_queue_traces[: len(report_queue_traces) - report_queue_trace_limit]
|
||||||
|
if trace_report_gates and pc in REPORT_GATE_PCS:
|
||||||
|
report_gate_traces.append(
|
||||||
|
_report_gate_trace(
|
||||||
|
emulator,
|
||||||
|
REPORT_GATE_PCS[pc],
|
||||||
|
last_ram_lifecycle_writes=last_ram_lifecycle_writes,
|
||||||
|
last_ram_lifecycle_nonzero_writes=last_ram_lifecycle_nonzero_writes,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(report_gate_traces) > report_gate_trace_limit:
|
||||||
|
del report_gate_traces[: len(report_gate_traces) - report_gate_trace_limit]
|
||||||
|
if tx_frame_watch and pc in TX_FRAME_WATCH_PCS:
|
||||||
|
tx_frame_snapshots.append(_tx_frame_snapshot(emulator, TX_FRAME_WATCH_PCS[pc]))
|
||||||
|
if len(tx_frame_snapshots) > tx_frame_snapshot_limit:
|
||||||
|
del tx_frame_snapshots[: len(tx_frame_snapshots) - tx_frame_snapshot_limit]
|
||||||
if pc in watch_set and watch_counts[pc] < watch_pc_limit:
|
if pc in watch_set and watch_counts[pc] < watch_pc_limit:
|
||||||
last_step = watch_last_step.get(pc)
|
last_step = watch_last_step.get(pc)
|
||||||
if last_step is None or emulator.cpu.steps - last_step >= watch_min_interval:
|
if last_step is None or emulator.cpu.steps - last_step >= watch_min_interval:
|
||||||
@@ -287,13 +1003,91 @@ def run_probe(
|
|||||||
watch_last_step[pc] = emulator.cpu.steps
|
watch_last_step[pc] = emulator.cpu.steps
|
||||||
last_access_index = len(emulator.memory.access_log)
|
last_access_index = len(emulator.memory.access_log)
|
||||||
try:
|
try:
|
||||||
emulator.step()
|
instruction = emulator.step()
|
||||||
except UnsupportedInstruction as exc:
|
except UnsupportedInstruction as exc:
|
||||||
stopped_reason = "unsupported_instruction"
|
stopped_reason = "unsupported_instruction"
|
||||||
unsupported = str(exc)
|
unsupported = str(exc)
|
||||||
break
|
break
|
||||||
|
|
||||||
for access in emulator.memory.access_log[last_access_index:]:
|
recent_accesses = emulator.memory.access_log[last_access_index:]
|
||||||
|
if trace_report_queue:
|
||||||
|
new_report_queue_traces = _report_queue_access_traces(
|
||||||
|
emulator,
|
||||||
|
pc=pc,
|
||||||
|
instruction=instruction,
|
||||||
|
accesses=recent_accesses,
|
||||||
|
values=watched_report_queue_values,
|
||||||
|
)
|
||||||
|
for trace in new_report_queue_traces:
|
||||||
|
if trace.kind == "queue_write" and trace.queue_index not in first_report_queue_writes:
|
||||||
|
first_report_queue_writes[trace.queue_index] = trace
|
||||||
|
if (
|
||||||
|
trace.kind == "queue_write"
|
||||||
|
and trace.queue_index not in first_report_queue_nonzero_writes
|
||||||
|
and trace.new_word is not None
|
||||||
|
and trace.new_word != 0
|
||||||
|
):
|
||||||
|
first_report_queue_nonzero_writes[trace.queue_index] = trace
|
||||||
|
if (
|
||||||
|
_report_queue_trace_matches_watch(trace, watch_report_id_set)
|
||||||
|
and len(report_queue_watch_hits) < report_queue_watch_hit_limit
|
||||||
|
):
|
||||||
|
report_queue_watch_hits.append(trace)
|
||||||
|
report_queue_traces.extend(new_report_queue_traces)
|
||||||
|
if len(report_queue_traces) > report_queue_trace_limit:
|
||||||
|
del report_queue_traces[: len(report_queue_traces) - report_queue_trace_limit]
|
||||||
|
|
||||||
|
if trace_ram_lifecycle:
|
||||||
|
new_ram_lifecycle_traces = _ram_lifecycle_access_traces(
|
||||||
|
emulator,
|
||||||
|
pc=pc,
|
||||||
|
instruction=instruction,
|
||||||
|
accesses=recent_accesses,
|
||||||
|
values=ram_lifecycle_values,
|
||||||
|
watch_names=ram_lifecycle_watch_names,
|
||||||
|
)
|
||||||
|
for trace in new_ram_lifecycle_traces:
|
||||||
|
last_ram_lifecycle_writes[trace.address] = trace
|
||||||
|
if trace.new_value != 0:
|
||||||
|
last_ram_lifecycle_nonzero_writes[trace.address] = trace
|
||||||
|
ram_lifecycle_traces.extend(new_ram_lifecycle_traces)
|
||||||
|
if len(ram_lifecycle_traces) > ram_lifecycle_trace_limit:
|
||||||
|
del ram_lifecycle_traces[: len(ram_lifecycle_traces) - ram_lifecycle_trace_limit]
|
||||||
|
|
||||||
|
for access in recent_accesses:
|
||||||
|
if (
|
||||||
|
trace_frame_sources
|
||||||
|
and access.kind == "write"
|
||||||
|
and TX_FRAME_TRACE_START <= access.address <= TX_FRAME_TRACE_END
|
||||||
|
):
|
||||||
|
old_value = watched_frame_values.get(access.address, 0)
|
||||||
|
watched_frame_values[access.address] = access.value & 0xFF
|
||||||
|
target_value = None
|
||||||
|
if target_frame is not None and TX_FRAME_START <= access.address < TX_FRAME_START + TX_FRAME_LENGTH:
|
||||||
|
target_value = target_frame[access.address - TX_FRAME_START]
|
||||||
|
tx_frame_write_traces.append(
|
||||||
|
TXFrameWriteTrace(
|
||||||
|
step=emulator.cpu.steps,
|
||||||
|
pc=pc,
|
||||||
|
address=access.address,
|
||||||
|
old_value=old_value,
|
||||||
|
new_value=access.value,
|
||||||
|
frame_after=_frame_from_watched_values(watched_frame_values),
|
||||||
|
instruction=instruction,
|
||||||
|
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
|
||||||
|
target_value=target_value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
trace = tx_frame_write_traces[-1]
|
||||||
|
if (
|
||||||
|
tx_builder_seen
|
||||||
|
and target_value is not None
|
||||||
|
and access.value != target_value
|
||||||
|
and access.address not in first_target_divergences
|
||||||
|
):
|
||||||
|
first_target_divergences[access.address] = trace
|
||||||
|
if len(tx_frame_write_traces) > frame_write_trace_limit:
|
||||||
|
del tx_frame_write_traces[: len(tx_frame_write_traces) - frame_write_trace_limit]
|
||||||
if access.address in (P9DDR, P9DR):
|
if access.address in (P9DDR, P9DR):
|
||||||
p9_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}")
|
p9_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}")
|
||||||
if len(p9_accesses) > p9_log_limit:
|
if len(p9_accesses) > p9_log_limit:
|
||||||
@@ -328,6 +1122,25 @@ def run_probe(
|
|||||||
sci1=_sci1_snapshot(emulator),
|
sci1=_sci1_snapshot(emulator),
|
||||||
sci1_txi=_sci1_txi_summary(emulator),
|
sci1_txi=_sci1_txi_summary(emulator),
|
||||||
watch_snapshots=snapshots,
|
watch_snapshots=snapshots,
|
||||||
|
tx_frame_snapshots=tx_frame_snapshots,
|
||||||
|
tx_frame_write_traces=tx_frame_write_traces,
|
||||||
|
tx_target_divergences=[first_target_divergences[address] for address in sorted(first_target_divergences)],
|
||||||
|
report_queue_traces=report_queue_traces,
|
||||||
|
report_queue_first_writes=[first_report_queue_writes[index] for index in sorted(first_report_queue_writes)],
|
||||||
|
report_queue_first_nonzero_writes=[
|
||||||
|
first_report_queue_nonzero_writes[index] for index in sorted(first_report_queue_nonzero_writes)
|
||||||
|
],
|
||||||
|
report_queue_watch_hits=report_queue_watch_hits,
|
||||||
|
report_gate_traces=report_gate_traces,
|
||||||
|
ram_lifecycle_traces=ram_lifecycle_traces,
|
||||||
|
ram_lifecycle_last_writes=[
|
||||||
|
last_ram_lifecycle_writes[address] for address in sorted(last_ram_lifecycle_writes)
|
||||||
|
],
|
||||||
|
ram_lifecycle_last_nonzero_writes=[
|
||||||
|
last_ram_lifecycle_nonzero_writes[address] for address in sorted(last_ram_lifecycle_nonzero_writes)
|
||||||
|
],
|
||||||
|
final_tx_frame=_ram_window(emulator, TX_FRAME_START, TX_FRAME_LENGTH),
|
||||||
|
target_frame=target_frame,
|
||||||
unsupported=unsupported,
|
unsupported=unsupported,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -355,6 +1168,57 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|||||||
parser.add_argument("--watch-snapshot-limit", type=int, default=32)
|
parser.add_argument("--watch-snapshot-limit", type=int, default=32)
|
||||||
parser.add_argument("--watch-pc-limit", type=int, default=8)
|
parser.add_argument("--watch-pc-limit", type=int, default=8)
|
||||||
parser.add_argument("--watch-min-interval", type=int, default=1024)
|
parser.add_argument("--watch-min-interval", type=int, default=1024)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tx-frame-watch",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=True,
|
||||||
|
help="snapshot F850-F85D at known TX builder/send PCs",
|
||||||
|
)
|
||||||
|
parser.add_argument("--tx-frame-snapshot-limit", type=int, default=32)
|
||||||
|
parser.add_argument(
|
||||||
|
"--trace-frame-sources",
|
||||||
|
action="store_true",
|
||||||
|
help="trace writes to TX staging/frame RAM H'F850-H'F85D with PC/register provenance",
|
||||||
|
)
|
||||||
|
parser.add_argument("--frame-write-trace-limit", type=int, default=64)
|
||||||
|
parser.add_argument(
|
||||||
|
"--target-frame",
|
||||||
|
type=parse_tx_frame,
|
||||||
|
help="6-byte frame to compare against, e.g. \"00 00 00 00 80 DA\" or 0000000080DA",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--trace-report-queue",
|
||||||
|
action="store_true",
|
||||||
|
help="trace autonomous report queue entries/cursors at H'F870-H'F96F, H'F9B0, and H'F9B5",
|
||||||
|
)
|
||||||
|
parser.add_argument("--report-queue-trace-limit", type=int, default=64)
|
||||||
|
parser.add_argument(
|
||||||
|
"--watch-report-id",
|
||||||
|
action="append",
|
||||||
|
type=parse_watch_pc,
|
||||||
|
default=[],
|
||||||
|
help="highlight queue traces involving a logical report id, e.g. 00FF or 0x00FF",
|
||||||
|
)
|
||||||
|
parser.add_argument("--report-queue-watch-hit-limit", type=int, default=32)
|
||||||
|
parser.add_argument(
|
||||||
|
"--trace-report-gates",
|
||||||
|
action="store_true",
|
||||||
|
help="trace loc_4046 report enqueue gates: F9C4, FAA5.bit7, F9C3, and F9B0/F9B5",
|
||||||
|
)
|
||||||
|
parser.add_argument("--report-gate-trace-limit", type=int, default=64)
|
||||||
|
parser.add_argument(
|
||||||
|
"--trace-ram-lifecycle",
|
||||||
|
action="store_true",
|
||||||
|
help="trace writes to key RAM lifecycle bytes such as F9C4/F9C3/FAA5/F9B0/F9B5",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ram-watch",
|
||||||
|
action="append",
|
||||||
|
type=parse_watch_pc,
|
||||||
|
default=[],
|
||||||
|
help="additional byte address to include in RAM lifecycle tracing, e.g. F9C4 or 0xF9C4",
|
||||||
|
)
|
||||||
|
parser.add_argument("--ram-lifecycle-trace-limit", type=int, default=64)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -381,6 +1245,20 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
watch_snapshot_limit=args.watch_snapshot_limit,
|
watch_snapshot_limit=args.watch_snapshot_limit,
|
||||||
watch_pc_limit=args.watch_pc_limit,
|
watch_pc_limit=args.watch_pc_limit,
|
||||||
watch_min_interval=args.watch_min_interval,
|
watch_min_interval=args.watch_min_interval,
|
||||||
|
tx_frame_watch=args.tx_frame_watch,
|
||||||
|
tx_frame_snapshot_limit=args.tx_frame_snapshot_limit,
|
||||||
|
trace_frame_sources=args.trace_frame_sources or args.target_frame is not None,
|
||||||
|
frame_write_trace_limit=args.frame_write_trace_limit,
|
||||||
|
target_frame=args.target_frame,
|
||||||
|
trace_report_queue=args.trace_report_queue,
|
||||||
|
report_queue_trace_limit=args.report_queue_trace_limit,
|
||||||
|
watch_report_ids=tuple(args.watch_report_id),
|
||||||
|
report_queue_watch_hit_limit=args.report_queue_watch_hit_limit,
|
||||||
|
trace_report_gates=args.trace_report_gates,
|
||||||
|
report_gate_trace_limit=args.report_gate_trace_limit,
|
||||||
|
trace_ram_lifecycle=args.trace_ram_lifecycle,
|
||||||
|
ram_watch_addresses=tuple(args.ram_watch),
|
||||||
|
ram_lifecycle_trace_limit=args.ram_lifecycle_trace_limit,
|
||||||
)
|
)
|
||||||
for line in report.lines(hot_limit=args.hot_limit):
|
for line in report.lines(hot_limit=args.hot_limit):
|
||||||
print(line)
|
print(line)
|
||||||
|
|||||||
@@ -278,15 +278,17 @@ class H8536Emulator:
|
|||||||
self._set_logic_flags(result, size)
|
self._set_logic_flags(result, size)
|
||||||
elif op == 0x16:
|
elif op == 0x16:
|
||||||
self._set_logic_flags(self._read_ea(ea, size), size)
|
self._set_logic_flags(self._read_ea(ea, size), size)
|
||||||
elif op == 0x10:
|
elif op in (0x10, 0x11, 0x12) and ea["mode"] == "reg" and size == 1:
|
||||||
value = self._read_ea(ea, size)
|
reg = int(ea["reg"])
|
||||||
result = ((value & 0xFF) << 8) | ((value >> 8) & 0xFF)
|
value = self.cpu.regs[reg] & 0xFFFF
|
||||||
self._write_ea(ea, result, 2)
|
if op == 0x10:
|
||||||
|
result = ((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF)
|
||||||
|
elif op == 0x11:
|
||||||
|
result = s8(value & 0xFF) & 0xFFFF
|
||||||
|
else:
|
||||||
|
result = value & 0x00FF
|
||||||
|
self.cpu.regs[reg] = result
|
||||||
self._set_logic_flags(result, 2)
|
self._set_logic_flags(result, 2)
|
||||||
elif op == 0x12:
|
|
||||||
value = self._read_ea(ea, size) & 0xFF
|
|
||||||
self._write_ea(ea, value, 2 if size == 2 else 1)
|
|
||||||
self._set_logic_flags(value, size)
|
|
||||||
elif op == 0x1A:
|
elif op == 0x1A:
|
||||||
value = self._read_ea(ea, size)
|
value = self._read_ea(ea, size)
|
||||||
result = (value << 1) & mask(size)
|
result = (value << 1) & mask(size)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ KEY_STATE_ADDRESSES: tuple[int, ...] = (
|
|||||||
0xF9C0,
|
0xF9C0,
|
||||||
0xF9C1,
|
0xF9C1,
|
||||||
0xF9C3,
|
0xF9C3,
|
||||||
|
0xF9C4,
|
||||||
0xF9C5,
|
0xF9C5,
|
||||||
0xF9C6,
|
0xF9C6,
|
||||||
0xF9C8,
|
0xF9C8,
|
||||||
@@ -52,6 +53,7 @@ def analyze_serial_gate(payload: dict[str, Any]) -> JsonObject:
|
|||||||
"queue_send_gate_loc_BAF2": _queue_send_gate(by_address),
|
"queue_send_gate_loc_BAF2": _queue_send_gate(by_address),
|
||||||
"resend_gate_path": _resend_gate_path(by_address),
|
"resend_gate_path": _resend_gate_path(by_address),
|
||||||
"rx_session_maintenance": _rx_session_maintenance(by_address),
|
"rx_session_maintenance": _rx_session_maintenance(by_address),
|
||||||
|
"idle_heartbeat_gate_loc_4046": _idle_heartbeat_gate(payload, by_address),
|
||||||
"timer_tick_evidence": _timer_tick_evidence(payload, by_address),
|
"timer_tick_evidence": _timer_tick_evidence(payload, by_address),
|
||||||
}
|
}
|
||||||
access_summary = _state_access_summary(instructions, labels)
|
access_summary = _state_access_summary(instructions, labels)
|
||||||
@@ -101,6 +103,15 @@ def format_text_report(analysis: dict[str, Any]) -> str:
|
|||||||
lines.append(" Candidate timer roles:")
|
lines.append(" Candidate timer roles:")
|
||||||
for role in roles:
|
for role in roles:
|
||||||
lines.append(f" - {role['address_hex']}: {role['role']}")
|
lines.append(f" - {role['address_hex']}: {role['role']}")
|
||||||
|
timer = section.get("timer")
|
||||||
|
if isinstance(timer, dict):
|
||||||
|
source = timer.get("source")
|
||||||
|
handler = timer.get("handler_address_hex")
|
||||||
|
ocra = timer.get("ocra_value_hex")
|
||||||
|
period = timer.get("observed_period_ms_candidate")
|
||||||
|
timer_bits = [str(part) for part in (source, handler, f"OCRA={ocra}" if ocra else "", f"observed period ~= {period}ms" if period else "") if part]
|
||||||
|
if timer_bits:
|
||||||
|
lines.append(f" Timer: {', '.join(timer_bits)}")
|
||||||
|
|
||||||
lines.extend(["", "State address readers/writers:"])
|
lines.extend(["", "State address readers/writers:"])
|
||||||
for entry in analysis.get("state_accesses", []):
|
for entry in analysis.get("state_accesses", []):
|
||||||
@@ -273,6 +284,76 @@ def _rx_session_maintenance(by_address: dict[int, JsonObject]) -> JsonObject:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _idle_heartbeat_gate(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
|
||||||
|
vector = _vector_entry(payload, 0x006A, "frt2_ocia")
|
||||||
|
handler = _int_field(vector, "target") if vector else None
|
||||||
|
if handler is None:
|
||||||
|
handler = 0xBF23
|
||||||
|
addresses = [
|
||||||
|
0x4046,
|
||||||
|
0x404A,
|
||||||
|
0x404C,
|
||||||
|
0x4050,
|
||||||
|
0x4052,
|
||||||
|
0x4056,
|
||||||
|
0x4058,
|
||||||
|
0x4059,
|
||||||
|
0x405F,
|
||||||
|
0x4063,
|
||||||
|
0x4067,
|
||||||
|
0x406C,
|
||||||
|
0x4070,
|
||||||
|
0x40E0,
|
||||||
|
0xBA31,
|
||||||
|
handler,
|
||||||
|
0xBF27,
|
||||||
|
0xBF2D,
|
||||||
|
]
|
||||||
|
present = _has_all(by_address, (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D))
|
||||||
|
return {
|
||||||
|
"title": "loc_4046 idle heartbeat/report gate",
|
||||||
|
"present": present,
|
||||||
|
"summary": (
|
||||||
|
"F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send "
|
||||||
|
"reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 "
|
||||||
|
"can enqueue H'00FF if the queue is empty and the FAA5/F9C3 RX gate permits it. With "
|
||||||
|
"FRT2 OCRA H'7A12 and CKS=phi/32, a phi near 10 MHz gives about 0.7s for H'07, matching "
|
||||||
|
"the observed heartbeat cadence."
|
||||||
|
),
|
||||||
|
"items": _items(by_address, addresses),
|
||||||
|
"gate_address_hex": h16(0x4046),
|
||||||
|
"queue_write_address_hex": h16(0x4067),
|
||||||
|
"initial_reload_address_hex": h16(0x40E0),
|
||||||
|
"post_tx_reload_address_hex": h16(0xBA31),
|
||||||
|
"tick_handler_address_hex": h16(handler),
|
||||||
|
"decrement_address_hex": h16(0xBF2D),
|
||||||
|
"initial_reload_value_hex": "H'14",
|
||||||
|
"post_tx_reload_value_hex": "H'07",
|
||||||
|
"timer": {
|
||||||
|
"source": "FRT2 OCIA",
|
||||||
|
"vector_address_hex": h16(0x006A),
|
||||||
|
"handler_address_hex": h16(handler),
|
||||||
|
"vector_target_label": str(vector.get("target_label", "")) if vector else "",
|
||||||
|
"tcr_address_hex": h16(0xFEA0),
|
||||||
|
"tcsr_address_hex": h16(0xFEA1),
|
||||||
|
"ocra_address_hex": h16(0xFEA4),
|
||||||
|
"ocra_value_hex": "H'7A12",
|
||||||
|
"clock_select": "CKS1=1 CKS0=0 => phi/32",
|
||||||
|
"observed_period_ms_candidate": 700,
|
||||||
|
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
|
||||||
|
},
|
||||||
|
"candidate_timer_roles": [
|
||||||
|
{
|
||||||
|
"address": 0xF9C4,
|
||||||
|
"address_hex": h16(0xF9C4),
|
||||||
|
"role": "candidate idle heartbeat/report gate countdown",
|
||||||
|
"evidence_address_hex": h16(0xBF2D),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"required_addresses_hex": [h16(address) for address in (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _timer_tick_evidence(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
|
def _timer_tick_evidence(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
|
||||||
vector = _vector_entry(payload, 0x0062, "frt1_ocia")
|
vector = _vector_entry(payload, 0x0062, "frt1_ocia")
|
||||||
handler = _int_field(vector, "target") if vector else None
|
handler = _int_field(vector, "target") if vector else None
|
||||||
|
|||||||
@@ -668,6 +668,15 @@ def _gate_queue_predicate_function_lines(value: object) -> list[str]:
|
|||||||
" return MEM8[0xF9B5u] != MEM8[0xF9B0u];",
|
" return MEM8[0xF9B5u] != MEM8[0xF9B0u];",
|
||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
|
"static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)",
|
||||||
|
"{",
|
||||||
|
" bool idle_timer_clear = MEM8[0xF9C4u] == 0u;",
|
||||||
|
" bool rx_gate_open = (MEM8[0xFAA5u] & 0x80u) == 0u || MEM8[0xF9C3u] == 0u;",
|
||||||
|
" bool queue_empty = MEM8[0xF9B0u] == MEM8[0xF9B5u];",
|
||||||
|
"",
|
||||||
|
" return idle_timer_clear && rx_gate_open && queue_empty;",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
"static bool sci1_candidate_periodic_resend_gate_open(void)",
|
"static bool sci1_candidate_periodic_resend_gate_open(void)",
|
||||||
"{",
|
"{",
|
||||||
" bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;",
|
" bool pending = (MEM8[0xFAA5u] & MEM8[0xFAA3u] & 0x80u) != 0u;",
|
||||||
@@ -759,12 +768,21 @@ def _timer_architecture_comment_lines(
|
|||||||
if not model:
|
if not model:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
vector = str(model.get("vector_address_hex") or "H'BEEA")
|
|
||||||
source = str(model.get("source") or "FRT1 OCIA")
|
|
||||||
lines = [f"{prefix}interrupt/timer architecture candidate:"]
|
lines = [f"{prefix}interrupt/timer architecture candidate:"]
|
||||||
lines.append(
|
sources = _timer_source_models(model)
|
||||||
f"{prefix}- {source} {vector} appears to be a periodic tick ISR for serial gate/cadence counters.",
|
if sources:
|
||||||
)
|
for source_model in sources:
|
||||||
|
source = str(source_model.get("source") or "timer")
|
||||||
|
handler = str(source_model.get("handler_address_hex") or source_model.get("vector_address_hex") or "")
|
||||||
|
details = f" {handler}" if handler else ""
|
||||||
|
summary = _comment_text(str(source_model.get("summary") or "appears to be a periodic tick ISR for serial counters."))
|
||||||
|
lines.append(f"{prefix}- {source}{details}: {summary}")
|
||||||
|
else:
|
||||||
|
vector = str(model.get("vector_address_hex") or model.get("handler_address_hex") or "H'BEEA")
|
||||||
|
source = str(model.get("source") or "FRT1 OCIA")
|
||||||
|
lines.append(
|
||||||
|
f"{prefix}- {source} {vector} appears to be a periodic tick ISR for serial gate/cadence counters.",
|
||||||
|
)
|
||||||
counters = _timer_counter_models(model)
|
counters = _timer_counter_models(model)
|
||||||
for counter in counters:
|
for counter in counters:
|
||||||
address = counter.get("address_hex") or _h(_int_field(counter, "address", 0))
|
address = counter.get("address_hex") or _h(_int_field(counter, "address", 0))
|
||||||
@@ -781,14 +799,39 @@ def _timer_architecture_function_lines(protocol: JsonObject) -> list[str]:
|
|||||||
model = _timer_architecture_model(protocol)
|
model = _timer_architecture_model(protocol)
|
||||||
if not model:
|
if not model:
|
||||||
return []
|
return []
|
||||||
|
sources = _timer_source_models(model)
|
||||||
|
if sources:
|
||||||
|
lines: list[str] = []
|
||||||
|
for source_model in sources:
|
||||||
|
counters = _timer_counter_models(source_model)
|
||||||
|
if not counters:
|
||||||
|
continue
|
||||||
|
source = str(source_model.get("source") or "timer")
|
||||||
|
handler = str(source_model.get("handler_address_hex") or source_model.get("vector_address_hex") or "")
|
||||||
|
lines.extend(
|
||||||
|
_timer_tick_function_lines(
|
||||||
|
_timer_source_function_name(source),
|
||||||
|
counters,
|
||||||
|
f"Candidate periodic tick at {handler or source}: decrement nonzero serial counters.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return lines
|
||||||
counters = _timer_counter_models(model)
|
counters = _timer_counter_models(model)
|
||||||
if not counters:
|
if not counters:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
return _timer_tick_function_lines(
|
||||||
|
"frt1_ocia_candidate_tick_isr",
|
||||||
|
counters,
|
||||||
|
"Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _timer_tick_function_lines(function_name: str, counters: list[JsonObject], summary: str) -> list[str]:
|
||||||
lines = [
|
lines = [
|
||||||
"void frt1_ocia_candidate_tick_isr(void)",
|
f"void {function_name}(void)",
|
||||||
"{",
|
"{",
|
||||||
" /* Candidate periodic tick at H'BEEA: decrement nonzero serial gate/cadence counters. */",
|
f" /* {_comment_text(summary)} */",
|
||||||
]
|
]
|
||||||
for counter in counters:
|
for counter in counters:
|
||||||
address = _int_field(counter, "address", 0)
|
address = _int_field(counter, "address", 0)
|
||||||
@@ -808,14 +851,23 @@ def _timer_architecture_function_lines(protocol: JsonObject) -> list[str]:
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _timer_source_models(model: JsonObject) -> list[JsonObject]:
|
||||||
|
return _object_list(model.get("sources"))
|
||||||
|
|
||||||
|
|
||||||
|
def _timer_source_function_name(source: str) -> str:
|
||||||
|
root = _safe_identifier(source.lower().replace(" ", "_"))
|
||||||
|
return f"{root}_candidate_tick_isr"
|
||||||
|
|
||||||
|
|
||||||
def _timer_architecture_model(protocol: JsonObject) -> JsonObject:
|
def _timer_architecture_model(protocol: JsonObject) -> JsonObject:
|
||||||
model = protocol.get("timer_interrupt_model")
|
model = protocol.get("timer_interrupt_model")
|
||||||
if isinstance(model, dict):
|
if isinstance(model, dict):
|
||||||
return model
|
return model
|
||||||
if isinstance(protocol.get("gate_queue_model"), dict) or isinstance(protocol.get("periodic_resend_model"), dict):
|
if isinstance(protocol.get("gate_queue_model"), dict) or isinstance(protocol.get("periodic_resend_model"), dict):
|
||||||
return {
|
return {
|
||||||
"source": "FRT1 OCIA",
|
"source": "FRT1/FRT2 OCIA",
|
||||||
"vector_address_hex": "H'BEEA",
|
"vector_address_hex": "H'BEEA/H'BF23",
|
||||||
"counters": [
|
"counters": [
|
||||||
{
|
{
|
||||||
"address": 0xF9C0,
|
"address": 0xF9C0,
|
||||||
@@ -835,6 +887,12 @@ def _timer_architecture_model(protocol: JsonObject) -> JsonObject:
|
|||||||
"name_candidate": "periodic_resend_cadence_counter_candidate",
|
"name_candidate": "periodic_resend_cadence_counter_candidate",
|
||||||
"role": "candidate periodic resend/heartbeat cadence counter.",
|
"role": "candidate periodic resend/heartbeat cadence counter.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"address": 0xF9C4,
|
||||||
|
"address_hex": "H'F9C4",
|
||||||
|
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
|
||||||
|
"role": "candidate idle/default report enqueue countdown.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
@@ -863,6 +921,12 @@ def _timer_counter_models(model: JsonObject) -> list[JsonObject]:
|
|||||||
"name_candidate": "periodic_resend_cadence_counter_candidate",
|
"name_candidate": "periodic_resend_cadence_counter_candidate",
|
||||||
"role": "candidate periodic resend/heartbeat cadence counter.",
|
"role": "candidate periodic resend/heartbeat cadence counter.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"address": 0xF9C4,
|
||||||
|
"address_hex": "H'F9C4",
|
||||||
|
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
|
||||||
|
"role": "candidate idle/default report enqueue countdown.",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ AUTONOMOUS_TX_REPORT_LABEL = "loc_BB43"
|
|||||||
MAIN_REPORT_GATE_ENTRY = 0x3FD3
|
MAIN_REPORT_GATE_ENTRY = 0x3FD3
|
||||||
MAIN_REPORT_GATE_CALL = 0x3FEB
|
MAIN_REPORT_GATE_CALL = 0x3FEB
|
||||||
SESSION_GATE_ENTRY = 0x3FEF
|
SESSION_GATE_ENTRY = 0x3FEF
|
||||||
|
IDLE_REPORT_GATE_ENTRY = 0x4046
|
||||||
|
IDLE_REPORT_QUEUE_WRITE = 0x4067
|
||||||
|
IDLE_REPORT_GATE_END = 0x4070
|
||||||
QUEUE_REPORT_ENTRY = 0xBAF2
|
QUEUE_REPORT_ENTRY = 0xBAF2
|
||||||
RESEND_GATE_ENTRY = 0xBE9E
|
RESEND_GATE_ENTRY = 0xBE9E
|
||||||
PERIODIC_RESEND_ENTRY = 0xBED5
|
PERIODIC_RESEND_ENTRY = 0xBED5
|
||||||
|
FRT1_OCIA_ENTRY = 0xBEEA
|
||||||
|
FRT2_OCIA_ENTRY = 0xBF23
|
||||||
INDEX_DECODER_ADDRESS = 0x622B
|
INDEX_DECODER_ADDRESS = 0x622B
|
||||||
INDEX_DECODER_LABEL = "loc_622B"
|
INDEX_DECODER_LABEL = "loc_622B"
|
||||||
CHECKSUM_SEED = 0x5A
|
CHECKSUM_SEED = 0x5A
|
||||||
@@ -82,6 +87,8 @@ STATE_VARIABLES = {
|
|||||||
0xF9B5: "event_queue_write_or_pending_cursor_candidate",
|
0xF9B5: "event_queue_write_or_pending_cursor_candidate",
|
||||||
0xF9B9: "event_queue_base_or_current_slot_candidate",
|
0xF9B9: "event_queue_base_or_current_slot_candidate",
|
||||||
0xF9C0: "serial_tx_busy_timer_candidate",
|
0xF9C0: "serial_tx_busy_timer_candidate",
|
||||||
|
0xF9C4: "idle_heartbeat_gate_countdown_candidate",
|
||||||
|
0xF9C5: "rx_session_timeout_candidate",
|
||||||
0xF9C6: "autonomous_report_period_timer_candidate",
|
0xF9C6: "autonomous_report_period_timer_candidate",
|
||||||
0xF9C8: "autonomous_report_resend_countdown_candidate",
|
0xF9C8: "autonomous_report_resend_countdown_candidate",
|
||||||
}
|
}
|
||||||
@@ -132,6 +139,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
|
|||||||
"gate_queue_model": None,
|
"gate_queue_model": None,
|
||||||
"tx_report_model": None,
|
"tx_report_model": None,
|
||||||
"periodic_resend_model": None,
|
"periodic_resend_model": None,
|
||||||
|
"timer_interrupt_model": None,
|
||||||
"confidence": "low",
|
"confidence": "low",
|
||||||
"confidence_score": 0.0,
|
"confidence_score": 0.0,
|
||||||
"caveat": "No protocol semantics are emitted without both RX and TX serial reconstruction candidates.",
|
"caveat": "No protocol semantics are emitted without both RX and TX serial reconstruction candidates.",
|
||||||
@@ -148,6 +156,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
|
|||||||
gate_queue_model = _gate_queue_model(ordered, commands)
|
gate_queue_model = _gate_queue_model(ordered, commands)
|
||||||
tx_report_model = _tx_report_model(ordered, responses)
|
tx_report_model = _tx_report_model(ordered, responses)
|
||||||
periodic_resend_model = _periodic_resend_model(ordered, responses)
|
periodic_resend_model = _periodic_resend_model(ordered, responses)
|
||||||
|
timer_interrupt_model = _timer_interrupt_model(ordered)
|
||||||
evidence = _top_level_evidence(ordered, dispatch, responses, rx_candidate, tx_candidate)
|
evidence = _top_level_evidence(ordered, dispatch, responses, rx_candidate, tx_candidate)
|
||||||
|
|
||||||
confidence_score = _confidence_score(frame_supported, dispatch, responses, commands)
|
confidence_score = _confidence_score(frame_supported, dispatch, responses, commands)
|
||||||
@@ -202,6 +211,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
|
|||||||
"gate_queue_model": gate_queue_model,
|
"gate_queue_model": gate_queue_model,
|
||||||
"tx_report_model": tx_report_model,
|
"tx_report_model": tx_report_model,
|
||||||
"periodic_resend_model": periodic_resend_model,
|
"periodic_resend_model": periodic_resend_model,
|
||||||
|
"timer_interrupt_model": timer_interrupt_model,
|
||||||
"evidence": evidence,
|
"evidence": evidence,
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -222,6 +232,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
|
|||||||
"gate_queue_model": protocol["gate_queue_model"],
|
"gate_queue_model": protocol["gate_queue_model"],
|
||||||
"tx_report_model": protocol["tx_report_model"],
|
"tx_report_model": protocol["tx_report_model"],
|
||||||
"periodic_resend_model": protocol["periodic_resend_model"],
|
"periodic_resend_model": protocol["periodic_resend_model"],
|
||||||
|
"timer_interrupt_model": protocol["timer_interrupt_model"],
|
||||||
"confidence": protocol["confidence"],
|
"confidence": protocol["confidence"],
|
||||||
"confidence_score": protocol["confidence_score"],
|
"confidence_score": protocol["confidence_score"],
|
||||||
"caveat": protocol["caveat"],
|
"caveat": protocol["caveat"],
|
||||||
@@ -1308,10 +1319,17 @@ def _logical_table_map_candidates(ordered: list[JsonObject]) -> list[JsonObject]
|
|||||||
|
|
||||||
def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
|
def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
|
||||||
candidates: list[JsonObject] = []
|
candidates: list[JsonObject] = []
|
||||||
|
state_regions = [
|
||||||
|
(SERIAL_HANDLER_START, SERIAL_HANDLER_END),
|
||||||
|
(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END),
|
||||||
|
(0x40E0, 0x40E0),
|
||||||
|
(FRT1_OCIA_ENTRY, 0xBF08),
|
||||||
|
(FRT2_OCIA_ENTRY, 0xBF37),
|
||||||
|
]
|
||||||
serial_region = [
|
serial_region = [
|
||||||
ins
|
ins
|
||||||
for ins in ordered
|
for ins in ordered
|
||||||
if SERIAL_HANDLER_START <= int(ins.get("address", -1)) <= SERIAL_HANDLER_END
|
if any(start <= int(ins.get("address", -1)) <= end for start, end in state_regions)
|
||||||
]
|
]
|
||||||
if not any(
|
if not any(
|
||||||
_has_ref_in_range(ins, min(STATE_VARIABLES), max(STATE_VARIABLES))
|
_has_ref_in_range(ins, min(STATE_VARIABLES), max(STATE_VARIABLES))
|
||||||
@@ -1366,8 +1384,8 @@ def _state_variable_candidates(ordered: list[JsonObject]) -> list[JsonObject]:
|
|||||||
"evidence_addresses_hex": _hlist(evidence),
|
"evidence_addresses_hex": _hlist(evidence),
|
||||||
"confidence": "candidate-medium",
|
"confidence": "candidate-medium",
|
||||||
"caveat": (
|
"caveat": (
|
||||||
"Role is inferred from references in the serial handler region and remains "
|
"Role is inferred from references in serial handler, gate, and timer regions "
|
||||||
"a state-variable candidate."
|
"and remains a state-variable candidate."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1621,6 +1639,7 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
|
|||||||
evidence = _dedupe_ints(
|
evidence = _dedupe_ints(
|
||||||
_addresses_in_ranges(ordered, [(MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)], MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)
|
_addresses_in_ranges(ordered, [(MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)], MAIN_REPORT_GATE_ENTRY, MAIN_REPORT_GATE_CALL)
|
||||||
+ _addresses_in_ranges(ordered, [(SESSION_GATE_ENTRY, 0x4007)], SESSION_GATE_ENTRY, 0x4007)
|
+ _addresses_in_ranges(ordered, [(SESSION_GATE_ENTRY, 0x4007)], SESSION_GATE_ENTRY, 0x4007)
|
||||||
|
+ _addresses_in_ranges(ordered, [(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)], IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)
|
||||||
+ _addresses_in_ranges(ordered, [(QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)], QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)
|
+ _addresses_in_ranges(ordered, [(QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)], QUEUE_REPORT_ENTRY, AUTONOMOUS_TX_REPORT_CALL)
|
||||||
+ _addresses_in_ranges(ordered, [(RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)], RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)
|
+ _addresses_in_ranges(ordered, [(RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)], RESEND_GATE_ENTRY, PERIODIC_RESEND_ENTRY)
|
||||||
)
|
)
|
||||||
@@ -1655,6 +1674,26 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
|
|||||||
MAIN_REPORT_GATE_CALL,
|
MAIN_REPORT_GATE_CALL,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "idle_heartbeat_report_may_enqueue",
|
||||||
|
"entry_label": "loc_4046",
|
||||||
|
"target_label": "loc_4067",
|
||||||
|
"condition_candidate": (
|
||||||
|
"F9C4 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0)) && F9B0 == F9B5"
|
||||||
|
),
|
||||||
|
"summary": (
|
||||||
|
"Idle/default report gate; when the FRT2 countdown clears and the queue is "
|
||||||
|
"empty, loc_4046 can enqueue H'00FF for the later loc_BAF2 -> loc_BA26 send path."
|
||||||
|
),
|
||||||
|
"state_addresses_hex": [_h16(0xF9C4), _h16(0xFAA5), _h16(0xF9C3), _h16(0xF9B0), _h16(0xF9B5)],
|
||||||
|
"enqueued_report_candidate_hex": _h16(0x00FF),
|
||||||
|
"evidence_addresses": _addresses_in_ranges(
|
||||||
|
ordered,
|
||||||
|
[(IDLE_REPORT_GATE_ENTRY, IDLE_REPORT_GATE_END)],
|
||||||
|
IDLE_REPORT_GATE_ENTRY,
|
||||||
|
IDLE_REPORT_GATE_END,
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "queue_has_pending_report",
|
"name": "queue_has_pending_report",
|
||||||
"entry_label": "loc_BAF2",
|
"entry_label": "loc_BAF2",
|
||||||
@@ -1705,6 +1744,20 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
|
|||||||
0x4007,
|
0x4007,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "idle_heartbeat_gate_initial_delay_loaded",
|
||||||
|
"summary": "Startup/init loads F9C4 with H'14 before the first idle/default report can be queued.",
|
||||||
|
"state_addresses_hex": [_h16(0xF9C4)],
|
||||||
|
"reload_value_hex": _h16(0x14, width=2),
|
||||||
|
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C4, 0x14),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idle_heartbeat_gate_post_send_delay_loaded",
|
||||||
|
"summary": "loc_BA26 reloads F9C4 with H'07 after each send, matching the observed heartbeat spacing.",
|
||||||
|
"state_addresses_hex": [_h16(0xF9C4)],
|
||||||
|
"reload_value_hex": _h16(0x07, width=2),
|
||||||
|
"evidence_addresses": _state_immediate_evidence(ordered, 0xF9C4, 0x07),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "host_ack_can_advance_queue",
|
"name": "host_ack_can_advance_queue",
|
||||||
"summary": "Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.",
|
"summary": "Commands 0x05/0x06 are modeled as acknowledgement paths that can clear pending state or advance F9B5.",
|
||||||
@@ -1729,6 +1782,111 @@ def _gate_queue_model(ordered: list[JsonObject], commands: list[JsonObject]) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _timer_interrupt_model(ordered: list[JsonObject]) -> JsonObject | None:
|
||||||
|
present_addresses = {int(ins["address"]) for ins in ordered if isinstance(ins.get("address"), int)}
|
||||||
|
frt1_evidence = _dedupe_ints(
|
||||||
|
_addresses_in_ranges(ordered, [(FRT1_OCIA_ENTRY, 0xBF08)], FRT1_OCIA_ENTRY, 0xBF08)
|
||||||
|
)
|
||||||
|
frt2_evidence = _dedupe_ints(
|
||||||
|
_addresses_in_ranges(ordered, [(FRT2_OCIA_ENTRY, 0xBF37)], FRT2_OCIA_ENTRY, 0xBF37)
|
||||||
|
)
|
||||||
|
sources: list[JsonObject] = []
|
||||||
|
if frt1_evidence:
|
||||||
|
frt1_counters = [
|
||||||
|
counter
|
||||||
|
for counter in (
|
||||||
|
_timer_counter(0xF9C0, "tx_report_gate_counter_candidate", "candidate gate counter used before entering the report builder.", 0xBEF4),
|
||||||
|
_timer_counter(0xF9C1, "rx_interbyte_timeout_candidate", "candidate RX interbyte timeout counter.", 0xBEFE),
|
||||||
|
_timer_counter(0xF9C6, "periodic_resend_cadence_counter_candidate", "candidate periodic resend/heartbeat cadence counter.", 0xBF08),
|
||||||
|
)
|
||||||
|
if int(counter["evidence_address"]) in present_addresses
|
||||||
|
]
|
||||||
|
sources.append(
|
||||||
|
{
|
||||||
|
"source": "FRT1 OCIA",
|
||||||
|
"vector_address_hex": _h16(0x0062),
|
||||||
|
"handler_address": FRT1_OCIA_ENTRY,
|
||||||
|
"handler_address_hex": _h16(FRT1_OCIA_ENTRY),
|
||||||
|
"summary": "Candidate periodic tick ISR for serial busy, interbyte, and resend counters.",
|
||||||
|
"counters": frt1_counters,
|
||||||
|
"evidence_addresses": frt1_evidence,
|
||||||
|
"evidence_addresses_hex": _hlist(frt1_evidence),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if frt2_evidence:
|
||||||
|
frt2_counters = [
|
||||||
|
counter
|
||||||
|
for counter in (
|
||||||
|
_timer_counter(0xF9C4, "idle_heartbeat_gate_countdown_candidate", "candidate idle/default report enqueue countdown.", 0xBF2D),
|
||||||
|
_timer_counter(0xF9C5, "rx_session_timeout_candidate", "candidate RX/session maintenance timeout counter.", 0xBF37),
|
||||||
|
)
|
||||||
|
if int(counter["evidence_address"]) in present_addresses
|
||||||
|
]
|
||||||
|
sources.append(
|
||||||
|
{
|
||||||
|
"source": "FRT2 OCIA",
|
||||||
|
"vector_address_hex": _h16(0x006A),
|
||||||
|
"handler_address": FRT2_OCIA_ENTRY,
|
||||||
|
"handler_address_hex": _h16(FRT2_OCIA_ENTRY),
|
||||||
|
"summary": "Candidate periodic tick ISR for idle heartbeat/report and RX session counters.",
|
||||||
|
"clock_select": "CKS1=1 CKS0=0 => phi/32",
|
||||||
|
"ocra_value_hex": "H'7A12",
|
||||||
|
"manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
|
||||||
|
"counters": frt2_counters,
|
||||||
|
"evidence_addresses": frt2_evidence,
|
||||||
|
"evidence_addresses_hex": _hlist(frt2_evidence),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not sources:
|
||||||
|
return None
|
||||||
|
|
||||||
|
counters = _dedupe_timer_counters(
|
||||||
|
counter
|
||||||
|
for source in sources
|
||||||
|
for counter in source.get("counters", [])
|
||||||
|
if isinstance(counter, dict)
|
||||||
|
)
|
||||||
|
evidence = _dedupe_ints(
|
||||||
|
addr
|
||||||
|
for source in sources
|
||||||
|
for addr in source.get("evidence_addresses", [])
|
||||||
|
if isinstance(addr, int)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"kind": "timer_interrupt_model_candidate",
|
||||||
|
"source": " / ".join(str(source["source"]) for source in sources),
|
||||||
|
"summary": "FRT compare-match handlers decrement serial gate, timeout, and cadence counters.",
|
||||||
|
"sources": sources,
|
||||||
|
"counters": counters,
|
||||||
|
"evidence_addresses": evidence,
|
||||||
|
"evidence_addresses_hex": _hlist(evidence),
|
||||||
|
"confidence": "candidate-medium",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _timer_counter(address: int, name: str, role: str, evidence_address: int) -> JsonObject:
|
||||||
|
return {
|
||||||
|
"address": address,
|
||||||
|
"address_hex": _h16(address),
|
||||||
|
"name_candidate": name,
|
||||||
|
"role": role,
|
||||||
|
"evidence_address": evidence_address,
|
||||||
|
"evidence_address_hex": _h16(evidence_address),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_timer_counters(counters: Iterable[JsonObject]) -> list[JsonObject]:
|
||||||
|
output = []
|
||||||
|
seen: set[int] = set()
|
||||||
|
for counter in counters:
|
||||||
|
address = counter.get("address")
|
||||||
|
if not isinstance(address, int) or address in seen:
|
||||||
|
continue
|
||||||
|
seen.add(address)
|
||||||
|
output.append(counter)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
def _tx_report_model(ordered: list[JsonObject], responses: list[JsonObject]) -> JsonObject | None:
|
def _tx_report_model(ordered: list[JsonObject], responses: list[JsonObject]) -> JsonObject | None:
|
||||||
report_responses = [
|
report_responses = [
|
||||||
response for response in responses
|
response for response in responses
|
||||||
|
|||||||
63
tests/test_emulator_addressing.py
Normal file
63
tests/test_emulator_addressing.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from h8536.emulator import H8536Emulator
|
||||||
|
|
||||||
|
|
||||||
|
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
|
||||||
|
rom = bytearray([0xFF] * size)
|
||||||
|
rom[0:2] = reset.to_bytes(2, "big")
|
||||||
|
return rom
|
||||||
|
|
||||||
|
|
||||||
|
class EmulatorAddressingTest(unittest.TestCase):
|
||||||
|
def test_txi_indexed_byte_load_uses_signed_word_displacement_and_full_index_register(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1004] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
|
||||||
|
|
||||||
|
emulator = H8536Emulator(bytes(rom))
|
||||||
|
emulator.cpu.regs[0] = 5
|
||||||
|
emulator.memory.write8(0xF85D, 0xA6)
|
||||||
|
emulator.step()
|
||||||
|
|
||||||
|
self.assertEqual(emulator.cpu.regs[0], 0x00A6)
|
||||||
|
self.assertEqual(emulator.cpu.pc, 0x1004)
|
||||||
|
|
||||||
|
def test_txi_indexed_byte_load_preserves_destination_high_byte(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1004] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
|
||||||
|
|
||||||
|
emulator = H8536Emulator(bytes(rom))
|
||||||
|
emulator.cpu.regs[0] = 0x0105
|
||||||
|
emulator.memory.write8(0xF95D, 0x4B)
|
||||||
|
emulator.step()
|
||||||
|
|
||||||
|
self.assertEqual(emulator.cpu.regs[0], 0x014B)
|
||||||
|
|
||||||
|
def test_extu_byte_clears_high_byte_before_txi_indexed_load(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1002] = b"\xA0\x12" # EXTU.B R0
|
||||||
|
rom[0x1002:0x1006] = b"\xF0\xF8\x58\x80" # MOV:G.B @(-H'07A8,R0), R0
|
||||||
|
|
||||||
|
emulator = H8536Emulator(bytes(rom))
|
||||||
|
emulator.cpu.regs[0] = 0x0105
|
||||||
|
emulator.memory.write8(0xF85D, 0xC7)
|
||||||
|
emulator.memory.write8(0xF95D, 0x99)
|
||||||
|
emulator.run(max_steps=2)
|
||||||
|
|
||||||
|
self.assertEqual(emulator.cpu.regs[0], 0x00C7)
|
||||||
|
self.assertEqual(emulator.cpu.pc, 0x1006)
|
||||||
|
|
||||||
|
def test_signed_byte_displacement_addresses_below_base(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1003] = b"\xE1\xFE\x81" # MOV:G.B @(H'-02,R1), R1
|
||||||
|
|
||||||
|
emulator = H8536Emulator(bytes(rom))
|
||||||
|
emulator.cpu.regs[1] = 0xFE90
|
||||||
|
emulator.memory.write8(0xFE8E, 0x37)
|
||||||
|
emulator.step()
|
||||||
|
|
||||||
|
self.assertEqual(emulator.cpu.regs[1], 0xFE37)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -64,6 +64,8 @@ class P9FastPathTest(unittest.TestCase):
|
|||||||
self.assertEqual(emulator.cpu.regs[7], 0xFE7E)
|
self.assertEqual(emulator.cpu.regs[7], 0xFE7E)
|
||||||
self.assertEqual(fast_path.events[-1].kind, "read_byte")
|
self.assertEqual(fast_path.events[-1].kind, "read_byte")
|
||||||
self.assertEqual(fast_path.events[-1].value, 0x3C)
|
self.assertEqual(fast_path.events[-1].value, 0x3C)
|
||||||
|
self.assertEqual(fast_path.events[-1].source, "initial")
|
||||||
|
self.assertEqual(fast_path.events[-1].queue_depth, 0)
|
||||||
self.assertFalse(emulator.cpu.z)
|
self.assertFalse(emulator.cpu.z)
|
||||||
self.assertFalse(emulator.cpu.n)
|
self.assertFalse(emulator.cpu.n)
|
||||||
self.assertFalse(emulator.cpu.v)
|
self.assertFalse(emulator.cpu.v)
|
||||||
@@ -79,9 +81,30 @@ class P9FastPathTest(unittest.TestCase):
|
|||||||
self.assertTrue(fast_path.try_handle(emulator))
|
self.assertTrue(fast_path.try_handle(emulator))
|
||||||
self.assertEqual(emulator.cpu.regs[5], 0x0081)
|
self.assertEqual(emulator.cpu.regs[5], 0x0081)
|
||||||
self.assertEqual(emulator.cpu.pc, 0x5678)
|
self.assertEqual(emulator.cpu.pc, 0x5678)
|
||||||
|
self.assertEqual(fast_path.events[-1].source, "default_input_byte")
|
||||||
|
self.assertEqual(fast_path.events[-1].queue_depth, 0)
|
||||||
|
self.assertIn("source=default_input_byte", fast_path.events[-1].line())
|
||||||
self.assertFalse(emulator.cpu.z)
|
self.assertFalse(emulator.cpu.z)
|
||||||
self.assertTrue(emulator.cpu.n)
|
self.assertTrue(emulator.cpu.n)
|
||||||
|
|
||||||
|
def test_named_input_script_records_read_source_and_remaining_depth(self):
|
||||||
|
emulator = H8536Emulator(bytes(rom_with_reset()))
|
||||||
|
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
|
||||||
|
emulator.cpu.regs[7] = 0xFE82
|
||||||
|
emulator.memory.write16(0xFE82, 0x6789)
|
||||||
|
|
||||||
|
fast_path = P9FastPath(P9FastPathConfig(enabled=True))
|
||||||
|
fast_path.queue_input_script("idle-panel", [0x00, 0x80])
|
||||||
|
|
||||||
|
self.assertTrue(fast_path.try_handle(emulator))
|
||||||
|
event = fast_path.events[-1]
|
||||||
|
self.assertEqual(emulator.cpu.regs[5], 0x0000)
|
||||||
|
self.assertEqual(event.kind, "read_byte")
|
||||||
|
self.assertEqual(event.value, 0x00)
|
||||||
|
self.assertEqual(event.source, "script:idle-panel")
|
||||||
|
self.assertEqual(event.queue_depth, 1)
|
||||||
|
self.assertEqual(fast_path.trace_lines(), ["read_byte pc=C0DB value=00 source=script:idle-panel queued=1"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from h8536.emulator.probe import DEFAULT_WATCH_PCS, parse_watch_pc, run_probe
|
from h8536.emulator.probe import (
|
||||||
|
DEFAULT_WATCH_PCS,
|
||||||
|
ProbeReport,
|
||||||
|
RAMLifecycleTrace,
|
||||||
|
ReportGateTrace,
|
||||||
|
ReportQueueTrace,
|
||||||
|
TXFrameSnapshot,
|
||||||
|
TXFrameWriteTrace,
|
||||||
|
parse_watch_pc,
|
||||||
|
parse_tx_frame,
|
||||||
|
run_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
|
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
|
||||||
@@ -15,6 +27,13 @@ class EmulatorProbeTest(unittest.TestCase):
|
|||||||
self.assertEqual(parse_watch_pc("0xC08B"), 0xC08B)
|
self.assertEqual(parse_watch_pc("0xC08B"), 0xC08B)
|
||||||
self.assertEqual(parse_watch_pc("H'C08B"), 0xC08B)
|
self.assertEqual(parse_watch_pc("H'C08B"), 0xC08B)
|
||||||
|
|
||||||
|
def test_parse_tx_frame_accepts_spaced_and_compact_hex(self):
|
||||||
|
expected = bytes.fromhex("00 00 00 00 80 DA")
|
||||||
|
|
||||||
|
self.assertEqual(parse_tx_frame("00 00 00 00 80 DA"), expected)
|
||||||
|
self.assertEqual(parse_tx_frame("0000000080DA"), expected)
|
||||||
|
self.assertEqual(parse_tx_frame("00,00,00,00,80,DA"), expected)
|
||||||
|
|
||||||
def test_default_watch_pcs_include_bit_bang_transfer_path(self):
|
def test_default_watch_pcs_include_bit_bang_transfer_path(self):
|
||||||
self.assertIn(0xC08B, DEFAULT_WATCH_PCS)
|
self.assertIn(0xC08B, DEFAULT_WATCH_PCS)
|
||||||
self.assertIn(0xC0DB, DEFAULT_WATCH_PCS)
|
self.assertIn(0xC0DB, DEFAULT_WATCH_PCS)
|
||||||
@@ -51,6 +70,291 @@ class EmulatorProbeTest(unittest.TestCase):
|
|||||||
self.assertIn("H'1005<-H'1003", snapshot.line())
|
self.assertIn("H'1005<-H'1003", snapshot.line())
|
||||||
self.assertTrue(any("recent_watch_snapshots:" == line for line in report.lines()))
|
self.assertTrue(any("recent_watch_snapshots:" == line for line in report.lines()))
|
||||||
|
|
||||||
|
def test_report_lines_include_compact_tx_frame_snapshots(self):
|
||||||
|
staging_and_frame = bytes.fromhex("11 22 33 44 55 66 77 88 00 00 00 00 80 DA")
|
||||||
|
report = ProbeReport(
|
||||||
|
steps=99,
|
||||||
|
pc=0xBA72,
|
||||||
|
stopped_reason="tx",
|
||||||
|
hot_pcs=Counter({0xBA72: 1}),
|
||||||
|
tx_frame_snapshots=[
|
||||||
|
TXFrameSnapshot(
|
||||||
|
step=98,
|
||||||
|
pc=0xBA72,
|
||||||
|
label="first_tdr",
|
||||||
|
bytes_f850_f85d=staging_and_frame,
|
||||||
|
computed_checksum=0xDA,
|
||||||
|
stored_checksum=0xDA,
|
||||||
|
checksum_ok=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = report.lines()
|
||||||
|
|
||||||
|
self.assertIn("recent_tx_frame_snapshots:", lines)
|
||||||
|
self.assertIn(
|
||||||
|
" step=98 pc=H'BA72 first_tdr "
|
||||||
|
"F850-F85D=11 22 33 44 55 66 77 88 00 00 00 00 80 DA "
|
||||||
|
"TX=00 00 00 00 80 DA computed=DA stored=DA checksum_ok=1",
|
||||||
|
lines,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_report_lines_include_target_frame_diff_and_write_sources(self):
|
||||||
|
report = ProbeReport(
|
||||||
|
steps=10,
|
||||||
|
pc=0x1004,
|
||||||
|
stopped_reason="max_steps",
|
||||||
|
hot_pcs=Counter({0x1000: 1}),
|
||||||
|
final_tx_frame=bytes.fromhex("00 01 7F 00 00 24"),
|
||||||
|
target_frame=bytes.fromhex("00 00 00 00 80 DA"),
|
||||||
|
tx_frame_write_traces=[
|
||||||
|
TXFrameWriteTrace(
|
||||||
|
step=2,
|
||||||
|
pc=0x1003,
|
||||||
|
address=0xF859,
|
||||||
|
old_value=0x00,
|
||||||
|
new_value=0x01,
|
||||||
|
frame_after=bytes.fromhex("00 01 00 00 00 00"),
|
||||||
|
instruction="H'1003: 1D F8 58 90 MOV:G.W R0, @H'F858",
|
||||||
|
regs=(0x017F, 0, 0, 0, 0, 0, 0, 0),
|
||||||
|
target_value=0x00,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
tx_target_divergences=[
|
||||||
|
TXFrameWriteTrace(
|
||||||
|
step=2,
|
||||||
|
pc=0x1003,
|
||||||
|
address=0xF859,
|
||||||
|
old_value=0x00,
|
||||||
|
new_value=0x01,
|
||||||
|
frame_after=bytes.fromhex("00 01 00 00 00 00"),
|
||||||
|
instruction="H'1003: 1D F8 58 90 MOV:G.W R0, @H'F858",
|
||||||
|
regs=(0x017F, 0, 0, 0, 0, 0, 0, 0),
|
||||||
|
target_value=0x00,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
report_queue_traces=[
|
||||||
|
ReportQueueTrace(
|
||||||
|
step=3,
|
||||||
|
pc=0x1007,
|
||||||
|
kind="queue_write",
|
||||||
|
head=0,
|
||||||
|
tail=0,
|
||||||
|
queue_index=0,
|
||||||
|
address=0xF870,
|
||||||
|
old_word=0x0000,
|
||||||
|
new_word=0x00FF,
|
||||||
|
instruction="H'1003: 1D F8 70 93 MOV:G.W R3, @H'F870",
|
||||||
|
regs=(0, 0, 0, 0x00FF, 0, 0, 0, 0),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
report_gate_traces=[
|
||||||
|
ReportGateTrace(
|
||||||
|
step=4,
|
||||||
|
pc=0x4050,
|
||||||
|
label="faa5_clear_enqueue_branch",
|
||||||
|
f9c4=0,
|
||||||
|
faa5=0,
|
||||||
|
f9c3=0,
|
||||||
|
head=0,
|
||||||
|
tail=0,
|
||||||
|
regs=(0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
|
z=True,
|
||||||
|
c=False,
|
||||||
|
n=False,
|
||||||
|
decision="enqueue_candidate_faa5_clear",
|
||||||
|
f9c4_last_write_step=3,
|
||||||
|
f9c4_last_write_pc=0x40E0,
|
||||||
|
f9c4_last_write_value=0x00,
|
||||||
|
f9c4_last_write_age=1,
|
||||||
|
f9c4_last_nonzero_step=2,
|
||||||
|
f9c4_last_nonzero_pc=0x40D8,
|
||||||
|
f9c4_last_nonzero_value=0x14,
|
||||||
|
f9c4_last_nonzero_age=2,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
ram_lifecycle_traces=[
|
||||||
|
RAMLifecycleTrace(
|
||||||
|
step=3,
|
||||||
|
pc=0x40E0,
|
||||||
|
address=0xF9C4,
|
||||||
|
name="F9C4_report_gate_timer",
|
||||||
|
old_value=0x14,
|
||||||
|
new_value=0x00,
|
||||||
|
instruction="H'40E0: 15 F9 C4 13 CLR.B @H'F9C4",
|
||||||
|
regs=(0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = report.lines()
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"target_frame=target=00 00 00 00 80 DA current=00 01 7F 00 00 24 "
|
||||||
|
"diffs=1:01!=00 2:7F!=00 4:00!=80 5:24!=DA pre_checksum_diffs=1,2,4",
|
||||||
|
lines,
|
||||||
|
)
|
||||||
|
self.assertIn("recent_tx_frame_writes:", lines)
|
||||||
|
self.assertIn("first_target_divergences:", lines)
|
||||||
|
self.assertIn("recent_report_queue:", lines)
|
||||||
|
self.assertIn("recent_report_gates:", lines)
|
||||||
|
self.assertIn("recent_ram_lifecycle:", lines)
|
||||||
|
self.assertTrue(any("TX[1]" in line and "target=00 DIFF" in line for line in lines))
|
||||||
|
self.assertTrue(any("queue_write" in line and "word=H'0000->H'00FF" in line for line in lines))
|
||||||
|
self.assertTrue(any("decision=enqueue_candidate_faa5_clear" in line for line in lines))
|
||||||
|
self.assertTrue(any("F9C4_last=step=3@H'40E0 value=00 age=1" in line for line in lines))
|
||||||
|
self.assertTrue(any("F9C4_last_nonzero=step=2@H'40D8 value=14 age=2" in line for line in lines))
|
||||||
|
self.assertTrue(any("F9C4_report_gate_timer H'F9C4 14->00" in line for line in lines))
|
||||||
|
|
||||||
|
def test_run_probe_captures_tx_frame_watch_pc_and_bad_checksum(self):
|
||||||
|
rom = rom_with_reset(reset=0xBA26, size=0xBB00)
|
||||||
|
rom[0xBA26] = 0xFF
|
||||||
|
|
||||||
|
report = run_probe(
|
||||||
|
bytes(rom),
|
||||||
|
max_steps=1,
|
||||||
|
interval_steps=512,
|
||||||
|
stop_on_tx=False,
|
||||||
|
p9_log_limit=8,
|
||||||
|
watch_pcs=(),
|
||||||
|
tx_frame_snapshot_limit=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(report.tx_frame_snapshots), 1)
|
||||||
|
snapshot = report.tx_frame_snapshots[0]
|
||||||
|
self.assertEqual(snapshot.pc, 0xBA26)
|
||||||
|
self.assertEqual(snapshot.label, "builder_entry")
|
||||||
|
self.assertEqual(snapshot.frame_bytes, b"\x00\x00\x00\x00\x00\x00")
|
||||||
|
self.assertEqual(snapshot.computed_checksum, 0x5A)
|
||||||
|
self.assertEqual(snapshot.stored_checksum, 0x00)
|
||||||
|
self.assertFalse(snapshot.checksum_ok)
|
||||||
|
|
||||||
|
def test_run_probe_traces_tx_frame_writes_against_target(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1003] = b"\x58\x01\x7F" # MOV:I.W #H'017F, R0
|
||||||
|
rom[0x1003:0x1007] = b"\x1D\xF8\x58\x90" # MOV:G.W R0, @H'F858
|
||||||
|
|
||||||
|
report = run_probe(
|
||||||
|
bytes(rom),
|
||||||
|
max_steps=2,
|
||||||
|
interval_steps=512,
|
||||||
|
stop_on_tx=False,
|
||||||
|
p9_log_limit=8,
|
||||||
|
watch_pcs=(),
|
||||||
|
trace_frame_sources=True,
|
||||||
|
target_frame=bytes.fromhex("00 00 00 00 80 DA"),
|
||||||
|
frame_write_trace_limit=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(report.final_tx_frame[:3], b"\x01\x7F\x00")
|
||||||
|
self.assertEqual(len(report.tx_frame_write_traces), 2)
|
||||||
|
self.assertEqual(report.tx_frame_write_traces[0].address, 0xF858)
|
||||||
|
self.assertEqual(report.tx_frame_write_traces[0].target_value, 0x00)
|
||||||
|
self.assertIn("MOV:G.W R0, @H'F858", report.tx_frame_write_traces[0].instruction)
|
||||||
|
self.assertEqual(report.tx_target_divergences, [])
|
||||||
|
self.assertTrue(any("target_frame=" in line for line in report.lines()))
|
||||||
|
|
||||||
|
def test_run_probe_traces_report_queue_writes_and_cursors(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1003] = b"\x5B\x00\xFF" # MOV:I.W #H'00FF, R3
|
||||||
|
rom[0x1003:0x1007] = b"\x1D\xF8\x70\x93" # MOV:G.W R3, @H'F870
|
||||||
|
rom[0x1007:0x100B] = b"\x15\xF9\xB0\x08" # ADD:Q.B #1, @H'F9B0
|
||||||
|
|
||||||
|
report = run_probe(
|
||||||
|
bytes(rom),
|
||||||
|
max_steps=3,
|
||||||
|
interval_steps=512,
|
||||||
|
stop_on_tx=False,
|
||||||
|
p9_log_limit=8,
|
||||||
|
watch_pcs=(),
|
||||||
|
trace_report_queue=True,
|
||||||
|
report_queue_trace_limit=8,
|
||||||
|
watch_report_ids=(0x00FF,),
|
||||||
|
)
|
||||||
|
|
||||||
|
queue_writes = [trace for trace in report.report_queue_traces if trace.kind == "queue_write"]
|
||||||
|
cursor_writes = [trace for trace in report.report_queue_traces if trace.kind == "cursor_head_write"]
|
||||||
|
|
||||||
|
self.assertEqual(len(queue_writes), 1)
|
||||||
|
self.assertEqual(queue_writes[0].queue_index, 0)
|
||||||
|
self.assertEqual(queue_writes[0].old_word, 0x0000)
|
||||||
|
self.assertEqual(queue_writes[0].new_word, 0x00FF)
|
||||||
|
self.assertEqual(report.report_queue_first_writes, queue_writes)
|
||||||
|
self.assertEqual(report.report_queue_first_nonzero_writes, queue_writes)
|
||||||
|
self.assertEqual(report.report_queue_watch_hits, queue_writes)
|
||||||
|
self.assertEqual(len(cursor_writes), 1)
|
||||||
|
self.assertEqual(cursor_writes[0].old_value, 0x00)
|
||||||
|
self.assertEqual(cursor_writes[0].new_value, 0x01)
|
||||||
|
self.assertIn("recent_report_queue:", report.lines())
|
||||||
|
|
||||||
|
def test_run_probe_traces_ram_lifecycle_writes_and_last_nonzero_value(self):
|
||||||
|
rom = rom_with_reset()
|
||||||
|
rom[0x1000:0x1005] = b"\x15\xF9\xC4\x06\x14" # MOV:G.B #H'14, @H'F9C4
|
||||||
|
rom[0x1005:0x1009] = b"\x15\xF9\xC4\x13" # CLR.B @H'F9C4
|
||||||
|
|
||||||
|
report = run_probe(
|
||||||
|
bytes(rom),
|
||||||
|
max_steps=2,
|
||||||
|
interval_steps=512,
|
||||||
|
stop_on_tx=False,
|
||||||
|
p9_log_limit=8,
|
||||||
|
watch_pcs=(),
|
||||||
|
trace_ram_lifecycle=True,
|
||||||
|
ram_lifecycle_trace_limit=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(report.ram_lifecycle_traces), 2)
|
||||||
|
set_trace, clear_trace = report.ram_lifecycle_traces
|
||||||
|
self.assertEqual(set_trace.address, 0xF9C4)
|
||||||
|
self.assertEqual(set_trace.old_value, 0x00)
|
||||||
|
self.assertEqual(set_trace.new_value, 0x14)
|
||||||
|
self.assertEqual(clear_trace.old_value, 0x14)
|
||||||
|
self.assertEqual(clear_trace.new_value, 0x00)
|
||||||
|
self.assertEqual(report.ram_lifecycle_last_writes, [clear_trace])
|
||||||
|
self.assertEqual(report.ram_lifecycle_last_nonzero_writes, [set_trace])
|
||||||
|
lines = report.lines()
|
||||||
|
self.assertIn("recent_ram_lifecycle:", lines)
|
||||||
|
self.assertIn("ram_lifecycle_last_writes:", lines)
|
||||||
|
self.assertIn("ram_lifecycle_last_nonzero_writes:", lines)
|
||||||
|
|
||||||
|
def test_run_probe_traces_loc_4046_report_gates(self):
|
||||||
|
rom = rom_with_reset(reset=0x4046, size=0x4080)
|
||||||
|
rom[0x4046:0x404A] = b"\x15\xF9\xC4\x16" # TST.B @H'F9C4
|
||||||
|
rom[0x404A:0x404C] = b"\x26\x0C" # BNE H'4058
|
||||||
|
rom[0x404C:0x4050] = b"\x15\xFA\xA5\xF7" # BTST.B #7, @H'FAA5
|
||||||
|
rom[0x4050:0x4052] = b"\x27\x07" # BEQ H'4059
|
||||||
|
rom[0x4052:0x4056] = b"\x15\xF9\xC3\x16" # TST.B @H'F9C3
|
||||||
|
rom[0x4056:0x4058] = b"\x27\x01" # BEQ H'4059
|
||||||
|
rom[0x4058] = 0x19 # RTS
|
||||||
|
rom[0x4059:0x405D] = b"\x15\xF9\xB0\x82" # MOV:G.B @H'F9B0, R2
|
||||||
|
rom[0x405D:0x405F] = b"\xA2\x12" # EXTU.B R2
|
||||||
|
rom[0x405F:0x4063] = b"\x15\xF9\xB5\x72" # CMP:G.B @H'F9B5, R2
|
||||||
|
rom[0x4063:0x4065] = b"\x26\x0F" # BNE H'4074
|
||||||
|
rom[0x4065:0x4067] = b"\xA2\x1A" # SHLL.B R2
|
||||||
|
rom[0x4067:0x406C] = b"\xFA\xF8\x70\x06\x00" # MOV:G.W #H'00, @(-H'0790,R2)
|
||||||
|
rom[0x406C:0x4070] = b"\x15\xF9\xB0\x08" # ADD:Q.B #1, @H'F9B0
|
||||||
|
rom[0x4070:0x4074] = b"\x15\xF9\xB0\xD7" # BCLR.B #7, @H'F9B0
|
||||||
|
|
||||||
|
report = run_probe(
|
||||||
|
bytes(rom),
|
||||||
|
max_steps=10,
|
||||||
|
interval_steps=512,
|
||||||
|
stop_on_tx=False,
|
||||||
|
p9_log_limit=8,
|
||||||
|
watch_pcs=(),
|
||||||
|
trace_report_gates=True,
|
||||||
|
report_gate_trace_limit=16,
|
||||||
|
)
|
||||||
|
|
||||||
|
decisions = {trace.pc: trace.decision for trace in report.report_gate_traces}
|
||||||
|
|
||||||
|
self.assertEqual(decisions[0x4046], "f9c4_zero_continue")
|
||||||
|
self.assertEqual(decisions[0x4050], "enqueue_candidate_faa5_clear")
|
||||||
|
self.assertEqual(decisions[0x4063], "enqueue_zero_report")
|
||||||
|
self.assertEqual(decisions[0x4067], "write_report_00ff_to_queue_slot")
|
||||||
|
self.assertTrue(any("recent_report_gates:" == line for line in report.lines()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ def fixture_payload() -> dict[str, object]:
|
|||||||
ins(0x3FEF, "TST.B @H'F9C5", references=[0xF9C5]),
|
ins(0x3FEF, "TST.B @H'F9C5", references=[0xF9C5]),
|
||||||
ins(0x3FF5, "CLR.B @H'F9B5", references=[0xF9B5]),
|
ins(0x3FF5, "CLR.B @H'F9B5", references=[0xF9B5]),
|
||||||
ins(0x3FF9, "CLR.B @H'F9B0", references=[0xF9B0]),
|
ins(0x3FF9, "CLR.B @H'F9B0", references=[0xF9B0]),
|
||||||
|
ins(0x4046, "TST.B @H'F9C4", references=[0xF9C4]),
|
||||||
|
ins(0x404A, "BNE loc_4058", targets=[0x4058]),
|
||||||
|
ins(0x404C, "BTST.B #7, @H'FAA5", references=[0xFAA5]),
|
||||||
|
ins(0x4050, "BEQ loc_4059", targets=[0x4059]),
|
||||||
|
ins(0x4052, "TST.B @H'F9C3", references=[0xF9C3]),
|
||||||
|
ins(0x4056, "BEQ loc_4059", targets=[0x4059]),
|
||||||
|
ins(0x4058, "RTS"),
|
||||||
|
ins(0x4059, "MOV:G.B @H'F9B0, R2", references=[0xF9B0]),
|
||||||
|
ins(0x405F, "CMP:G.B @H'F9B5, R2", references=[0xF9B5]),
|
||||||
|
ins(0x4063, "BNE loc_4074", targets=[0x4074]),
|
||||||
|
ins(0x4067, "MOV:G.W #H'00, @(-H'0790,R2)"),
|
||||||
|
ins(0x406C, "ADD:Q.B #1, @H'F9B0", references=[0xF9B0]),
|
||||||
|
ins(0x4070, "BCLR.B #7, @H'F9B0", references=[0xF9B0]),
|
||||||
|
ins(0x40E0, "MOV:G.B #H'14, @H'F9C4", references=[0xF9C4]),
|
||||||
ins(0xBAF2, "MOV:G.B @H'F9B5, R1", references=[0xF9B5]),
|
ins(0xBAF2, "MOV:G.B @H'F9B5, R1", references=[0xF9B5]),
|
||||||
ins(0xBAF8, "CMP:G.B @H'F9B0, R1", references=[0xF9B0]),
|
ins(0xBAF8, "CMP:G.B @H'F9B0, R1", references=[0xF9B0]),
|
||||||
ins(0xBAFC, "BNE loc_BB00", targets=[0xBB00]),
|
ins(0xBAFC, "BNE loc_BB00", targets=[0xBB00]),
|
||||||
@@ -67,6 +81,7 @@ def fixture_payload() -> dict[str, object]:
|
|||||||
ins(0xBEA5, "AND.B @H'FAA3, R0", references=[0xFAA3]),
|
ins(0xBEA5, "AND.B @H'FAA3, R0", references=[0xFAA3]),
|
||||||
ins(0xBEA9, "MOV:G.B R0, @H'FAA3", references=[0xFAA3]),
|
ins(0xBEA9, "MOV:G.B R0, @H'FAA3", references=[0xFAA3]),
|
||||||
ins(0xBEAF, "CLR.B @H'FAA2", references=[0xFAA2]),
|
ins(0xBEAF, "CLR.B @H'FAA2", references=[0xFAA2]),
|
||||||
|
ins(0xBA31, "MOV:G.B #H'07, @H'F9C4", references=[0xF9C4]),
|
||||||
ins(0xBEB5, "TST.W @H'F9C6", references=[0xF9C6]),
|
ins(0xBEB5, "TST.W @H'F9C6", references=[0xF9C6]),
|
||||||
ins(0xBEBB, "TST.B @H'F9C8", references=[0xF9C8]),
|
ins(0xBEBB, "TST.B @H'F9C8", references=[0xF9C8]),
|
||||||
ins(0xBEC5, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]),
|
ins(0xBEC5, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]),
|
||||||
@@ -83,9 +98,15 @@ def fixture_payload() -> dict[str, object]:
|
|||||||
ins(0xBF02, "TST.W @H'F9C6", references=[0xF9C6]),
|
ins(0xBF02, "TST.W @H'F9C6", references=[0xF9C6]),
|
||||||
ins(0xBF06, "BEQ loc_BF0C", targets=[0xBF0C]),
|
ins(0xBF06, "BEQ loc_BF0C", targets=[0xBF0C]),
|
||||||
ins(0xBF08, "ADD:Q.W #-1, @H'F9C6", references=[0xF9C6]),
|
ins(0xBF08, "ADD:Q.W #-1, @H'F9C6", references=[0xF9C6]),
|
||||||
|
ins(0xBF23, "BCLR.B #5, @FRT2_TCSR"),
|
||||||
|
ins(0xBF27, "TST.B @H'F9C4", references=[0xF9C4]),
|
||||||
|
ins(0xBF2D, "ADD:Q.B #-1, @H'F9C4", references=[0xF9C4]),
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
"vectors": [{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"}],
|
"vectors": [
|
||||||
|
{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"},
|
||||||
|
{"address": 0x006A, "name": "frt2_ocia", "target": 0xBF23, "target_label": "vec_frt2_ocia_BF23"},
|
||||||
|
],
|
||||||
"call_graph": {"nodes": [{"start": 0x3FD3, "label": "loc_3FD3"}, {"start": 0xBAF2, "label": "loc_BAF2"}]},
|
"call_graph": {"nodes": [{"start": 0x3FD3, "label": "loc_3FD3"}, {"start": 0xBAF2, "label": "loc_BAF2"}]},
|
||||||
"instructions": rows,
|
"instructions": rows,
|
||||||
}
|
}
|
||||||
@@ -119,6 +140,17 @@ class SerialGateTest(unittest.TestCase):
|
|||||||
self.assertIn("secondary delay", roles[0xF9C1]["role"])
|
self.assertIn("secondary delay", roles[0xF9C1]["role"])
|
||||||
self.assertIn("periodic report/heartbeat", roles[0xF9C6]["role"])
|
self.assertIn("periodic report/heartbeat", roles[0xF9C6]["role"])
|
||||||
|
|
||||||
|
def test_json_analysis_includes_frt2_idle_heartbeat_gate(self):
|
||||||
|
analysis = analyze_serial_gate(fixture_payload())
|
||||||
|
gate = analysis["evidence"]["idle_heartbeat_gate_loc_4046"]
|
||||||
|
|
||||||
|
self.assertTrue(gate["present"])
|
||||||
|
self.assertEqual(gate["timer"]["handler_address_hex"], "H'BF23")
|
||||||
|
self.assertEqual(gate["post_tx_reload_value_hex"], "H'07")
|
||||||
|
self.assertIn("0.7s", gate["summary"])
|
||||||
|
roles = {role["address"]: role for role in gate["candidate_timer_roles"]}
|
||||||
|
self.assertIn("heartbeat", roles[0xF9C4]["role"])
|
||||||
|
|
||||||
def test_summarizes_key_state_readers_and_writers(self):
|
def test_summarizes_key_state_readers_and_writers(self):
|
||||||
analysis = analyze_serial_gate(fixture_payload())
|
analysis = analyze_serial_gate(fixture_payload())
|
||||||
accesses = {entry["address"]: entry for entry in analysis["state_accesses"]}
|
accesses = {entry["address"]: entry for entry in analysis["state_accesses"]}
|
||||||
@@ -126,6 +158,7 @@ class SerialGateTest(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(accesses[0xF9B5]["read_count"], 1)
|
self.assertGreaterEqual(accesses[0xF9B5]["read_count"], 1)
|
||||||
self.assertGreaterEqual(accesses[0xF9B5]["read_write_count"], 1)
|
self.assertGreaterEqual(accesses[0xF9B5]["read_write_count"], 1)
|
||||||
self.assertGreaterEqual(accesses[0xF9C1]["read_write_count"], 1)
|
self.assertGreaterEqual(accesses[0xF9C1]["read_write_count"], 1)
|
||||||
|
self.assertGreaterEqual(accesses[0xF9C4]["read_write_count"], 1)
|
||||||
self.assertGreaterEqual(accesses[0xFAA3]["write_count"], 1)
|
self.assertGreaterEqual(accesses[0xFAA3]["write_count"], 1)
|
||||||
self.assertIn("sample_accesses", accesses[0xFAA2])
|
self.assertIn("sample_accesses", accesses[0xFAA2])
|
||||||
|
|
||||||
@@ -145,6 +178,14 @@ class SerialGateTest(unittest.TestCase):
|
|||||||
self.assertIn("H'F9C1: candidate secondary delay countdown", text)
|
self.assertIn("H'F9C1: candidate secondary delay countdown", text)
|
||||||
self.assertIn("H'F9C6: candidate periodic report/heartbeat countdown", text)
|
self.assertIn("H'F9C6: candidate periodic report/heartbeat countdown", text)
|
||||||
|
|
||||||
|
def test_text_report_mentions_frt2_idle_heartbeat_gate(self):
|
||||||
|
text = format_text_report(analyze_serial_gate(fixture_payload()))
|
||||||
|
|
||||||
|
self.assertIn("loc_4046 idle heartbeat/report gate: present", text)
|
||||||
|
self.assertIn("FRT2 OCIA", text)
|
||||||
|
self.assertIn("H'F9C4: candidate idle heartbeat/report gate countdown", text)
|
||||||
|
self.assertIn("observed period ~= 700ms", text)
|
||||||
|
|
||||||
def test_cli_json_output_and_out_file(self):
|
def test_cli_json_output_and_out_file(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
input_path = Path(tmp) / "rom.json"
|
input_path = Path(tmp) / "rom.json"
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ class SerialPseudocodeTest(unittest.TestCase):
|
|||||||
self.assertIn("host_ack_can_advance_queue: Commands 0x05/0x06 can ack or advance F9B5", text)
|
self.assertIn("host_ack_can_advance_queue: Commands 0x05/0x06 can ack or advance F9B5", text)
|
||||||
self.assertIn("static bool sci1_candidate_main_report_gate_open(void)", text)
|
self.assertIn("static bool sci1_candidate_main_report_gate_open(void)", text)
|
||||||
self.assertIn("static bool sci1_candidate_report_queue_nonempty(void)", text)
|
self.assertIn("static bool sci1_candidate_report_queue_nonempty(void)", text)
|
||||||
|
self.assertIn("static bool sci1_candidate_idle_heartbeat_enqueue_gate_open(void)", text)
|
||||||
self.assertIn("static bool sci1_candidate_periodic_resend_gate_open(void)", text)
|
self.assertIn("static bool sci1_candidate_periodic_resend_gate_open(void)", text)
|
||||||
self.assertIn("TX/autonomous report model candidate:", text)
|
self.assertIn("TX/autonomous report model candidate:", text)
|
||||||
self.assertIn("loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate", text)
|
self.assertIn("loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate", text)
|
||||||
@@ -421,6 +422,41 @@ class SerialPseudocodeTest(unittest.TestCase):
|
|||||||
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
|
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
|
||||||
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
|
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
|
||||||
|
|
||||||
|
def test_timer_source_models_emit_separate_tick_isrs(self):
|
||||||
|
analysis = {
|
||||||
|
"protocol_semantics": [
|
||||||
|
{
|
||||||
|
"confidence": "medium",
|
||||||
|
"confidence_score": 0.6,
|
||||||
|
"timer_interrupt_model": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"source": "FRT2 OCIA",
|
||||||
|
"handler_address_hex": "H'BF23",
|
||||||
|
"summary": "Candidate periodic tick ISR for idle heartbeat/report counters.",
|
||||||
|
"counters": [
|
||||||
|
{
|
||||||
|
"address": 0xF9C4,
|
||||||
|
"address_hex": "H'F9C4",
|
||||||
|
"name_candidate": "idle_heartbeat_gate_countdown_candidate",
|
||||||
|
"role": "candidate idle/default report enqueue countdown.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("h8536.serial_pseudocode.analyze_serial_semantics", return_value=analysis):
|
||||||
|
text = generate_serial_pseudocode(candidate_payload())
|
||||||
|
|
||||||
|
self.assertIn("FRT2 OCIA H'BF23", text)
|
||||||
|
self.assertIn("H'F9C4 idle_heartbeat_gate_countdown_candidate", text)
|
||||||
|
self.assertIn("void frt2_ocia_candidate_tick_isr(void)", text)
|
||||||
|
self.assertIn("MEM8[0xF9C4u] = (u8)(MEM8[0xF9C4u] - 1u);", text)
|
||||||
|
|
||||||
def test_tx_only_option_omits_rx_functions(self):
|
def test_tx_only_option_omits_rx_functions(self):
|
||||||
text = generate_serial_pseudocode(
|
text = generate_serial_pseudocode(
|
||||||
candidate_payload(),
|
candidate_payload(),
|
||||||
|
|||||||
@@ -117,6 +117,16 @@ def planned_semantics_payload() -> dict:
|
|||||||
instruction(0x3FF9, "CLR.B", "@H'F9B0", [0xF9B0]),
|
instruction(0x3FF9, "CLR.B", "@H'F9B0", [0xF9B0]),
|
||||||
instruction(0x3FFD, "BCLR.B", "#7, @H'FAA5", [0xFAA5]),
|
instruction(0x3FFD, "BCLR.B", "#7, @H'FAA5", [0xFAA5]),
|
||||||
instruction(0x4007, "BSET.B", "#7, @H'FAA5", [0xFAA5]),
|
instruction(0x4007, "BSET.B", "#7, @H'FAA5", [0xFAA5]),
|
||||||
|
instruction(0x4046, "TST.B", "@H'F9C4", [0xF9C4]),
|
||||||
|
instruction(0x404C, "BTST.B", "#7, @H'FAA5", [0xFAA5]),
|
||||||
|
instruction(0x4050, "BEQ", "loc_4059", targets=[0x4059]),
|
||||||
|
instruction(0x4052, "TST.B", "@H'F9C3", [0xF9C3]),
|
||||||
|
instruction(0x405F, "CMP:G.B", "@H'F9B5, R2", [0xF9B5]),
|
||||||
|
instruction(0x4067, "MOV:G.W", "#H'00, @(-H'0790,R2)"),
|
||||||
|
instruction(0x406C, "ADD:Q.B", "#1, @H'F9B0", [0xF9B0]),
|
||||||
|
instruction(0x4070, "BCLR.B", "#7, @H'F9B0", [0xF9B0]),
|
||||||
|
instruction(0x40E0, "MOV:G.B", "#H'14, @H'F9C4", [0xF9C4]),
|
||||||
|
instruction(0xBA31, "MOV:G.B", "#H'07, @H'F9C4", [0xF9C4]),
|
||||||
instruction(0xC000, "MOV:G.B", "@H'F861, R1", [0xF861]),
|
instruction(0xC000, "MOV:G.B", "@H'F861, R1", [0xF861]),
|
||||||
instruction(0xC004, "MOV:G.B", "@H'F862, R2", [0xF862]),
|
instruction(0xC004, "MOV:G.B", "@H'F862, R2", [0xF862]),
|
||||||
instruction(0xC008, "BSR", "loc_622B", targets=[0x622B]),
|
instruction(0xC008, "BSR", "loc_622B", targets=[0x622B]),
|
||||||
@@ -319,6 +329,8 @@ class SerialSemanticsTest(unittest.TestCase):
|
|||||||
self.assertIn("faa2", state_text)
|
self.assertIn("faa2", state_text)
|
||||||
self.assertIn("f9b5", state_text)
|
self.assertIn("f9b5", state_text)
|
||||||
self.assertIn("f9c0", state_text)
|
self.assertIn("f9c0", state_text)
|
||||||
|
self.assertIn("f9c4", state_text)
|
||||||
|
self.assertIn("idle_heartbeat_gate_countdown", state_text)
|
||||||
|
|
||||||
def test_planned_retry_error_model_identifies_retransmit_and_checksum_error(self):
|
def test_planned_retry_error_model_identifies_retransmit_and_checksum_error(self):
|
||||||
semantics = only_semantics(self, planned_semantics_payload())
|
semantics = only_semantics(self, planned_semantics_payload())
|
||||||
@@ -368,6 +380,8 @@ class SerialSemanticsTest(unittest.TestCase):
|
|||||||
self.assertIn("baf2", gate_text)
|
self.assertIn("baf2", gate_text)
|
||||||
self.assertIn("faa2 == 0", gate_text)
|
self.assertIn("faa2 == 0", gate_text)
|
||||||
self.assertIn("f9c0 == 0", gate_text)
|
self.assertIn("f9c0 == 0", gate_text)
|
||||||
|
self.assertIn("f9c4 == 0", gate_text)
|
||||||
|
self.assertIn("h'00ff", gate_text)
|
||||||
self.assertIn("f9b5 != f9b0", gate_text)
|
self.assertIn("f9b5 != f9b0", gate_text)
|
||||||
self.assertIn("bb43", gate_text)
|
self.assertIn("bb43", gate_text)
|
||||||
self.assertIn("be9e", gate_text)
|
self.assertIn("be9e", gate_text)
|
||||||
@@ -377,6 +391,28 @@ class SerialSemanticsTest(unittest.TestCase):
|
|||||||
self.assertIn("0x06", gate_text)
|
self.assertIn("0x06", gate_text)
|
||||||
self.assertIn("not rom constants", gate_text)
|
self.assertIn("not rom constants", gate_text)
|
||||||
|
|
||||||
|
def test_timer_interrupt_model_surfaces_frt2_idle_heartbeat_counter(self):
|
||||||
|
semantics = only_semantics(
|
||||||
|
self,
|
||||||
|
base_payload(
|
||||||
|
[
|
||||||
|
instruction(0xBF23, "BCLR.B", "#5, @FRT2_TCSR"),
|
||||||
|
instruction(0xBF27, "TST.B", "@H'F9C4", [0xF9C4]),
|
||||||
|
instruction(0xBF2D, "ADD:Q.B", "#-1, @H'F9C4", [0xF9C4]),
|
||||||
|
instruction(0xBF31, "TST.B", "@H'F9C5", [0xF9C5]),
|
||||||
|
instruction(0xBF37, "ADD:Q.B", "#-1, @H'F9C5", [0xF9C5]),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
timer = semantics["timer_interrupt_model"]
|
||||||
|
timer_text = semantic_text(timer)
|
||||||
|
|
||||||
|
self.assertIn("frt2 ocia", timer_text)
|
||||||
|
self.assertIn("f9c4", timer_text)
|
||||||
|
self.assertIn("idle_heartbeat_gate_countdown", timer_text)
|
||||||
|
self.assertIn("phi/32", timer_text)
|
||||||
|
|
||||||
def test_missing_serial_reconstruction_candidates_emit_no_protocol_semantics(self):
|
def test_missing_serial_reconstruction_candidates_emit_no_protocol_semantics(self):
|
||||||
payload = {
|
payload = {
|
||||||
"serial_reconstruction": {"candidates": []},
|
"serial_reconstruction": {"candidates": []},
|
||||||
|
|||||||
Reference in New Issue
Block a user