1
0

EEPROM layout

This commit is contained in:
Aiden
2026-05-26 11:35:21 +10:00
parent 1ad03d5692
commit edb8ed78f3
19 changed files with 169583 additions and 8 deletions

View File

@@ -34,6 +34,7 @@ To run the newer sidecar protocol and gate/queue analysis tools:
.\.venv\Scripts\python.exe h8536_report_source_trace.py build\rom_decompiled.json --out build\rom_report_sources.txt
.\.venv\Scripts\python.exe h8536_table_xrefs.py --out build\rom_table_xrefs.txt
.\.venv\Scripts\python.exe h8536_ccu_seed_hints.py build\rom_decompiled.json --out build\rom_ccu_seed_hints.txt
.\.venv\Scripts\python.exe h8536_eeprom_layout.py build\rom_decompiled.json --out build\rom_eeprom_layout.txt
.\.venv\Scripts\python.exe h8536_consistency.py build\rom_decompiled.json --out build\rom_consistency.txt
.\.venv\Scripts\python.exe h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt
```
@@ -53,6 +54,7 @@ To start the current emulator harness:
.\.venv\Scripts\python.exe scripts\state_map_runner.py --analyze-log captures\ack-race-000-001.txt
.\.venv\Scripts\python.exe h8536_emulator_state_search.py --preset connect-queue --target ok --first-hit --json-out build\connect-state-search-ok.json
.\.venv\Scripts\python.exe h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity
.\.venv\Scripts\python.exe h8536_emulator.py --max-steps 250000 --p9-fast-path --eeprom-seed blank --eeprom-save build\emulator-eeprom-boot.bin --eeprom-report build\emulator-eeprom-boot.txt --eeprom-report-json build\emulator-eeprom-boot.json
```
The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed.
@@ -83,6 +85,7 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\
- 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.
- Mines ROM-backed CCU seed hints from table xrefs, selector dispatch, LCD text terms, and observed report overlays, then proposes syntactically valid command-0 seed frames and command-1 readback frames for high-value selectors.
- Mines the ROM-backed X24164 EEPROM layout, including the factory F400-F4FF shadow defaults, the page-0 EEPROM signature/options header, the fifteen blank-by-default 8-byte record slots loaded into F7B8-F82F, and the serial selector-to-persistent-offset map used by command 0/4 handlers.
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
- Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers.
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
@@ -96,7 +99,7 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, an X24164 two-wire EEPROM model on traced `P91/SCL` and `P97/SDA`, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path.
- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, an X24164 two-wire EEPROM model on traced `P91/SCL` and `P97/SDA`, logical EEPROM image load/save/reporting, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path.
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, can optionally schedule 38400 8N1 byte arrivals at real UART spacing, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects.
- Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs.
@@ -119,6 +122,8 @@ Current serial observations:
- Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue.
- Board/P9 finding: traced MCU pin 62 `P91` reaches X24164 pin 6 `SCL`, and MCU pin 68 `P97` reaches the shared X24164 pin 5 `SDA` node. The emulator now treats the ROM's `C121/C08B/C0DB/C10C/C142` P9 routines as an X24164-style two-wire EEPROM bus, with ROM logical addresses `0x000-0x7FF` on the `H'A0/H'A1` control-byte family and `0x800-0xFFF` on `H'E0/H'E1`.
- EEPROM role finding: `loc_40BB` checks `P7DR.7` and the `F402 == H'6B6F` signature before defaulting EEPROM/shadow tables; `loc_4103` writes ROM default words through `BFE0`, `loc_41D2` reads sixteen 8-byte records into `F7B0-F82F`, and the command-4 path at `BD2B-BD5F` can persist serial table writes when `F76E.7` is set.
- EEPROM layout finding: `build\rom_eeprom_layout.txt` currently identifies the ROM factory table at `H'C964-H'CA63`, the F400 shadow defaults, page 0 offset `0x000-0x007` as the signature/options header (`00 00 6B 6F FE 00 00 00`), pages 1-F offset `0x00-0x07` as blank-by-default record slots, and 89 selector mappings from the `H'C564` table into F400/EEPROM offsets. `F404` defaults to `H'FE00` and is tested as option/feature bits, while `F76E` combines persistence enable, dispatch suppression, and low-nibble EEPROM page selection.
- Emulator EEPROM-image finding: `build\emulator-eeprom-boot.txt` captures a blank-EEPROM boot defaulting pass. The ROM writes 2108 words, leaves page 0's signature/options header intact, blanks page 1-F record headers, and the final image matches the ROM factory/default baseline. Use `--eeprom-load`/`--eeprom-save` to persist an emulated EEPROM image across runs and compare command-induced changes.
- Emulator board-state finding: P7 now reads external pin state for input bits, so the DIP-off default is modeled as `--p7-input 0xFF`; `--eeprom-seed factory` can pre-seed the X24164 devices and `F400-F4FF` shadow from the ROM default table for already-initialized-state experiments.
- RX probe finding: the `--preset connect-lcd` sequence is sensitive to injection timing and modeled external state. With timed UART injection, the emulator can still reach `CONNECT: OK`/`02 00 02 00 00 5A`, while the real bench remains at `CONNECT NOT ACT`; this points to missing session/P9/external-panel context rather than a simple checksum or UART-spacing issue.
- Emulator state-search finding: the minimum ROM-visible OK display condition is now reproducible without serial. Direct entry at `loc_2CB9` with `E000[0]=0x8080` and unsuppressed `F730=0` reaches `CONNECT: OK`; the queued selector-zero path also reaches OK when `F970[0]=0`, `F9B9=0`, `F9B4=1`, `E000[0]=0x8080`, and `F730=0`. This makes the bench problem sharper: prove whether serial can retain `E000[0]=0x8080` and enqueue selector zero without the reset/clobber path clearing it first.
@@ -147,7 +152,9 @@ build/rom_serial_gate.txt
build/rom_report_sources.txt
build/rom_table_xrefs.txt
build/rom_ccu_seed_hints.txt
build/rom_eeprom_layout.txt
build/rom_consistency.txt
build/emulator-eeprom-boot.txt
build/callgraph.dot
```
@@ -212,6 +219,7 @@ python h8536_rx_branch_trace.py --help
python h8536_report_source_trace.py --help
python h8536_table_xrefs.py --help
python h8536_ccu_seed_hints.py --help
python h8536_eeprom_layout.py --help
python h8536_consistency.py --help
```
@@ -220,6 +228,7 @@ python h8536_consistency.py --help
- `h8536_report_source_trace.py`: traces direct `loc_3E54` report enqueue sources. Current finding: no direct static `R3 = 0x0007` enqueue in the JSON, so CAM power `0x0007` remains runtime/capture-observed unless a later indirect/table path proves it.
- `h8536_table_xrefs.py`: emits candidate table/index xrefs and LCD text correlation hints.
- `h8536_ccu_seed_hints.py`: mines table, dispatch, LCD, and observed-report hints for the CCU-side state stream the RCP may expect before active displays/reports.
- `h8536_eeprom_layout.py`: mines the X24164 EEPROM layout, ROM factory defaults, persistent record slots, and serial selector-to-EEPROM offset mapping.
- `h8536_consistency.py`: flags JSON-to-pseudocode semantic hazards such as byte immediates written to word destinations.
For the emulator harness:
@@ -241,6 +250,9 @@ python h8536_emulator_rx_probe.py --help
- `--p9-fast-optimistic-wrapper`: legacy fallback for older wrapper experiments; the known `BFE0/BFFE` EEPROM wrappers now use the X24164 model instead.
- `--p7-input 0xFF`: set external P7 input pin state; this matters for the EEPROM defaulting gate at `P7DR.7` and the DIP-switch style inputs.
- `--eeprom-seed blank|factory`: choose blank X24164 power-on state or pre-seed the X24164/shadow tables from the ROM defaults before reset.
- `--eeprom-load PATH`: load a 0x1000-byte logical X24164 EEPROM image before boot/probe; page 0 is also mirrored into the F400 shadow so the ROM's early `F402` signature check sees the loaded state.
- `--eeprom-save PATH`: save the final 0x1000-byte logical EEPROM image after boot/probe.
- `--eeprom-report PATH` / `--eeprom-report-json PATH`: write a ROM-layout-aware EEPROM snapshot with page records, write logs, factory diffs, and F400 shadow diffs.
- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history.
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
@@ -289,8 +301,10 @@ python h8536_emulator_rx_probe.py --help
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
- `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation.
- `h8536/ccu_seed_hints.py`: ROM miner for likely fake-CCU state seed selectors and candidate command/readback frames.
- `h8536/eeprom_layout.py`: ROM miner for X24164 EEPROM defaults, 8-byte record slots, and serial persistence mapping.
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, 38400 8N1 UART injection timing, P9/X24164 EEPROM bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding.
- `h8536/emulator/eeprom_image.py`: logical EEPROM image dump/report helpers for emulator runs, including factory diffs and record-slot summaries.
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
- `h8536/emulator/state_search.py`: bounded internal-state search for CONNECT LCD outcomes using ROM execution plus explicit RAM/table patches.
- `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path.
@@ -301,7 +315,7 @@ python h8536_emulator_rx_probe.py --help
- `h8536_pseudocode.py`: pseudocode CLI wrapper.
- `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper.
- `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers.
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_eeprom_layout.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers.
- `h8536_emulator_state_search.py`: emulator CONNECT state-search CLI wrapper.
- `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence.

Binary file not shown.

158603
build/emulator-eeprom-boot.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
Emulator EEPROM Snapshot
size=0x1000 sha256=4bed7704e1ea085487ca325c43bd60da75d37b6ae6f8292544e069a8825c64c6
writes: bytes=4216 words=2108 factory_diff_words=0
Persistent Records:
- page 0x0 EEPROM 0x000-0x007 bytes=00 00 6B 6F FE 00 00 00 text='..ko....'
- page 0x1 EEPROM 0x100-0x107 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x2 EEPROM 0x200-0x207 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x3 EEPROM 0x300-0x307 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x4 EEPROM 0x400-0x407 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x5 EEPROM 0x500-0x507 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x6 EEPROM 0x600-0x607 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x7 EEPROM 0x700-0x707 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x8 EEPROM 0x800-0x807 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0x9 EEPROM 0x900-0x907 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xA EEPROM 0xA00-0xA07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xB EEPROM 0xB00-0xB07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xC EEPROM 0xC00-0xC07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xD EEPROM 0xD00-0xD07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xE EEPROM 0xE00-0xE07 bytes=20 20 20 20 20 20 20 20 text=' '
- page 0xF EEPROM 0xF00-0xF07 bytes=20 20 20 20 20 20 20 20 text=' '
EEPROM Word Writes:
- 0x0FE page=0x0 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x1FE page=0x1 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x2FE page=0x2 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x3FE page=0x3 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x4FE page=0x4 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x5FE page=0x5 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x6FE page=0x6 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x7FE page=0x7 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x8FE page=0x8 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x9FE page=0x9 offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xAFE page=0xA offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xBFE page=0xB offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xCFE page=0xC offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xDFE page=0xD offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xEFE page=0xE offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xFFE page=0xF offset=0xFE 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x0FC page=0x0 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x1FC page=0x1 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x2FC page=0x2 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x3FC page=0x3 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x4FC page=0x4 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x5FC page=0x5 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x6FC page=0x6 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x7FC page=0x7 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x8FC page=0x8 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x9FC page=0x9 offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xAFC page=0xA offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xBFC page=0xB offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xCFC page=0xC offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xDFC page=0xD offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xEFC page=0xE offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xFFC page=0xF offset=0xFC 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x0FA page=0x0 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x1FA page=0x1 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x2FA page=0x2 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x3FA page=0x3 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x4FA page=0x4 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x5FA page=0x5 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x6FA page=0x6 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x7FA page=0x7 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x8FA page=0x8 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x9FA page=0x9 offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xAFA page=0xA offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xBFA page=0xB offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xCFA page=0xC offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xDFA page=0xD offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xEFA page=0xE offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xFFA page=0xF offset=0xFA 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x0F8 page=0x0 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x1F8 page=0x1 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x2F8 page=0x2 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x3F8 page=0x3 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x4F8 page=0x4 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x5F8 page=0x5 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x6F8 page=0x6 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x7F8 page=0x7 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x8F8 page=0x8 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x9F8 page=0x9 offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xAF8 page=0xA offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xBF8 page=0xB offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xCF8 page=0xC offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xDF8 page=0xD offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xEF8 page=0xE offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xFF8 page=0xF offset=0xF8 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x0F6 page=0x0 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x1F6 page=0x1 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x2F6 page=0x2 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x3F6 page=0x3 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x4F6 page=0x4 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x5F6 page=0x5 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x6F6 page=0x6 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x7F6 page=0x7 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x8F6 page=0x8 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0x9F6 page=0x9 offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xAF6 page=0xA offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xBF6 page=0xB offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xCF6 page=0xC offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xDF6 page=0xD offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xEF6 page=0xE offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- 0xFF6 page=0xF offset=0xF6 0xFFFF->0x0000 source=linear_word (factory_shadow_offset)
- ... 2028 more word writes omitted
Factory Diffs:
- current EEPROM image matches ROM factory/default image
F400 Shadow Diffs:
- F400-F4FF shadow matches ROM factory words or no ROM factory baseline was supplied

View File

@@ -0,0 +1,300 @@
{
"factory_diffs": [],
"kind": "emulator_eeprom_snapshot",
"records": [
{
"address": 0,
"address_hex": "0x000",
"ascii": "..ko....",
"bytes_hex": "00 00 6B 6F FE 00 00 00",
"is_blank_spaces": false,
"page": 0,
"page_hex": "0x0",
"range_hex": "0x000-0x007",
"words_hex": [
"0x0000",
"0x6B6F",
"0xFE00",
"0x0000"
]
},
{
"address": 256,
"address_hex": "0x100",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 1,
"page_hex": "0x1",
"range_hex": "0x100-0x107",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 512,
"address_hex": "0x200",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 2,
"page_hex": "0x2",
"range_hex": "0x200-0x207",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 768,
"address_hex": "0x300",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 3,
"page_hex": "0x3",
"range_hex": "0x300-0x307",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 1024,
"address_hex": "0x400",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 4,
"page_hex": "0x4",
"range_hex": "0x400-0x407",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 1280,
"address_hex": "0x500",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 5,
"page_hex": "0x5",
"range_hex": "0x500-0x507",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 1536,
"address_hex": "0x600",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 6,
"page_hex": "0x6",
"range_hex": "0x600-0x607",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 1792,
"address_hex": "0x700",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 7,
"page_hex": "0x7",
"range_hex": "0x700-0x707",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 2048,
"address_hex": "0x800",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 8,
"page_hex": "0x8",
"range_hex": "0x800-0x807",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 2304,
"address_hex": "0x900",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 9,
"page_hex": "0x9",
"range_hex": "0x900-0x907",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 2560,
"address_hex": "0xA00",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 10,
"page_hex": "0xA",
"range_hex": "0xA00-0xA07",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 2816,
"address_hex": "0xB00",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 11,
"page_hex": "0xB",
"range_hex": "0xB00-0xB07",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 3072,
"address_hex": "0xC00",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 12,
"page_hex": "0xC",
"range_hex": "0xC00-0xC07",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 3328,
"address_hex": "0xD00",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 13,
"page_hex": "0xD",
"range_hex": "0xD00-0xD07",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 3584,
"address_hex": "0xE00",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 14,
"page_hex": "0xE",
"range_hex": "0xE00-0xE07",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
},
{
"address": 3840,
"address_hex": "0xF00",
"ascii": " ",
"bytes_hex": "20 20 20 20 20 20 20 20",
"is_blank_spaces": true,
"page": 15,
"page_hex": "0xF",
"range_hex": "0xF00-0xF07",
"words_hex": [
"0x2020",
"0x2020",
"0x2020",
"0x2020"
]
}
],
"shadow_f400": {
"diff_count": 1,
"diffs": [
{
"actual_word": 21760,
"actual_word_hex": "0x5500",
"address": 62634,
"address_hex": "H'F4AA",
"aligned_offset": 170,
"aligned_offset_hex": "0xAA",
"expected_word": 32768,
"expected_word_hex": "0x8000",
"mapped_selectors": [
274
],
"mapped_selectors_hex": [
"0x112"
],
"offset": 170,
"offset_hex": "0xAA",
"page": 0,
"page_hex": "0x0",
"record_byte": null,
"role": "factory_shadow_offset"
}
]
},
"summary": {
"factory_diff_word_count": 0,
"logical_size": 4096,
"logical_size_hex": "0x1000",
"record_count": 16,
"sha256": "4bed7704e1ea085487ca325c43bd60da75d37b6ae6f8292544e069a8825c64c6",
"write_byte_count": 0,
"write_word_count": 0
},
"write_events": [],
"write_word_events": []
}

View File

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

8903
build/rom_eeprom_layout.json Normal file

File diff suppressed because it is too large Load Diff

179
build/rom_eeprom_layout.txt Normal file
View File

@@ -0,0 +1,179 @@
H8/536 EEPROM Layout Report
Summary: The ROM treats the traced P9 bus as two X24164-style EEPROM banks, mirrors a 0x100-byte factory/default block into F400-F4FF, and loads sixteen 8-byte persistent records into F7B0-F82F at boot.
Confidence: medium-high
Physical / Logical Model:
- lower_x24164_candidate: logical 0x000-0x7FF control A0/A1
- upper_x24164_candidate: logical 0x800-0xFFF control E0/E1
- bus: P91/SCL and P97/SDA bit-banged two-wire bus through ROM routines C121/C08B/C0DB/C10C/C142
- page model: 16 logical pages of 0x100 bytes; low 8 address bits are sent as the EEPROM word address
Boot Flow:
- H'40BB eeprom_boot_gate: initializes queue/table scratch, checks P7DR.7, then checks F402 == H'6B6F before trusting persisted state
- H'4103 factory_default_fill: copies ROM C964-CA63 into F400-F4FF and writes the same 0x100-byte defaults to each EEPROM page
- H'4187 record_header_blank: overwrites page offsets 0x00-0x07 on pages 0x1-0xF with four H'2020 words after factory replication; page 0 keeps the signature/options header
- H'41D2 persistent_record_load: reads page offsets 0x00-0x07 from pages 0x0-0xF into F7B0-F82F; record 0 is a signature/options header, records 1-F are label/identity-like slots
- H'BD2B serial_persist_path: command-4 continuation writes the live value into F400+map[selector] and persists it through BFE0 when F76E.7 is set
Factory Shadow Block:
- ROM H'C964 length 0x100 mirrors to H'F400-H'F4FF
- H'F402 offset 0x02 default 0x6B6F (ascii='ko'; xrefs=1)
- H'F404 offset 0x04 default 0xFE00 (xrefs=8)
- H'F408 offset 0x08 default 0x8000 (selectors=0x004)
- H'F40A offset 0x0A default 0x0000 (selectors=0x012)
- H'F40C offset 0x0C default 0x0000 (selectors=0x013)
- H'F40E offset 0x0E default 0x8000 (selectors=0x017)
- H'F410 offset 0x10 default 0x8000 (selectors=0x018)
- H'F412 offset 0x12 default 0x0808 (selectors=0x01A)
- H'F414 offset 0x14 default 0x0000 (selectors=0x01F)
- H'F416 offset 0x16 default 0x0000 (selectors=0x020)
- H'F418 offset 0x18 default 0x0000 (selectors=0x023)
- H'F41A offset 0x1A default 0x0000 (selectors=0x037)
- H'F41C offset 0x1C default 0x0000 (selectors=0x038)
- H'F41E offset 0x1E default 0x8000 (selectors=0x080)
- H'F420 offset 0x20 default 0x8000 (selectors=0x081)
- H'F422 offset 0x22 default 0x0020 (ascii='. '; selectors=0x083)
- H'F424 offset 0x24 default 0x0000 (selectors=0x088)
- H'F426 offset 0x26 default 0x0400 (selectors=0x089)
- H'F428 offset 0x28 default 0x0800 (selectors=0x08B)
- H'F42A offset 0x2A default 0x0040 (ascii='.@'; selectors=0x08D)
- H'F42C offset 0x2C default 0x0000 (selectors=0x08F)
- H'F42E offset 0x2E default 0x8000 (selectors=0x091)
- H'F430 offset 0x30 default 0xFF80 (selectors=0x092)
- H'F432 offset 0x32 default 0x4040 (ascii='@@'; selectors=0x093)
- H'F434 offset 0x34 default 0x0000 (selectors=0x095)
- H'F436 offset 0x36 default 0x0000 (selectors=0x098)
- H'F438 offset 0x38 default 0x8000 (selectors=0x09A)
- H'F43A offset 0x3A default 0x0000 (selectors=0x09D)
- H'F43C offset 0x3C default 0x8000 (selectors=0x09E)
- H'F43E offset 0x3E default 0x8000 (selectors=0x09F)
- H'F440 offset 0x40 default 0x8000 (selectors=0x0A3)
- H'F442 offset 0x42 default 0x8000 (selectors=0x0A4)
Persistent 8-Byte Records:
- 16 records: EEPROM 0x000-0x007 .. 0xF00-0xF07 load into RAM H'F7B0-H'F7B7 .. H'F828-H'F82F
- record 0x0: EEPROM 0x000-0x007 -> RAM H'F7B0-H'F7B7 default '..ko....'
- record 0x1: EEPROM 0x100-0x107 -> RAM H'F7B8-H'F7BF default ' '
- record 0x2: EEPROM 0x200-0x207 -> RAM H'F7C0-H'F7C7 default ' '
- record 0x3: EEPROM 0x300-0x307 -> RAM H'F7C8-H'F7CF default ' '
- record 0x4: EEPROM 0x400-0x407 -> RAM H'F7D0-H'F7D7 default ' '
- record 0x5: EEPROM 0x500-0x507 -> RAM H'F7D8-H'F7DF default ' '
- record 0x6: EEPROM 0x600-0x607 -> RAM H'F7E0-H'F7E7 default ' '
- record 0x7: EEPROM 0x700-0x707 -> RAM H'F7E8-H'F7EF default ' '
- record 0x8: EEPROM 0x800-0x807 -> RAM H'F7F0-H'F7F7 default ' '
- record 0x9: EEPROM 0x900-0x907 -> RAM H'F7F8-H'F7FF default ' '
- record 0xA: EEPROM 0xA00-0xA07 -> RAM H'F800-H'F807 default ' '
- record 0xB: EEPROM 0xB00-0xB07 -> RAM H'F808-H'F80F default ' '
- record 0xC: EEPROM 0xC00-0xC07 -> RAM H'F810-H'F817 default ' '
- record 0xD: EEPROM 0xD00-0xD07 -> RAM H'F818-H'F81F default ' '
- record 0xE: EEPROM 0xE00-0xE07 -> RAM H'F820-H'F827 default ' '
- record 0xF: EEPROM 0xF00-0xF07 -> RAM H'F828-H'F82F default ' '
Serial Selector -> Shadow/EEPROM Mapping:
- table H'C564 has 89 nonzero low-byte mappings
- formula: command 4 persists to EEPROM address (((F76E & 0x0F) << 8) | (mapping_low_byte & 0xFE)) when F76E.7 is set
- selector 0x004 map_word=0x4808 -> H'F408 page+0x08 default=0x8000
- selector 0x012 map_word=0x080A -> H'F40A page+0x0A default=0x0000
- selector 0x013 map_word=0x080C -> H'F40C page+0x0C default=0x0000
- selector 0x017 map_word=0x600E -> H'F40E page+0x0E default=0x8000
- selector 0x018 map_word=0x6010 -> H'F410 page+0x10 default=0x8000
- selector 0x01A map_word=0x1012 -> H'F412 page+0x12 default=0x0808
- selector 0x01F map_word=0x4414 -> H'F414 page+0x14 default=0x0000
- selector 0x020 map_word=0x4416 -> H'F416 page+0x16 default=0x0000
- selector 0x023 map_word=0x0418 -> H'F418 page+0x18 default=0x0000
- selector 0x037 map_word=0x641A -> H'F41A page+0x1A default=0x0000
- selector 0x038 map_word=0x641C -> H'F41C page+0x1C default=0x0000
- selector 0x080 map_word=0xE01E -> H'F41E page+0x1E default=0x8000
- selector 0x081 map_word=0x6020 -> H'F420 page+0x20 default=0x8000
- selector 0x083 map_word=0x6022 -> H'F422 page+0x22 default=0x0020
- selector 0x088 map_word=0x4024 -> H'F424 page+0x24 default=0x0000
- selector 0x089 map_word=0x4426 -> H'F426 page+0x26 default=0x0400
- selector 0x08B map_word=0x4428 -> H'F428 page+0x28 default=0x0800
- selector 0x08D map_word=0x442A -> H'F42A page+0x2A default=0x0040
- selector 0x08F map_word=0x602C -> H'F42C page+0x2C default=0x0000
- selector 0x091 map_word=0x602E -> H'F42E page+0x2E default=0x8000
- selector 0x092 map_word=0x6030 -> H'F430 page+0x30 default=0xFF80
- selector 0x093 map_word=0x6032 -> H'F432 page+0x32 default=0x4040
- selector 0x095 map_word=0x6034 -> H'F434 page+0x34 default=0x0000
- selector 0x098 map_word=0x4036 -> H'F436 page+0x36 default=0x0000
- selector 0x09A map_word=0x6038 -> H'F438 page+0x38 default=0x8000
- selector 0x09D map_word=0x443A -> H'F43A page+0x3A default=0x0000
- selector 0x09E map_word=0x603C -> H'F43C page+0x3C default=0x8000
- selector 0x09F map_word=0x603E -> H'F43E page+0x3E default=0x8000
- selector 0x0A3 map_word=0x6040 -> H'F440 page+0x40 default=0x8000
- selector 0x0A4 map_word=0x6042 -> H'F442 page+0x42 default=0x8000
- selector 0x0A5 map_word=0x6044 -> H'F444 page+0x44 default=0x8000
- selector 0x0A6 map_word=0x6046 -> H'F446 page+0x46 default=0x8000
- selector 0x0A7 map_word=0x4048 -> H'F448 page+0x48 default=0xF000
- selector 0x0A9 map_word=0xE04A -> H'F44A page+0x4A default=0x8000
- selector 0x0AA map_word=0xC44C -> H'F44C page+0x4C default=0x2000
- selector 0x0AC map_word=0xC44E -> H'F44E page+0x4E default=0x8000
- selector 0x0AD map_word=0xC450 -> H'F450 page+0x50 default=0x8000
- selector 0x0AE map_word=0xC452 -> H'F452 page+0x52 default=0x8000
- selector 0x0AF map_word=0xC454 -> H'F454 page+0x54 default=0x8000
- selector 0x0B0 map_word=0xC456 -> H'F456 page+0x56 default=0x8000
- selector 0x0B2 map_word=0xC458 -> H'F458 page+0x58 default=0x8000
- selector 0x0B3 map_word=0xC45A -> H'F45A page+0x5A default=0x8000
- selector 0x0B4 map_word=0xC45C -> H'F45C page+0x5C default=0x8000
- selector 0x0B6 map_word=0xC45E -> H'F45E page+0x5E default=0x8000
- selector 0x0B7 map_word=0x4060 -> H'F460 page+0x60 default=0xF800
- selector 0x0B9 map_word=0x6862 -> H'F462 page+0x62 default=0x4000
- selector 0x0BC map_word=0xE064 -> H'F464 page+0x64 default=0x8000
- selector 0x0BD map_word=0xC066 -> H'F466 page+0x66 default=0x8000
- selector 0x0C0 map_word=0x4468 -> H'F468 page+0x68 default=0x8000
- selector 0x0C1 map_word=0xC46A -> H'F46A page+0x6A default=0x8000
- selector 0x0C3 map_word=0xE46C -> H'F46C page+0x6C default=0x4000
- selector 0x0C4 map_word=0x446E -> H'F46E page+0x6E default=0x8000
- selector 0x0C5 map_word=0xC070 -> H'F470 page+0x70 default=0x8000
- selector 0x0C6 map_word=0x4472 -> H'F472 page+0x72 default=0x8000
- selector 0x0C7 map_word=0xC474 -> H'F474 page+0x74 default=0x8000
- selector 0x0C8 map_word=0xC476 -> H'F476 page+0x76 default=0x0000
- selector 0x0C9 map_word=0xC478 -> H'F478 page+0x78 default=0x0000
- selector 0x0CA map_word=0xC47A -> H'F47A page+0x7A default=0x0000
- selector 0x0CB map_word=0xC47C -> H'F47C page+0x7C default=0x0000
- selector 0x0CC map_word=0xC47E -> H'F47E page+0x7E default=0x0000
- selector 0x0CD map_word=0xC480 -> H'F480 page+0x80 default=0x0000
- selector 0x0D4 map_word=0xC482 -> H'F482 page+0x82 default=0x8000
- selector 0x0D5 map_word=0xC484 -> H'F484 page+0x84 default=0x8000
- selector 0x0D6 map_word=0xC086 -> H'F486 page+0x86 default=0x8000
- selector 0x0D7 map_word=0xC088 -> H'F488 page+0x88 default=0x8000
- selector 0x0D8 map_word=0x408A -> H'F48A page+0x8A default=0x8000
- selector 0x0D9 map_word=0x408C -> H'F48C page+0x8C default=0x8000
- selector 0x0DA map_word=0x408E -> H'F48E page+0x8E default=0x8000
- selector 0x0F6 map_word=0x4090 -> H'F490 page+0x90 default=0x8000
- selector 0x0F9 map_word=0x4492 -> H'F492 page+0x92 default=0x8000
- selector 0x0FA map_word=0x4494 -> H'F494 page+0x94 default=0x8000
- selector 0x0FB map_word=0x4496 -> H'F496 page+0x96 default=0x8000
- selector 0x0FC map_word=0x4498 -> H'F498 page+0x98 default=0x8000
- selector 0x0FD map_word=0x409A -> H'F49A page+0x9A default=0x8000
- selector 0x0FE map_word=0x449C -> H'F49C page+0x9C default=0x8000
- selector 0x0FF map_word=0x449E -> H'F49E page+0x9E default=0x8000
- selector 0x100 map_word=0x44A0 -> H'F4A0 page+0xA0 default=0x8000
- selector 0x101 map_word=0x44A2 -> H'F4A2 page+0xA2 default=0x8000
- selector 0x10F map_word=0xC4A4 -> H'F4A4 page+0xA4 default=0x8000
- selector 0x110 map_word=0x48A6 -> H'F4A6 page+0xA6 default=0x0000
- ... 9 more mapped selectors omitted
State Byte Hints:
- H'F402 factory_signature_word: factory 0x6B6F; boot accepts persisted state only when this word is H'6B6F (xrefs=1)
- H'F404 feature_or_option_flags_candidate: factory 0xFE00; bits 1-4 are tested with F791 gates in display/status routines (xrefs=8)
- H'F730 connect_display_state_candidate: volatile/no factory word (xrefs=3)
- H'F731 session_latch_candidate: volatile/no factory word (xrefs=22)
- H'F732 display_dispatch_selector_candidate: volatile display dispatch selector feeding the 493E pointer table and 48FA report bridge (xrefs=11)
- H'F76E eeprom_page_and_persist_flags: bit7 enables command-4 EEPROM persistence, bit6 suppresses 48FA dispatch, low nibble selects EEPROM page (xrefs=4)
- H'F790 connection_latch_shadow_candidate: volatile/no factory word (xrefs=1)
- H'F791 feature_flag_gate_candidate: volatile gate tested alongside F404 option bits before setting report/display flags (xrefs=12)
- H'FB03 report_bridge_suppress_candidate: volatile/no factory word (xrefs=10)
Bench Implications:
- A live EEPROM dump should first compare F400-F4FF shadow-equivalent offsets against the ROM factory table, especially F402/F404 and mapped offsets.
- Pages 0x0-0xF offset 0x00-0x07 are loaded into F7B0-F82F; page 0 carries the signature/options header and pages 1-F default to spaces, so non-space bytes there are high-value identity/config data.
- Serial command 0/4 can mirror values into F400 offsets selected by the ROM mapping table, but command 4 only persists when the continuation path reaches BD2B and F76E.7 is set.
- F76E is not just a page byte: bit7 gates EEPROM persistence, bit6 suppresses the 48FA dispatch path, and only its low nibble survives into the EEPROM page address.
- CONNECT OK is still likely gated by volatile table/session state as well as EEPROM-backed defaults; EEPROM differences may explain why bench and emulator diverge, but are unlikely to be the whole protocol by themselves.
Caveats:
- The selector map proves where firmware mirrors/persists serial values, not what every field means to the panel UI.
- High bytes in the C564 selector map look structured, but the observed command-0/command-4 paths only use the low byte for F400/EEPROM offsets.
- Indexed F7B0-F82F record consumers can be missed by a static xref pass; dynamic emulator traces should be used once interesting record bytes are found.

730
h8536/eeprom_layout.py Normal file
View File

@@ -0,0 +1,730 @@
from __future__ import annotations
import argparse
import json
from collections import Counter
from collections.abc import Iterable, Mapping
from pathlib import Path
from typing import Any
from .emulator.peripherals.x24164 import (
X24164_FACTORY_DEFAULT_BASE,
X24164_FACTORY_DEFAULT_BYTES,
X24164_LOGICAL_PAGE_COUNT,
X24164_LOGICAL_PAGE_SIZE,
factory_default_words_from_rom,
)
from .formatting import h16, label_for
from .rom import Rom
JsonObject = dict[str, Any]
DEFAULT_INPUT = Path("build/rom_decompiled.json")
DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN")
SHADOW_BASE = 0xF400
SHADOW_SIZE = 0x0100
SELECTOR_MAP_BASE = 0xC564
SELECTOR_MAP_COUNT = 0x0200
RECORD_RAM_BASE = 0xF7B0
RECORD_BYTES = 8
STATE_BYTES: tuple[tuple[int, str], ...] = (
(0xF402, "factory_signature_word"),
(0xF404, "feature_or_option_flags_candidate"),
(0xF730, "connect_display_state_candidate"),
(0xF731, "session_latch_candidate"),
(0xF732, "display_dispatch_selector_candidate"),
(0xF76E, "eeprom_page_and_persist_flags"),
(0xF790, "connection_latch_shadow_candidate"),
(0xF791, "feature_flag_gate_candidate"),
(0xFB03, "report_bridge_suppress_candidate"),
)
def load_eeprom_layout_input(path: Path) -> JsonObject:
with path.open("r", encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict) or "instructions" not in payload:
raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
return payload
def analyze_eeprom_layout(payload: Mapping[str, Any], *, rom_path: Path | None = DEFAULT_ROM) -> JsonObject:
rom = _load_rom(rom_path)
factory_entries = _factory_entries(rom)
selector_map = _selector_map_entries(rom, factory_entries)
xrefs = _xref_summary(payload)
return {
"kind": "eeprom_layout",
"summary": {
"confidence": "medium-high",
"model": (
"The ROM treats the traced P9 bus as two X24164-style EEPROM banks, "
"mirrors a 0x100-byte factory/default block into F400-F4FF, and loads "
"sixteen 8-byte persistent records into F7B0-F82F at boot."
),
"factory_default_count": len(factory_entries),
"selector_persistence_mapping_count": len(selector_map),
"persistent_record_count": X24164_LOGICAL_PAGE_COUNT,
},
"physical_model": _physical_model(),
"boot_flow": _boot_flow(),
"factory_defaults": {
"rom_base": X24164_FACTORY_DEFAULT_BASE,
"rom_base_hex": h16(X24164_FACTORY_DEFAULT_BASE),
"byte_count": X24164_FACTORY_DEFAULT_BYTES,
"shadow_base": SHADOW_BASE,
"shadow_base_hex": h16(SHADOW_BASE),
"shadow_range_hex": f"{h16(SHADOW_BASE)}-{h16(SHADOW_BASE + SHADOW_SIZE - 1)}",
"entries": factory_entries,
"notable_entries": _notable_factory_entries(factory_entries, xrefs, selector_map),
},
"persistent_records": _persistent_records(factory_entries),
"serial_persistence_mapping": {
"table_base": SELECTOR_MAP_BASE,
"table_base_hex": h16(SELECTOR_MAP_BASE),
"entry_count": SELECTOR_MAP_COUNT,
"mapped_entry_count": len(selector_map),
"entries": selector_map,
"offset_histogram": _offset_histogram(selector_map),
"high_byte_histogram": _high_byte_histogram(selector_map),
"address_formula": (
"command 4 persists to EEPROM address "
"(((F76E & 0x0F) << 8) | (mapping_low_byte & 0xFE)) when F76E.7 is set"
),
},
"xrefs": xrefs,
"state_byte_hints": _state_byte_hints(factory_entries, xrefs),
"bench_implications": [
"A live EEPROM dump should first compare F400-F4FF shadow-equivalent offsets against the ROM factory table, especially F402/F404 and mapped offsets.",
"Pages 0x0-0xF offset 0x00-0x07 are loaded into F7B0-F82F; page 0 carries the signature/options header and pages 1-F default to spaces, so non-space bytes there are high-value identity/config data.",
"Serial command 0/4 can mirror values into F400 offsets selected by the ROM mapping table, but command 4 only persists when the continuation path reaches BD2B and F76E.7 is set.",
"F76E is not just a page byte: bit7 gates EEPROM persistence, bit6 suppresses the 48FA dispatch path, and only its low nibble survives into the EEPROM page address.",
"CONNECT OK is still likely gated by volatile table/session state as well as EEPROM-backed defaults; EEPROM differences may explain why bench and emulator diverge, but are unlikely to be the whole protocol by themselves.",
],
"caveats": [
"The selector map proves where firmware mirrors/persists serial values, not what every field means to the panel UI.",
"High bytes in the C564 selector map look structured, but the observed command-0/command-4 paths only use the low byte for F400/EEPROM offsets.",
"Indexed F7B0-F82F record consumers can be missed by a static xref pass; dynamic emulator traces should be used once interesting record bytes are found.",
],
}
def format_text_report(analysis: Mapping[str, Any]) -> str:
summary = analysis["summary"]
lines = [
"H8/536 EEPROM Layout Report",
"",
f"Summary: {summary['model']}",
f"Confidence: {summary['confidence']}",
"",
"Physical / Logical Model:",
]
physical = analysis.get("physical_model", {})
for row in physical.get("banks", []):
lines.append(
f"- {row['name']}: logical {row['logical_range_hex']} control {row['control_write_hex']}/{row['control_read_hex']}"
)
lines.append(f"- bus: {physical.get('bus', 'unknown')}")
lines.append(f"- page model: {physical.get('page_model', 'unknown')}")
lines.extend(["", "Boot Flow:"])
for step in analysis.get("boot_flow", []):
lines.append(f"- {step['address_hex']} {step['name']}: {step['summary']}")
defaults = analysis.get("factory_defaults", {})
lines.extend(["", "Factory Shadow Block:"])
lines.append(
f"- ROM {defaults.get('rom_base_hex')} length 0x{int(defaults.get('byte_count', 0)):02X} "
f"mirrors to {defaults.get('shadow_range_hex')}"
)
for entry in defaults.get("notable_entries", [])[:32]:
details = []
if entry.get("ascii"):
details.append(f"ascii={entry['ascii']!r}")
if entry.get("mapped_selectors_hex"):
details.append(f"selectors={', '.join(entry['mapped_selectors_hex'][:8])}")
if entry.get("xref_count"):
details.append(f"xrefs={entry['xref_count']}")
suffix = f" ({'; '.join(details)})" if details else ""
lines.append(
f"- {entry['shadow_address_hex']} offset {entry['offset_hex']} "
f"default {entry['factory_word_hex']}{suffix}"
)
lines.extend(["", "Persistent 8-Byte Records:"])
records = analysis.get("persistent_records", [])
if records:
first = records[0]
last = records[-1]
lines.append(
f"- {len(records)} records: EEPROM {first['eeprom_range_hex']} .. {last['eeprom_range_hex']} "
f"load into RAM {first['ram_range_hex']} .. {last['ram_range_hex']}"
)
for record in records[:16]:
lines.append(
f" - record {record['record_index_hex']}: EEPROM {record['eeprom_range_hex']} "
f"-> RAM {record['ram_range_hex']} default {record['default_text']!r}"
)
mapping = analysis.get("serial_persistence_mapping", {})
lines.extend(["", "Serial Selector -> Shadow/EEPROM Mapping:"])
lines.append(
f"- table {mapping.get('table_base_hex')} has {mapping.get('mapped_entry_count', 0)} nonzero low-byte mappings"
)
lines.append(f"- formula: {mapping.get('address_formula')}")
for entry in mapping.get("entries", [])[:80]:
lines.append(
f" - selector {entry['selector_hex']} map_word={entry['mapping_word_hex']} "
f"-> {entry['shadow_address_hex']} page+{entry['eeprom_word_offset_hex']} "
f"default={entry.get('factory_word_hex', 'unknown')}"
)
omitted = int(mapping.get("mapped_entry_count", 0)) - min(80, len(mapping.get("entries", [])))
if omitted > 0:
lines.append(f" - ... {omitted} more mapped selectors omitted")
lines.extend(["", "State Byte Hints:"])
for hint in analysis.get("state_byte_hints", []):
lines.append(
f"- {hint['address_hex']} {hint['name']}: {hint['summary']} "
f"(xrefs={hint['xref_count']})"
)
lines.extend(["", "Bench Implications:"])
for item in analysis.get("bench_implications", []):
lines.append(f"- {item}")
lines.extend(["", "Caveats:"])
for item in analysis.get("caveats", []):
lines.append(f"- {item}")
return "\n".join(lines).rstrip() + "\n"
def write_eeprom_layout(
input_path: Path,
output_path: Path,
*,
rom_path: Path | None = DEFAULT_ROM,
as_json: bool = False,
) -> JsonObject:
analysis = analyze_eeprom_layout(load_eeprom_layout_input(input_path), rom_path=rom_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if as_json:
output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
else:
output_path.write_text(format_text_report(analysis), encoding="utf-8")
return analysis
def main(argv: list[str] | None = None, stdout: Any | None = None) -> int:
parser = argparse.ArgumentParser(description="Mine ROM-backed X24164 EEPROM layout and persistence hints.")
parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
parser.add_argument("--rom", type=Path, default=DEFAULT_ROM, help="ROM binary to mine")
parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text")
parser.add_argument("--out", type=Path, default=None, help="write report to this path")
args = parser.parse_args(argv)
stream = stdout
if stream is None:
import sys
stream = sys.stdout
rom_path = args.rom if args.rom and args.rom.exists() else None
analysis = analyze_eeprom_layout(load_eeprom_layout_input(args.input), rom_path=rom_path)
rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n" if args.json else format_text_report(analysis)
if args.out:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(rendered, encoding="utf-8")
print(f"wrote {args.out}", file=stream)
else:
print(rendered, end="", file=stream)
return 0
def _load_rom(path: Path | None) -> Rom | None:
if path is None or not path.exists():
return None
return Rom(path.read_bytes())
def _physical_model() -> JsonObject:
return {
"bus": "P91/SCL and P97/SDA bit-banged two-wire bus through ROM routines C121/C08B/C0DB/C10C/C142",
"page_size": X24164_LOGICAL_PAGE_SIZE,
"page_count": X24164_LOGICAL_PAGE_COUNT,
"page_model": "16 logical pages of 0x100 bytes; low 8 address bits are sent as the EEPROM word address",
"banks": [
{
"name": "lower_x24164_candidate",
"logical_range": [0x0000, 0x07FF],
"logical_range_hex": "0x000-0x7FF",
"control_write": 0xA0,
"control_write_hex": "A0",
"control_read": 0xA1,
"control_read_hex": "A1",
},
{
"name": "upper_x24164_candidate",
"logical_range": [0x0800, 0x0FFF],
"logical_range_hex": "0x800-0xFFF",
"control_write": 0xE0,
"control_write_hex": "E0",
"control_read": 0xE1,
"control_read_hex": "E1",
},
],
}
def _boot_flow() -> list[JsonObject]:
return [
{
"address": 0x40BB,
"address_hex": h16(0x40BB),
"name": "eeprom_boot_gate",
"summary": "initializes queue/table scratch, checks P7DR.7, then checks F402 == H'6B6F before trusting persisted state",
},
{
"address": 0x4103,
"address_hex": h16(0x4103),
"name": "factory_default_fill",
"summary": "copies ROM C964-CA63 into F400-F4FF and writes the same 0x100-byte defaults to each EEPROM page",
},
{
"address": 0x4187,
"address_hex": h16(0x4187),
"name": "record_header_blank",
"summary": "overwrites page offsets 0x00-0x07 on pages 0x1-0xF with four H'2020 words after factory replication; page 0 keeps the signature/options header",
},
{
"address": 0x41D2,
"address_hex": h16(0x41D2),
"name": "persistent_record_load",
"summary": "reads page offsets 0x00-0x07 from pages 0x0-0xF into F7B0-F82F; record 0 is a signature/options header, records 1-F are label/identity-like slots",
},
{
"address": 0xBD2B,
"address_hex": h16(0xBD2B),
"name": "serial_persist_path",
"summary": "command-4 continuation writes the live value into F400+map[selector] and persists it through BFE0 when F76E.7 is set",
},
]
def _factory_entries(rom: Rom | None) -> list[JsonObject]:
if rom is None:
return []
entries = []
for offset, word in factory_default_words_from_rom(rom.data):
shadow_address = SHADOW_BASE + offset
bytes_pair = [(word >> 8) & 0xFF, word & 0xFF]
entries.append(
{
"offset": offset,
"offset_hex": f"0x{offset:02X}",
"rom_address": X24164_FACTORY_DEFAULT_BASE + offset,
"rom_address_hex": h16(X24164_FACTORY_DEFAULT_BASE + offset),
"shadow_address": shadow_address,
"shadow_address_hex": h16(shadow_address),
"factory_word": word,
"factory_word_hex": f"0x{word:04X}",
"bytes_hex": " ".join(f"{byte:02X}" for byte in bytes_pair),
"ascii": _word_ascii(word),
"default_eeprom_word_after_record_blank": 0x2020 if offset < RECORD_BYTES else word,
"default_eeprom_word_after_record_blank_hex": "0x2020" if offset < RECORD_BYTES else f"0x{word:04X}",
}
)
return entries
def _selector_map_entries(rom: Rom | None, factory_entries: list[JsonObject]) -> list[JsonObject]:
if rom is None:
return []
defaults_by_offset = {int(entry["offset"]): entry for entry in factory_entries}
entries: list[JsonObject] = []
for selector in range(SELECTOR_MAP_COUNT):
address = SELECTOR_MAP_BASE + selector * 2
if not rom.contains(address, 2):
break
word = rom.u16(address)
low = word & 0xFF
if low == 0:
continue
aligned = low & 0xFE
shadow_address = SHADOW_BASE + low
aligned_shadow_address = SHADOW_BASE + aligned
default = defaults_by_offset.get(aligned)
entry: JsonObject = {
"selector": selector,
"selector_hex": f"0x{selector:03X}",
"entry_address": address,
"entry_address_hex": h16(address),
"mapping_word": word,
"mapping_word_hex": f"0x{word:04X}",
"mapping_high_byte": (word >> 8) & 0xFF,
"mapping_high_byte_hex": f"0x{(word >> 8) & 0xFF:02X}",
"mapping_low_byte": low,
"mapping_low_byte_hex": f"0x{low:02X}",
"shadow_offset": low,
"shadow_offset_hex": f"0x{low:02X}",
"shadow_address": shadow_address,
"shadow_address_hex": h16(shadow_address),
"aligned_shadow_address": aligned_shadow_address,
"aligned_shadow_address_hex": h16(aligned_shadow_address),
"eeprom_word_offset": aligned,
"eeprom_word_offset_hex": f"0x{aligned:02X}",
"command0_shadow_mirror": True,
"command4_shadow_mirror": True,
"command4_persist_when_f76e_bit7": True,
}
if default is not None:
entry["factory_word"] = default["factory_word"]
entry["factory_word_hex"] = default["factory_word_hex"]
entry["factory_ascii"] = default["ascii"]
entries.append(entry)
return entries
def _notable_factory_entries(
factory_entries: list[JsonObject],
xrefs: Mapping[str, Any],
selector_map: list[JsonObject],
) -> list[JsonObject]:
direct_counts: Counter[int] = Counter()
for item in xrefs.get("direct_xrefs", []):
if not isinstance(item, Mapping) or not isinstance(item.get("address"), int):
continue
address = int(item["address"])
if SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE:
direct_counts[address & 0xFF] += 1
selectors_by_offset: dict[int, list[str]] = {}
for entry in selector_map:
selectors_by_offset.setdefault(int(entry["eeprom_word_offset"]), []).append(str(entry["selector_hex"]))
notable = []
for entry in factory_entries:
offset = int(entry["offset"])
word = int(entry["factory_word"])
mapped = selectors_by_offset.get(offset, [])
xref_count = direct_counts.get(offset, 0)
if word in (0x0000, 0xFFFF) and not mapped and not xref_count and offset not in (0x02, 0x04):
continue
item = dict(entry)
item["xref_count"] = xref_count
item["mapped_selectors_hex"] = mapped[:24]
notable.append(item)
return notable
def _persistent_records(factory_entries: list[JsonObject]) -> list[JsonObject]:
factory_by_offset = {int(entry["offset"]): int(entry["factory_word"]) for entry in factory_entries}
records = []
for index in range(X24164_LOGICAL_PAGE_COUNT):
eeprom_base = index * X24164_LOGICAL_PAGE_SIZE
ram_base = RECORD_RAM_BASE + index * RECORD_BYTES
default_words = [
factory_by_offset.get(offset, 0xFFFF) if index == 0 else 0x2020
for offset in range(0, RECORD_BYTES, 2)
]
default_bytes = bytearray()
for word in default_words:
default_bytes.extend([(word >> 8) & 0xFF, word & 0xFF])
records.append(
{
"record_index": index,
"record_index_hex": f"0x{index:X}",
"eeprom_base": eeprom_base,
"eeprom_base_hex": f"0x{eeprom_base:03X}",
"eeprom_range_hex": f"0x{eeprom_base:03X}-0x{eeprom_base + RECORD_BYTES - 1:03X}",
"ram_base": ram_base,
"ram_base_hex": h16(ram_base),
"ram_range_hex": f"{h16(ram_base)}-{h16(ram_base + RECORD_BYTES - 1)}",
"word_offsets": [0, 2, 4, 6],
"default_words": default_words,
"default_words_hex": [f"0x{word:04X}" for word in default_words],
"default_bytes_hex": default_bytes.hex(" ").upper(),
"default_text": _ascii(default_bytes),
"role_candidate": "page-0 signature/options header" if index == 0 else "8-byte persistent label/identity slot",
}
)
return records
def _offset_histogram(entries: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
counts = Counter(int(entry["eeprom_word_offset"]) for entry in entries)
return [
{"offset": offset, "offset_hex": f"0x{offset:02X}", "selector_count": count}
for offset, count in sorted(counts.items())
]
def _high_byte_histogram(entries: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
counts = Counter(int(entry["mapping_high_byte"]) for entry in entries)
return [
{"high_byte": value, "high_byte_hex": f"0x{value:02X}", "selector_count": count}
for value, count in sorted(counts.items())
]
def _state_byte_hints(factory_entries: list[JsonObject], xrefs: Mapping[str, Any]) -> list[JsonObject]:
defaults = {int(entry["shadow_address"]): entry for entry in factory_entries}
refs_by_address: Counter[int] = Counter()
examples_by_address: dict[int, list[JsonObject]] = {}
for item in xrefs.get("direct_xrefs", []):
if not isinstance(item, Mapping) or not isinstance(item.get("address"), int):
continue
address = int(item["address"])
refs_by_address[address] += 1
examples_by_address.setdefault(address, []).append(dict(item))
hints = []
for address, name in STATE_BYTES:
default = defaults.get(address)
summary = _state_summary(address, default)
hints.append(
{
"address": address,
"address_hex": h16(address),
"name": name,
"summary": summary,
"factory_word_hex": default.get("factory_word_hex") if default else None,
"xref_count": refs_by_address.get(address, 0),
"xrefs": examples_by_address.get(address, [])[:12],
}
)
return hints
def _state_summary(address: int, default: Mapping[str, Any] | None) -> str:
factory = f"factory {default['factory_word_hex']}" if default else "volatile/no factory word"
if address == 0xF402:
return f"{factory}; boot accepts persisted state only when this word is H'6B6F"
if address == 0xF404:
return f"{factory}; bits 1-4 are tested with F791 gates in display/status routines"
if address == 0xF76E:
return "bit7 enables command-4 EEPROM persistence, bit6 suppresses 48FA dispatch, low nibble selects EEPROM page"
if address == 0xF791:
return "volatile gate tested alongside F404 option bits before setting report/display flags"
if address == 0xF732:
return "volatile display dispatch selector feeding the 493E pointer table and 48FA report bridge"
return factory
def _xref_summary(payload: Mapping[str, Any]) -> JsonObject:
instructions = _instruction_sequence(payload.get("instructions"))
functions = _function_ranges(payload)
direct = []
dynamic = []
for ins in instructions:
for ref in _references(ins):
if _is_interesting_address(ref):
direct.append(_xref_item(ins, ref, functions))
operands = str(ins.get("operands", ""))
dynamic_kind = _dynamic_kind(operands)
if dynamic_kind:
item = _base_instruction_item(ins, functions)
item["kind"] = dynamic_kind
item["operand"] = operands
dynamic.append(item)
return {
"direct_xref_count": len(direct),
"dynamic_xref_count": len(dynamic),
"direct_xrefs": direct,
"dynamic_xrefs": dynamic,
}
def _xref_item(ins: Mapping[str, Any], address: int, functions: list[JsonObject]) -> JsonObject:
item = _base_instruction_item(ins, functions)
item.update(
{
"address": address,
"address_hex": h16(address),
"region": _region_for_address(address),
"access": _access_direction(ins, address) or "read_write_candidate",
}
)
return item
def _base_instruction_item(ins: Mapping[str, Any], functions: list[JsonObject]) -> JsonObject:
address = int(ins["address"])
function = _function_for_address(functions, address)
item: JsonObject = {
"instruction_address": address,
"instruction_address_hex": h16(address),
"mnemonic": str(ins.get("mnemonic", "")),
"operands": str(ins.get("operands", "")),
"instruction": str(ins.get("text") or _instruction_text(ins)),
}
if function:
item["function_start"] = function["start"]
item["function_start_hex"] = h16(int(function["start"]))
item["function_label"] = function["label"]
return item
def _is_interesting_address(address: int) -> bool:
return (
SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE
or RECORD_RAM_BASE <= address < RECORD_RAM_BASE + X24164_LOGICAL_PAGE_COUNT * RECORD_BYTES
or any(address == item[0] for item in STATE_BYTES)
)
def _region_for_address(address: int) -> str:
if SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE:
return "f400_shadow_defaults"
if RECORD_RAM_BASE <= address < RECORD_RAM_BASE + X24164_LOGICAL_PAGE_COUNT * RECORD_BYTES:
return "persistent_record_ram"
return "state_or_gate_ram"
def _dynamic_kind(operands: str) -> str | None:
patterns = (
("@(-H'0C00", "indexed_f400_shadow_access"),
("@(-H'0850", "indexed_persistent_record_store"),
("@(-H'084E", "indexed_persistent_record_store"),
("@(-H'084C", "indexed_persistent_record_store"),
("@(-H'084A", "indexed_persistent_record_store"),
("@(-H'3A9C", "selector_to_shadow_mapping_word"),
("@(-H'3A9B", "selector_to_shadow_mapping_low_byte"),
)
upper = operands.upper()
for pattern, kind in patterns:
if pattern in upper:
return kind
return None
def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
call_graph = payload.get("call_graph")
if not isinstance(call_graph, Mapping):
return []
nodes = call_graph.get("nodes")
if not isinstance(nodes, list):
return []
ranges: list[JsonObject] = []
for node in nodes:
if not isinstance(node, Mapping):
continue
start = node.get("start")
end = node.get("end")
if isinstance(start, int) and isinstance(end, int):
ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))})
return sorted(ranges, key=lambda item: int(item["start"]))
def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None:
for function in functions:
if int(function["start"]) <= address <= int(function["end"]):
return function
return None
def _instruction_sequence(value: object) -> list[JsonObject]:
if isinstance(value, Mapping):
values: Iterable[Any] = value.values()
elif isinstance(value, list):
values = value
else:
values = []
return sorted(
[item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)],
key=lambda item: int(item["address"]),
)
def _references(ins: Mapping[str, Any]) -> list[int]:
refs = ins.get("references", [])
if not isinstance(refs, list):
return []
output: list[int] = []
for ref in refs:
if isinstance(ref, Mapping) and isinstance(ref.get("address"), int):
output.append(int(ref["address"]))
elif isinstance(ref, int):
output.append(ref)
return output
def _access_direction(ins: Mapping[str, Any], address: int) -> str | None:
root = _mnemonic_root(str(ins.get("mnemonic", "")))
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}:
return "read"
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
return "write"
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
return "write"
if root in {"MOV:G", "MOV:S", "MOVTPE"}:
source, destination = _source_destination_operands(str(ins.get("operands", "")))
if _operand_mentions_address(destination, address):
return "write"
if _operand_mentions_address(source, address):
return "read"
return None
def _source_destination_operands(operands: str) -> tuple[str, str]:
depth = 0
split_at: int | None = None
for index, char in enumerate(operands):
if char in "({":
depth += 1
elif char in ")}" and depth:
depth -= 1
elif char == "," and depth == 0:
split_at = index
if split_at is None:
operand = operands.strip()
return "", operand
return operands[:split_at].strip(), operands[split_at + 1 :].strip()
def _operand_mentions_address(operand: str, address: int) -> bool:
operand_upper = operand.upper().replace(" ", "")
negative = (0x10000 - address) & 0xFFFF
return (
f"H'{address:04X}" in operand_upper
or f"0X{address:04X}" in operand_upper
or f"${address:04X}" in operand_upper
or f"-H'{negative:04X}" in operand_upper
or f"-0X{negative:04X}" in operand_upper
or f"-${negative:04X}" in operand_upper
)
def _instruction_text(ins: Mapping[str, Any]) -> str:
operands = str(ins.get("operands", ""))
return f"{ins.get('mnemonic', '')} {operands}".strip()
def _mnemonic_root(mnemonic: str) -> str:
return mnemonic.rsplit(".", 1)[0].upper()
def _word_ascii(word: int) -> str:
chars = [(word >> 8) & 0xFF, word & 0xFF]
if all(0x20 <= byte <= 0x7E for byte in chars):
return "".join(chr(byte) for byte in chars)
if any(0x20 <= byte <= 0x7E for byte in chars):
return "".join(chr(byte) if 0x20 <= byte <= 0x7E else "." for byte in chars)
return ""
def _ascii(data: bytes | bytearray) -> str:
return "".join(chr(value) if 0x20 <= value <= 0x7E else "." for value in data)
__all__ = [
"analyze_eeprom_layout",
"format_text_report",
"load_eeprom_layout_input",
"main",
"write_eeprom_layout",
]

View File

@@ -4,6 +4,7 @@ import argparse
from pathlib import Path
from ..formatting import h16, parse_int
from .eeprom_image import write_eeprom_snapshot
from .memory import describe_regions
from .runner import H8536Emulator
@@ -47,6 +48,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true", help="legacy fallback for older wrapper experiments; known BFE0/BFFE wrappers use the X24164 model")
parser.add_argument("--p7-input", type=parse_int, default=0xFF, help="external P7 pin state for input bits; DIP-off board default is 0xFF")
parser.add_argument("--eeprom-seed", choices=("blank", "factory"), default="blank", help="initial X24164/shadow state before reset")
parser.add_argument("--eeprom-load", type=Path, help="load a 0x1000-byte logical EEPROM image before running")
parser.add_argument("--eeprom-save", type=Path, help="save the final 0x1000-byte logical EEPROM image after running")
parser.add_argument("--eeprom-report", type=Path, help="write a readable EEPROM snapshot report after running")
parser.add_argument("--eeprom-report-json", type=Path, help="write a structured EEPROM snapshot report after running")
parser.add_argument("--eeprom-report-include-image", action="store_true", help="include the full EEPROM image as hex in JSON reports")
return parser
@@ -70,6 +76,9 @@ def main(argv: list[str] | None = None) -> int:
p7_input=args.p7_input,
eeprom_seed=args.eeprom_seed,
)
if args.eeprom_load:
emulator.memory.load_eeprom_image(args.eeprom_load.read_bytes())
print(f"eeprom_loaded={args.eeprom_load}")
print(f"rom={rom_path}")
print(f"reset_vector={h16(emulator.reset_vector())}")
if args.memory_map:
@@ -82,4 +91,20 @@ def main(argv: list[str] | None = None) -> int:
print(line)
if not report.heartbeat_seen:
print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR")
if args.eeprom_save:
args.eeprom_save.parent.mkdir(parents=True, exist_ok=True)
args.eeprom_save.write_bytes(emulator.memory.dump_eeprom_image())
print(f"eeprom_saved={args.eeprom_save}")
if args.eeprom_report:
write_eeprom_snapshot(emulator.memory, args.eeprom_report, rom_bytes=rom_bytes)
print(f"eeprom_report={args.eeprom_report}")
if args.eeprom_report_json:
write_eeprom_snapshot(
emulator.memory,
args.eeprom_report_json,
rom_bytes=rom_bytes,
as_json=True,
include_image_hex=args.eeprom_report_include_image,
)
print(f"eeprom_report_json={args.eeprom_report_json}")
return 0

View File

@@ -0,0 +1,351 @@
from __future__ import annotations
import hashlib
import json
from collections import defaultdict
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from ..formatting import h16
from .memory import MemoryMap
from .peripherals.x24164 import (
X24164_FACTORY_DEFAULT_BYTES,
X24164_LOGICAL_PAGE_COUNT,
X24164_LOGICAL_PAGE_SIZE,
X24164_LOGICAL_SIZE,
factory_default_words_from_rom,
)
JsonObject = dict[str, Any]
SELECTOR_MAP_BASE = 0xC564
SELECTOR_MAP_COUNT = 0x0200
RECORD_BYTES = 8
SHADOW_BASE = 0xF400
def build_eeprom_snapshot(
memory: MemoryMap,
*,
rom_bytes: bytes | None = None,
include_image_hex: bool = False,
) -> JsonObject:
image = memory.dump_eeprom_image()
selectors_by_offset = _selectors_by_offset(rom_bytes)
factory_image = _factory_image(rom_bytes)
write_events = _write_events(memory, selectors_by_offset)
write_words = _coalesced_write_words(memory, selectors_by_offset)
factory_diffs = _factory_diffs(image, factory_image, selectors_by_offset)
report: JsonObject = {
"kind": "emulator_eeprom_snapshot",
"summary": {
"logical_size": len(image),
"logical_size_hex": f"0x{len(image):04X}",
"sha256": hashlib.sha256(image).hexdigest(),
"write_byte_count": len(write_events),
"write_word_count": len(write_words),
"factory_diff_word_count": len(factory_diffs),
"record_count": X24164_LOGICAL_PAGE_COUNT,
},
"records": _records(image),
"write_events": write_events,
"write_word_events": write_words,
"factory_diffs": factory_diffs,
"shadow_f400": _shadow_summary(memory, rom_bytes, selectors_by_offset),
}
if include_image_hex:
report["image_hex"] = image.hex()
return report
def format_eeprom_snapshot(report: Mapping[str, Any], *, limit: int = 80) -> str:
summary = report["summary"]
lines = [
"Emulator EEPROM Snapshot",
"",
f"size={summary['logical_size_hex']} sha256={summary['sha256']}",
(
f"writes: bytes={summary['write_byte_count']} words={summary['write_word_count']} "
f"factory_diff_words={summary['factory_diff_word_count']}"
),
"",
"Persistent Records:",
]
for record in report.get("records", []):
lines.append(
f"- page {record['page_hex']} EEPROM {record['range_hex']} "
f"bytes={record['bytes_hex']} text={record['ascii']!r}"
)
lines.extend(["", "EEPROM Word Writes:"])
word_events = list(report.get("write_word_events", []))
if not word_events:
lines.append("- none since EEPROM setup/load")
for event in word_events[:limit]:
suffix = _event_suffix(event)
lines.append(
f"- {event['address_hex']} page={event['page_hex']} offset={event['offset_hex']} "
f"{event['old_word_hex']}->{event['new_word_hex']} source={event['source']}{suffix}"
)
if len(word_events) > limit:
lines.append(f"- ... {len(word_events) - limit} more word writes omitted")
lines.extend(["", "Factory Diffs:"])
diffs = list(report.get("factory_diffs", []))
if not diffs:
lines.append("- current EEPROM image matches ROM factory/default image")
for diff in diffs[:limit]:
suffix = _event_suffix(diff)
lines.append(
f"- {diff['address_hex']} page={diff['page_hex']} offset={diff['offset_hex']} "
f"expected={diff['expected_word_hex']} actual={diff['actual_word_hex']}{suffix}"
)
if len(diffs) > limit:
lines.append(f"- ... {len(diffs) - limit} more factory diffs omitted")
shadow = report.get("shadow_f400", {})
lines.extend(["", "F400 Shadow Diffs:"])
shadow_diffs = list(shadow.get("diffs", [])) if isinstance(shadow, Mapping) else []
if not shadow_diffs:
lines.append("- F400-F4FF shadow matches ROM factory words or no ROM factory baseline was supplied")
for diff in shadow_diffs[:limit]:
suffix = _event_suffix(diff)
lines.append(
f"- {diff['address_hex']} offset={diff['offset_hex']} "
f"expected={diff['expected_word_hex']} actual={diff['actual_word_hex']}{suffix}"
)
if len(shadow_diffs) > limit:
lines.append(f"- ... {len(shadow_diffs) - limit} more shadow diffs omitted")
return "\n".join(lines).rstrip() + "\n"
def write_eeprom_snapshot(
memory: MemoryMap,
output_path: Path,
*,
rom_bytes: bytes | None = None,
as_json: bool = False,
include_image_hex: bool = False,
) -> JsonObject:
report = build_eeprom_snapshot(memory, rom_bytes=rom_bytes, include_image_hex=include_image_hex)
output_path.parent.mkdir(parents=True, exist_ok=True)
if as_json:
output_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
else:
output_path.write_text(format_eeprom_snapshot(report), encoding="utf-8")
return report
def _write_events(memory: MemoryMap, selectors_by_offset: Mapping[int, list[int]]) -> list[JsonObject]:
events = []
for index, event in enumerate(memory.p9_bus.x24164_bus.write_events):
item = _address_info(event.logical_address, selectors_by_offset)
item.update(
{
"index": index,
"device": event.device,
"device_offset": event.device_offset,
"device_offset_hex": f"0x{event.device_offset:03X}",
"old_value": event.old_value,
"old_value_hex": f"0x{event.old_value & 0xFF:02X}",
"new_value": event.new_value,
"new_value_hex": f"0x{event.new_value & 0xFF:02X}",
"source": event.source,
}
)
events.append(item)
return events
def _coalesced_write_words(memory: MemoryMap, selectors_by_offset: Mapping[int, list[int]]) -> list[JsonObject]:
events = memory.p9_bus.x24164_bus.write_events
words: list[JsonObject] = []
index = 0
while index < len(events):
event = events[index]
next_event = events[index + 1] if index + 1 < len(events) else None
if (
next_event is not None
and (event.logical_address & 1) == 0
and next_event.logical_address == ((event.logical_address + 1) & 0x0FFF)
and next_event.device == event.device
and next_event.source == event.source
):
old_word = ((event.old_value & 0xFF) << 8) | (next_event.old_value & 0xFF)
new_word = ((event.new_value & 0xFF) << 8) | (next_event.new_value & 0xFF)
item = _address_info(event.logical_address, selectors_by_offset)
item.update(
{
"index": index,
"device": event.device,
"old_word": old_word,
"old_word_hex": f"0x{old_word:04X}",
"new_word": new_word,
"new_word_hex": f"0x{new_word:04X}",
"source": event.source,
}
)
words.append(item)
index += 2
else:
index += 1
return words
def _factory_diffs(
image: bytes,
factory_image: bytes | None,
selectors_by_offset: Mapping[int, list[int]],
) -> list[JsonObject]:
if factory_image is None:
return []
diffs = []
for address in range(0, min(len(image), len(factory_image)), 2):
expected = (factory_image[address] << 8) | factory_image[address + 1]
actual = (image[address] << 8) | image[address + 1]
if expected == actual:
continue
item = _address_info(address, selectors_by_offset)
item.update(
{
"expected_word": expected,
"expected_word_hex": f"0x{expected:04X}",
"actual_word": actual,
"actual_word_hex": f"0x{actual:04X}",
}
)
diffs.append(item)
return diffs
def _shadow_summary(
memory: MemoryMap,
rom_bytes: bytes | None,
selectors_by_offset: Mapping[int, list[int]],
) -> JsonObject:
if rom_bytes is None:
return {"diffs": [], "note": "no ROM factory baseline supplied"}
factory_words = dict(factory_default_words_from_rom(rom_bytes))
diffs = []
for offset in range(0, X24164_FACTORY_DEFAULT_BYTES, 2):
expected = factory_words[offset]
address = SHADOW_BASE + offset
high = memory.external.get(address & 0xFFFF, 0xFF)
low = memory.external.get((address + 1) & 0xFFFF, 0xFF)
actual = ((high & 0xFF) << 8) | (low & 0xFF)
if expected == actual:
continue
item = _address_info(offset, selectors_by_offset)
item.update(
{
"address": address,
"address_hex": h16(address),
"expected_word": expected,
"expected_word_hex": f"0x{expected:04X}",
"actual_word": actual,
"actual_word_hex": f"0x{actual:04X}",
}
)
diffs.append(item)
return {"diffs": diffs, "diff_count": len(diffs)}
def _records(image: bytes) -> list[JsonObject]:
records = []
for page in range(X24164_LOGICAL_PAGE_COUNT):
base = page * X24164_LOGICAL_PAGE_SIZE
data = image[base : base + RECORD_BYTES]
records.append(
{
"page": page,
"page_hex": f"0x{page:X}",
"address": base,
"address_hex": f"0x{base:03X}",
"range_hex": f"0x{base:03X}-0x{base + RECORD_BYTES - 1:03X}",
"bytes_hex": data.hex(" ").upper(),
"words_hex": [
f"0x{((data[index] << 8) | data[index + 1]):04X}"
for index in range(0, len(data), 2)
],
"ascii": _ascii(data),
"is_blank_spaces": data == (b" " * RECORD_BYTES),
}
)
return records
def _factory_image(rom_bytes: bytes | None) -> bytes | None:
if rom_bytes is None:
return None
image = bytearray([0xFF] * X24164_LOGICAL_SIZE)
for offset, word in factory_default_words_from_rom(rom_bytes):
for page in range(X24164_LOGICAL_PAGE_COUNT):
address = (page * X24164_LOGICAL_PAGE_SIZE) + offset
image[address] = (word >> 8) & 0xFF
image[address + 1] = word & 0xFF
for page in range(1, X24164_LOGICAL_PAGE_COUNT):
base = page * X24164_LOGICAL_PAGE_SIZE
for offset in range(0, RECORD_BYTES, 2):
image[base + offset] = 0x20
image[base + offset + 1] = 0x20
return bytes(image)
def _selectors_by_offset(rom_bytes: bytes | None) -> dict[int, list[int]]:
if rom_bytes is None:
return {}
result: dict[int, list[int]] = defaultdict(list)
for selector in range(SELECTOR_MAP_COUNT):
address = SELECTOR_MAP_BASE + selector * 2
if address + 1 >= len(rom_bytes):
break
low = rom_bytes[address + 1]
if low:
result[low & 0xFE].append(selector)
return dict(result)
def _address_info(address: int, selectors_by_offset: Mapping[int, list[int]]) -> JsonObject:
address &= 0x0FFF
page = (address // X24164_LOGICAL_PAGE_SIZE) & 0x0F
offset = address & 0xFF
aligned_offset = offset & 0xFE
selectors = selectors_by_offset.get(aligned_offset, [])
return {
"address": address,
"address_hex": f"0x{address:03X}",
"page": page,
"page_hex": f"0x{page:X}",
"offset": offset,
"offset_hex": f"0x{offset:02X}",
"aligned_offset": aligned_offset,
"aligned_offset_hex": f"0x{aligned_offset:02X}",
"record_byte": offset if offset < RECORD_BYTES else None,
"role": "record_header_or_label" if offset < RECORD_BYTES else "factory_shadow_offset",
"mapped_selectors": selectors[:24],
"mapped_selectors_hex": [f"0x{selector:03X}" for selector in selectors[:24]],
}
def _event_suffix(event: Mapping[str, Any]) -> str:
parts = []
if event.get("role"):
parts.append(str(event["role"]))
selectors = event.get("mapped_selectors_hex")
if isinstance(selectors, list) and selectors:
parts.append("selectors=" + ",".join(str(item) for item in selectors[:6]))
return f" ({'; '.join(parts)})" if parts else ""
def _ascii(data: bytes) -> str:
return "".join(chr(value) if 0x20 <= value <= 0x7E else "." for value in data)
__all__ = [
"build_eeprom_snapshot",
"format_eeprom_snapshot",
"write_eeprom_snapshot",
]

View File

@@ -161,6 +161,20 @@ class MemoryMap:
self.p9_bus.x24164_bus.seed_factory_defaults_from_rom(self.rom.data)
self.p9_bus.clear_x24164_trace()
def load_eeprom_image(self, data: bytes | bytearray, *, mirror_shadow: bool = True) -> None:
self.p9_bus.x24164_bus.load_linear(data)
if mirror_shadow:
image = self.p9_bus.x24164_bus.dump_linear()
for offset in range(min(0x0100, len(image))):
self.external[(0xF400 + offset) & 0xFFFF] = image[offset]
self.p9_bus.clear_x24164_trace()
def dump_eeprom_image(self) -> bytes:
return self.p9_bus.x24164_bus.dump_linear()
def clear_eeprom_write_log(self) -> None:
self.p9_bus.x24164_bus.clear_write_log()
def _set_register(self, address: int, value: int) -> None:
self.registers[address - REGISTER_FIELD_START] = value & 0xFF

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from .lcd import LCD, LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS, LCD_LINE_WIDTH
from .p9_bus import P9_ACK_BIT, P9_STROBE_BIT, P9Bus, P9StrobeEvent, P9TraceEvent
from .x24164 import X24164Bus, X24164Device, X24164TraceEvent, factory_default_words_from_rom
from .x24164 import X24164Bus, X24164Device, X24164TraceEvent, X24164WriteEvent, factory_default_words_from_rom
__all__ = [
"LCD_E_CLOCK_DATA",
@@ -17,5 +17,6 @@ __all__ = [
"X24164Bus",
"X24164Device",
"X24164TraceEvent",
"X24164WriteEvent",
"factory_default_words_from_rom",
]

View File

@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
X24164_SIZE = 2048
X24164_LOGICAL_SIZE = 4096
X24164_FACTORY_DEFAULT_BASE = 0xC964
X24164_FACTORY_DEFAULT_BYTES = 0x0100
X24164_LOGICAL_PAGE_SIZE = 0x0100
@@ -72,12 +73,30 @@ class X24164TraceEvent:
return " ".join(parts)
@dataclass(frozen=True)
class X24164WriteEvent:
logical_address: int
device: str
device_offset: int
old_value: int
new_value: int
source: str
def line(self) -> str:
return (
f"addr={self.logical_address & 0x0FFF:03X} device={self.device} "
f"offset={self.device_offset & (X24164_SIZE - 1):03X} "
f"{self.old_value & 0xFF:02X}->{self.new_value & 0xFF:02X} source={self.source}"
)
class X24164Bus:
"""Bit-level two-wire bus model for X24164 EEPROMs."""
def __init__(self, devices: list[X24164Device] | None = None) -> None:
self.devices = devices if devices is not None else default_x24164_devices()
self.trace_events: list[X24164TraceEvent] = []
self.write_events: list[X24164WriteEvent] = []
self.active = False
self.phase = "idle"
self.selected: X24164Device | None = None
@@ -197,6 +216,18 @@ class X24164Bus:
)
return True, value
def read_linear_byte(self, address: int) -> tuple[bool, int]:
device = self._device_for_linear_address(address)
if device is None:
self.trace_events.append(
X24164TraceEvent("x24164_linear_read_miss", address=address & 0x0FFF, message="no_mapped_device")
)
return False, 0xFF
offset = address & (X24164_SIZE - 1)
value = device.read(offset)
self.trace_events.append(X24164TraceEvent("x24164_linear_read_byte", device.name, value=value, address=offset))
return True, value
def write_linear_word(self, address: int, value: int) -> bool:
device = self._device_for_linear_address(address)
if device is None:
@@ -205,8 +236,8 @@ class X24164Bus:
)
return False
offset = address & (X24164_SIZE - 1)
device.write(offset, (value >> 8) & 0xFF)
device.write((offset + 1) & (X24164_SIZE - 1), value & 0xFF)
self._write_device_byte(device, offset, (value >> 8) & 0xFF, source="linear_word")
self._write_device_byte(device, (offset + 1) & (X24164_SIZE - 1), value & 0xFF, source="linear_word")
self.trace_events.append(
X24164TraceEvent(
"x24164_linear_write_word",
@@ -218,20 +249,61 @@ class X24164Bus:
)
return True
def write_linear_byte(self, address: int, value: int, *, source: str = "linear_byte") -> bool:
device = self._device_for_linear_address(address)
if device is None:
self.trace_events.append(
X24164TraceEvent("x24164_linear_write_miss", address=address & 0x0FFF, message="no_mapped_device")
)
return False
offset = address & (X24164_SIZE - 1)
self._write_device_byte(device, offset, value, source=source)
self.trace_events.append(X24164TraceEvent("x24164_linear_write_byte", device.name, value=value & 0xFF, address=offset))
return True
def dump_linear(self) -> bytes:
data = bytearray()
for address in range(X24164_LOGICAL_SIZE):
device = self._device_for_linear_address(address)
if device is None:
data.append(0xFF)
else:
data.append(device.read(address & (X24164_SIZE - 1)))
return bytes(data)
def load_linear(self, data: bytes | bytearray, *, fill: int = 0xFF) -> None:
if len(data) > X24164_LOGICAL_SIZE:
raise ValueError(f"EEPROM image is too large: {len(data)} > {X24164_LOGICAL_SIZE}")
padded = bytearray([fill & 0xFF] * X24164_LOGICAL_SIZE)
padded[: len(data)] = data
for address, value in enumerate(padded):
device = self._device_for_linear_address(address)
if device is not None:
device.write(address & (X24164_SIZE - 1), value)
self.clear_write_log()
def clear_write_log(self) -> None:
self.write_events.clear()
def seed_factory_defaults_from_rom(self, rom_bytes: bytes) -> None:
for offset, word in factory_default_words_from_rom(rom_bytes):
for page in range(X24164_LOGICAL_PAGE_COUNT):
self.write_linear_word((page * X24164_LOGICAL_PAGE_SIZE) + offset, word)
for page in range(X24164_LOGICAL_PAGE_COUNT):
for page in range(1, X24164_LOGICAL_PAGE_COUNT):
base = page * X24164_LOGICAL_PAGE_SIZE
for offset in range(0, 8, 2):
self.write_linear_word(base + offset, 0x2020)
self.clear_write_log()
def trace_lines(self, limit: int | None = None) -> list[str]:
events = self.trace_events if limit is None else self.trace_events[-limit:]
return [event.line() for event in events]
def write_log_lines(self, limit: int | None = None) -> list[str]:
events = self.write_events if limit is None else self.write_events[-limit:]
return [event.line() for event in events]
def _scl_rising(self, master_sda: bool, master_sda_output: bool) -> None:
if not self.active:
return
@@ -327,7 +399,7 @@ class X24164Bus:
self._ack_armed_on_current_clock = True
self.phase = "ignore"
return
self.selected.write(self.address, value)
self._write_device_byte(self.selected, self.address, value, source="bit_banged")
self.trace_events.append(
X24164TraceEvent("x24164_write_data", self.selected.name, value=value, address=self.address, ack=True)
)
@@ -365,6 +437,24 @@ class X24164Bus:
return device
return None
def _linear_base_for_device(self, device: X24164Device) -> int:
return 0x0800 if device.control_base == 0xE0 else 0x0000
def _write_device_byte(self, device: X24164Device, offset: int, value: int, *, source: str) -> None:
offset &= X24164_SIZE - 1
old_value = device.read(offset)
device.write(offset, value)
self.write_events.append(
X24164WriteEvent(
logical_address=(self._linear_base_for_device(device) + offset) & 0x0FFF,
device=device.name,
device_offset=offset,
old_value=old_value,
new_value=value & 0xFF,
source=source,
)
)
def _selected_name(self) -> str | None:
return self.selected.name if self.selected is not None else None

View File

@@ -15,6 +15,7 @@ from .constants import (
SCI_SSR_RDRF,
VECTOR_SCI1_RXI,
)
from .eeprom_image import write_eeprom_snapshot
from .errors import UnsupportedInstruction
from .memory import MemoryAccess
from .runner import H8536Emulator
@@ -216,6 +217,7 @@ def run_rx_probe(
p9_fast_optimistic_wrapper: bool = False,
p7_input: int = 0xFF,
eeprom_seed: str = "blank",
eeprom_image: bytes | None = None,
stop_after_tx_frame: bool = True,
) -> tuple[Path, H8536Emulator, str, list[FrameResult]]:
rom_bytes, discovered_rom_path = load_rom(rom_path)
@@ -231,6 +233,8 @@ def run_rx_probe(
p7_input=p7_input,
eeprom_seed=eeprom_seed,
)
if eeprom_image is not None:
emulator.memory.load_eeprom_image(eeprom_image)
boot_context = RunContext()
boot_steps_used, boot_reason = _run_until(emulator, boot_steps, _rx_ready, boot_context)
@@ -277,6 +281,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true", help="legacy fallback for older wrapper experiments; known BFE0/BFFE wrappers use the X24164 model")
parser.add_argument("--p7-input", type=parse_int, default=0xFF, help="external P7 pin state for input bits; DIP-off board default is 0xFF")
parser.add_argument("--eeprom-seed", choices=("blank", "factory"), default="blank", help="initial X24164/shadow state before reset")
parser.add_argument("--eeprom-load", type=Path, help="load a 0x1000-byte logical EEPROM image before booting the ROM")
parser.add_argument("--eeprom-save", type=Path, help="save the final 0x1000-byte logical EEPROM image after probing")
parser.add_argument("--eeprom-report", type=Path, help="write a readable EEPROM snapshot report after probing")
parser.add_argument("--eeprom-report-json", type=Path, help="write a structured EEPROM snapshot report after probing")
parser.add_argument("--eeprom-report-include-image", action="store_true", help="include the full EEPROM image as hex in JSON reports")
return parser
@@ -305,16 +314,35 @@ def main(argv: list[str] | None = None) -> int:
p9_fast_optimistic_wrapper=args.p9_fast_optimistic_wrapper,
p7_input=args.p7_input,
eeprom_seed=args.eeprom_seed,
eeprom_image=args.eeprom_load.read_bytes() if args.eeprom_load else None,
stop_after_tx_frame=not args.keep_listening,
)
print(f"rom={rom_path}")
if args.eeprom_load:
print(f"eeprom_loaded={args.eeprom_load}")
print(f"reset_vector={h16(emulator.reset_vector())}")
print(boot_summary)
for index, result in enumerate(results):
for line in result.lines(index):
print(line)
print("total_tx_frames=" + " | ".join(format_frame(frame) for frame in emulator.sci1.tx_frames))
if args.eeprom_save:
args.eeprom_save.parent.mkdir(parents=True, exist_ok=True)
args.eeprom_save.write_bytes(emulator.memory.dump_eeprom_image())
print(f"eeprom_saved={args.eeprom_save}")
if args.eeprom_report:
write_eeprom_snapshot(emulator.memory, args.eeprom_report, rom_bytes=emulator.memory.rom.data)
print(f"eeprom_report={args.eeprom_report}")
if args.eeprom_report_json:
write_eeprom_snapshot(
emulator.memory,
args.eeprom_report_json,
rom_bytes=emulator.memory.rom.data,
as_json=True,
include_image_hex=args.eeprom_report_include_image,
)
print(f"eeprom_report_json={args.eeprom_report_json}")
return 0

8
h8536_eeprom_layout.py Normal file
View File

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

120
tests/test_eeprom_layout.py Normal file
View File

@@ -0,0 +1,120 @@
import io
import json
import tempfile
import unittest
from pathlib import Path
from h8536.eeprom_layout import (
analyze_eeprom_layout,
format_text_report,
main,
write_eeprom_layout,
)
def reference(address: int) -> dict:
return {"address": address}
def instruction(address: int, mnemonic: str, operands: str = "", refs: list[int] | None = None) -> dict:
return {
"address": address,
"mnemonic": mnemonic,
"operands": operands,
"text": f"{mnemonic} {operands}".strip(),
"references": [reference(item) for item in (refs or [])],
"targets": [],
}
def payload() -> dict:
return {
"instructions": [
instruction(0x410C, "MOV:G.W", "R5, @(-H'0C00,R0)"),
instruction(0x41E8, "MOV:G.W", "R5, @(-H'0850,R1)"),
instruction(0x40FA, "CMP:G.W", "#H'6B6F, @H'F402", [0xF402]),
instruction(0xBD3D, "MOV:G.B", "@(-H'3A9B,R4), R1"),
instruction(0xBD45, "MOV:G.W", "R0, @(-H'0C00,R1)"),
instruction(0xBD49, "BTST.B", "#7, @H'F76E", [0xF76E]),
],
"call_graph": {
"nodes": [
{"start": 0x4000, "end": 0x4216, "label": "loc_4000"},
{"start": 0xBD0E, "end": 0xBD67, "label": "loc_BD0E"},
],
},
}
def rom_bytes() -> bytes:
rom = bytearray([0xFF] * 0xCB00)
rom[0xC564 : 0xC564 + 0x400] = b"\x00" * 0x400
rom[0xC564 + 0x015 * 2 : 0xC564 + 0x015 * 2 + 2] = b"\x40\xAA"
rom[0xC966:0xC968] = b"\x6B\x6F"
rom[0xC968:0xC96A] = b"\xFE\x00"
rom[0xCA0E:0xCA10] = b"\x80\x00"
return bytes(rom)
class EepromLayoutTest(unittest.TestCase):
def test_analysis_extracts_factory_defaults_records_and_selector_mapping(self):
with tempfile.TemporaryDirectory() as tmp:
rom_path = Path(tmp) / "rom.bin"
rom_path.write_bytes(rom_bytes())
analysis = analyze_eeprom_layout(payload(), rom_path=rom_path)
self.assertEqual(analysis["kind"], "eeprom_layout")
self.assertEqual(analysis["factory_defaults"]["entries"][1]["factory_word_hex"], "0x6B6F")
self.assertEqual(analysis["persistent_records"][3]["ram_base_hex"], "H'F7C8")
self.assertEqual(analysis["persistent_records"][3]["eeprom_base_hex"], "0x300")
mapped = analysis["serial_persistence_mapping"]["entries"][0]
self.assertEqual(mapped["selector_hex"], "0x015")
self.assertEqual(mapped["shadow_address_hex"], "H'F4AA")
self.assertEqual(mapped["eeprom_word_offset_hex"], "0xAA")
self.assertEqual(mapped["factory_word_hex"], "0x8000")
def test_text_report_mentions_core_layout(self):
with tempfile.TemporaryDirectory() as tmp:
rom_path = Path(tmp) / "rom.bin"
rom_path.write_bytes(rom_bytes())
analysis = analyze_eeprom_layout(payload(), rom_path=rom_path)
text = format_text_report(analysis)
self.assertIn("Persistent 8-Byte Records", text)
self.assertIn("selector 0x015", text)
self.assertIn("F76E", text)
def test_write_json_output(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"
output_path = Path(tmp) / "eeprom.json"
rom_path = Path(tmp) / "rom.bin"
input_path.write_text(json.dumps(payload()), encoding="utf-8")
rom_path.write_bytes(rom_bytes())
write_eeprom_layout(input_path, output_path, rom_path=rom_path, as_json=True)
written = json.loads(output_path.read_text(encoding="utf-8"))
self.assertEqual(written["kind"], "eeprom_layout")
self.assertEqual(written["summary"]["persistent_record_count"], 16)
def test_cli_writes_text_report(self):
with tempfile.TemporaryDirectory() as tmp:
input_path = Path(tmp) / "rom.json"
output_path = Path(tmp) / "eeprom.txt"
rom_path = Path(tmp) / "rom.bin"
input_path.write_text(json.dumps(payload()), encoding="utf-8")
rom_path.write_bytes(rom_bytes())
stdout = io.StringIO()
rc = main([str(input_path), "--rom", str(rom_path), "--out", str(output_path)], stdout=stdout)
self.assertEqual(rc, 0)
self.assertIn("wrote", stdout.getvalue())
self.assertIn("EEPROM Layout Report", output_path.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()

View File

@@ -37,7 +37,8 @@ class EmulatorAddressingTest(unittest.TestCase):
self.assertEqual(memory.read16(0xF402), 0x6B6F)
self.assertEqual(memory.p9_bus.fast_read_word(0x0010), (True, 0x8000))
self.assertEqual(memory.p9_bus.fast_read_word(0x0110), (True, 0x8000))
self.assertEqual(memory.p9_bus.fast_read_word(0x0002), (True, 0x2020))
self.assertEqual(memory.p9_bus.fast_read_word(0x0002), (True, 0x6B6F))
self.assertEqual(memory.p9_bus.fast_read_word(0x0102), (True, 0x2020))
def test_txi_indexed_byte_load_uses_signed_word_displacement_and_full_index_register(self):
rom = rom_with_reset()

View File

@@ -0,0 +1,66 @@
import json
import tempfile
import unittest
from pathlib import Path
from h8536.emulator.eeprom_image import build_eeprom_snapshot, format_eeprom_snapshot, write_eeprom_snapshot
from h8536.emulator.memory import MemoryMap
from h8536.emulator.peripherals.x24164 import X24164_LOGICAL_SIZE
def rom_bytes() -> bytes:
rom = bytearray([0xFF] * 0xCB00)
rom[0xC564 : 0xC564 + 0x400] = b"\x00" * 0x400
rom[0xC564 + 0x18 * 2 : 0xC564 + 0x18 * 2 + 2] = b"\x60\x10"
rom[0xC964 : 0xC964 + 0x100] = b"\x00" * 0x100
rom[0xC966:0xC968] = b"\x6B\x6F"
rom[0xC974:0xC976] = b"\x80\x00"
return bytes(rom)
class EmulatorEepromImageTest(unittest.TestCase):
def test_load_dump_and_write_log_cover_full_logical_image(self):
memory = MemoryMap(rom_bytes())
image = bytes([index & 0xFF for index in range(X24164_LOGICAL_SIZE)])
memory.load_eeprom_image(image)
self.assertEqual(memory.dump_eeprom_image(), image)
self.assertEqual(memory.external[0xF400], 0x00)
self.assertEqual(memory.external[0xF402], 0x02)
self.assertTrue(memory.p9_bus.fast_write_word(0x0810, 0x1234))
self.assertEqual(memory.p9_bus.fast_read_word(0x0810), (True, 0x1234))
self.assertEqual(len(memory.p9_bus.x24164_bus.write_events), 2)
self.assertIn("addr=810", memory.p9_bus.x24164_bus.write_log_lines()[-2])
def test_snapshot_reports_records_factory_diffs_and_mapped_writes(self):
memory = MemoryMap(rom_bytes())
memory.seed_factory_eeprom_and_shadow()
memory.p9_bus.fast_write_word(0x0110, 0x1234)
report = build_eeprom_snapshot(memory, rom_bytes=rom_bytes())
text = format_eeprom_snapshot(report)
self.assertEqual(report["summary"]["write_word_count"], 1)
self.assertEqual(report["write_word_events"][0]["new_word_hex"], "0x1234")
self.assertEqual(report["write_word_events"][0]["mapped_selectors_hex"], ["0x018"])
self.assertEqual(report["factory_diffs"][0]["address_hex"], "0x110")
self.assertIn("EEPROM Word Writes", text)
self.assertIn("selector", text)
def test_write_snapshot_json(self):
memory = MemoryMap(rom_bytes())
memory.seed_factory_eeprom_and_shadow()
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "eeprom.json"
write_eeprom_snapshot(memory, output, rom_bytes=rom_bytes(), as_json=True, include_image_hex=True)
payload = json.loads(output.read_text(encoding="utf-8"))
self.assertEqual(payload["kind"], "emulator_eeprom_snapshot")
self.assertEqual(len(payload["image_hex"]), X24164_LOGICAL_SIZE * 2)
if __name__ == "__main__":
unittest.main()