1
0

Bench testing to find out contents of EPROM

This commit is contained in:
Aiden
2026-05-25 23:48:22 +10:00
parent 3f9f03388c
commit 1e4f87675d
4 changed files with 246 additions and 0 deletions

View File

@@ -224,6 +224,7 @@ python h8536_emulator_rx_probe.py --help
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses. - `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
- `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with 8N1 wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`. - `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with 8N1 wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`.
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates. - `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
- `scripts\serial_table_dump.py --port COM5 --relay-port COM6 --start 0x000 --count 0x200 --log captures\table-read.txt`: read-only command-1 sweep of the firmware-exposed serial table state for EEPROM/shadow inference.
- `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text. - `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text.
- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode. - `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode.
- Current status: boots from `H'1000`, initializes SCI1, models the traced X24164 EEPROM bus on P9, captures P9 byte candidates, can optionally fast-path known P9 EEPROM routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`. - Current status: boots from `H'1000`, initializes SCI1, models the traced X24164 EEPROM bus on P9, captures P9 byte candidates, can optionally fast-path known P9 EEPROM routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`.
@@ -270,3 +271,4 @@ python h8536_emulator_rx_probe.py --help
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers. - `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers. - `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers.
- `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence. - `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence.
- `scripts/serial_table_dump.py`: read-only COM5/COM6 command-1 table sweep for inferring live EEPROM-backed parameter state.

198
h8536/serial_table_dump.py Normal file
View File

@@ -0,0 +1,198 @@
from __future__ import annotations
import argparse
import sys
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import TextIO
from .bench_connect_lcd import (
BenchLogger,
FrameDetector,
_import_serial,
_read_for,
_relay_command,
_relay_settle,
_wait_for_ready,
format_frame,
frame_checksum,
frame_checksum_ok,
)
READ_COMMAND = 0x01
FRAME_LENGTH = 6
@dataclass(frozen=True)
class SelectorEncoding:
selector: int
frame_hi: int
frame_lo: int
def encode_selector(selector: int) -> SelectorEncoding:
selector &= 0x01FF
if selector <= 0x007F:
return SelectorEncoding(selector, 0x00, selector)
if selector <= 0x017F:
return SelectorEncoding(selector, 0x01, selector - 0x0080)
return SelectorEncoding(selector, 0x02, selector - 0x0180)
def build_read_frame(selector: int) -> bytes:
encoded = encode_selector(selector)
body = bytes([READ_COMMAND, encoded.frame_hi, encoded.frame_lo, 0x00, 0x00])
return body + bytes([frame_checksum(body)])
def decode_table_read_response(frame: bytes) -> tuple[int, int] | None:
if len(frame) != FRAME_LENGTH or not frame_checksum_ok(frame):
return None
if frame[0] != 0x04:
return None
return frame[2], (frame[3] << 8) | frame[4]
def default_log_path() -> Path:
return Path("captures") / f"serial-table-dump-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Read-only serial table sweep. This uses command 1 frames only; it does not write EEPROM."
)
)
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP")
parser.add_argument("--baud", type=int, default=38400, help="RCP serial baud rate")
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port")
parser.add_argument("--relay-baud", type=int, default=115200, help="Pico relay serial baud rate")
parser.add_argument("--no-power-cycle", action="store_true", help="do not send relay off/on before the sweep")
parser.add_argument("--power-off-command", default="off", help="relay command used to remove DUT power")
parser.add_argument("--power-on-command", default="on", help="relay command used to apply DUT power")
parser.add_argument("--off-seconds", type=float, default=1.5, help="seconds to hold the DUT powered off")
parser.add_argument("--relay-settle", type=float, default=2.0, help="seconds to wait after opening the relay port")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="seconds to wait for heartbeat before reading")
parser.add_argument("--ready-heartbeats", type=int, default=2, help="heartbeat frames to observe before reading")
parser.add_argument("--require-ready", action="store_true", help="abort if ready heartbeat count is not observed")
parser.add_argument("--start", type=lambda text: int(text, 0), default=0x000, help="first logical selector")
parser.add_argument("--count", type=lambda text: int(text, 0), default=0x80, help="number of selectors to read")
parser.add_argument("--selector", action="append", type=lambda text: int(text, 0), help="specific selector to read; repeatable")
parser.add_argument("--gap", type=float, default=0.080, help="seconds to listen after each read frame")
parser.add_argument("--pre-drain", type=float, default=0.250, help="seconds to drain/log RX before the sweep")
parser.add_argument("--post-read", type=float, default=1.0, help="seconds to listen after the sweep")
parser.add_argument("--log", type=Path, help="capture log path")
parser.add_argument("--dry-run", action="store_true", help="print planned read frames without opening serial ports")
return parser
def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
args = build_arg_parser().parse_args(argv)
selectors = _selectors(args)
log_path = args.log or default_log_path()
if args.dry_run:
print(f"device={args.port} {args.baud} 8N1", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle={int(not args.no_power_cycle)}", file=stdout)
for selector in selectors:
encoded = encode_selector(selector)
frame = build_read_frame(selector)
print(
f"selector=0x{selector:03X} encoded={encoded.frame_hi:02X} {encoded.frame_lo:02X} "
f"frame={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}",
file=stdout,
)
print(f"log={log_path}", file=stdout)
return 0
serial = _import_serial()
logger = BenchLogger(log_path, stdout=stdout)
detector = FrameDetector()
response_rows: list[tuple[int, bytes, tuple[int, int] | None]] = []
try:
logger.emit("Read-only serial table sweep")
logger.emit(f"device={args.port} {args.baud} 8N1 relay={args.relay_port} {args.relay_baud}")
logger.emit(f"log={log_path}")
logger.emit(f"selectors={len(selectors)} command=01 write_frames=0")
with serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0.05) as device:
relay = None
try:
if not args.no_power_cycle:
relay = serial.Serial(args.relay_port, args.relay_baud, timeout=0.25)
_relay_settle(relay, args.relay_settle, logger)
_relay_command(relay, args.power_off_command, logger)
time.sleep(args.off_seconds)
device.reset_input_buffer()
detector = FrameDetector()
_relay_command(relay, args.power_on_command, logger)
else:
device.reset_input_buffer()
ready = _wait_for_ready(device, detector, logger, args.ready_timeout, args.ready_heartbeats)
if args.require_ready and not ready:
logger.event("ABORT ready heartbeat threshold was not observed")
return 2
if args.pre_drain > 0:
logger.event(f"DRAIN before read sweep {args.pre_drain:.3f}s")
_read_for(device, detector, logger, args.pre_drain)
for selector in selectors:
frame = build_read_frame(selector)
before = len(detector.frames)
logger.event(f"READ selector=0x{selector:03X} frame={format_frame(frame)}")
device.write(frame)
device.flush()
logger.chunk("TX", frame)
_read_for(device, detector, logger, args.gap)
for response in detector.frames[before:]:
decoded = decode_table_read_response(response)
if decoded is not None:
page_or_echo, value = decoded
logger.event(
f"TABLE selector=0x{selector:03X} echo={page_or_echo:02X} value={value:04X}"
)
response_rows.append((selector, response, decoded))
_read_for(device, detector, logger, args.post_read)
finally:
if relay is not None:
relay.close()
_summary(response_rows, detector, logger)
return 0
finally:
logger.close()
def _selectors(args: argparse.Namespace) -> list[int]:
if args.selector:
return [selector & 0x01FF for selector in args.selector]
start = args.start & 0x01FF
count = max(0, args.count)
return [((start + offset) & 0x01FF) for offset in range(count)]
def _summary(
response_rows: list[tuple[int, bytes, tuple[int, int] | None]],
detector: FrameDetector,
logger: BenchLogger,
) -> None:
table_rows = [(selector, decoded[1]) for selector, _frame, decoded in response_rows if decoded is not None]
logger.emit()
logger.emit("Summary")
logger.emit(f"rx_frames={len(detector.frames)} table_response_rows={len(table_rows)}")
for selector, value in table_rows:
logger.emit(f"table selector=0x{selector:03X} value=0x{value:04X}")
__all__ = [
"build_arg_parser",
"build_read_frame",
"decode_table_read_response",
"encode_selector",
"main",
]

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""Read-only command-1 serial table sweep helper."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from h8536.serial_table_dump import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,32 @@
import unittest
from h8536.bench_connect_lcd import frame_checksum_ok
from h8536.serial_table_dump import build_read_frame, decode_table_read_response, encode_selector
class SerialTableDumpTest(unittest.TestCase):
def test_encode_selector_matches_rom_loc_622b_ranges(self):
self.assertEqual((encode_selector(0x000).frame_hi, encode_selector(0x000).frame_lo), (0x00, 0x00))
self.assertEqual((encode_selector(0x07F).frame_hi, encode_selector(0x07F).frame_lo), (0x00, 0x7F))
self.assertEqual((encode_selector(0x080).frame_hi, encode_selector(0x080).frame_lo), (0x01, 0x00))
self.assertEqual((encode_selector(0x17F).frame_hi, encode_selector(0x17F).frame_lo), (0x01, 0xFF))
self.assertEqual((encode_selector(0x180).frame_hi, encode_selector(0x180).frame_lo), (0x02, 0x00))
self.assertEqual((encode_selector(0x1FF).frame_hi, encode_selector(0x1FF).frame_lo), (0x02, 0x7F))
def test_build_read_frame_uses_command_1_and_checksum(self):
frame = build_read_frame(0x180)
self.assertEqual(frame[:5], bytes.fromhex("01 02 00 00 00"))
self.assertTrue(frame_checksum_ok(frame))
def test_decode_table_read_response_extracts_value_candidate(self):
frame = bytes.fromhex("04 00 12 80 80 4C")
self.assertEqual(decode_table_read_response(frame), (0x12, 0x8080))
def test_decode_table_read_response_ignores_non_readback(self):
self.assertIsNone(decode_table_read_response(bytes.fromhex("00 00 00 00 80 DA")))
if __name__ == "__main__":
unittest.main()