1
0

non-volatile storage emulation

This commit is contained in:
Aiden
2026-05-25 23:16:41 +10:00
parent 0c241877eb
commit 0819701b22
12 changed files with 647 additions and 36 deletions

View File

@@ -86,7 +86,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, 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`, 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.
@@ -102,6 +102,8 @@ Current serial observations:
- Emulator timing finding: the ROM initializes FRT2 with `TCR=H'02` and `OCRA=H'7A12`; using the manual's `phi/32` prescaler gives a 1,000,000-cycle OCIA period, so the default `--clock-hz 10000000` models that tick as 100 ms and the post-send `F9C4=H'07` heartbeat delay as about 700 ms.
- Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`.
- Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue.
- 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.
- 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.
- Bench follow-up: replaying the emulator CONNECT sequence on the real device did not switch the LCD to OK. The real device answered the `04 00 00 80 00 DE` step with `07 80 C0 60 20 5D` in the captured run and remained at `CONNECT NOT ACT`, so the next mismatch to chase is the missing visible `07 80 C0 60 20 5D` response/session context rather than the LCD OK branch.
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
@@ -212,8 +214,8 @@ python h8536_emulator_rx_probe.py --help
- `--interval-steps N`: tune the scaffolded interval timer cadence.
- `--clock-hz N`: set the CPU/phi clock used for calibrated FRT1/FRT2 compare timing; the default is 10 MHz.
- `--frt1-ocia-steps N` / `--frt2-ocia-steps N`: optional legacy overrides for forcing rough FRT compare cadence in targeted tests.
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration. Fast-path wrapper calls now default to timeout unless a modeled P9 response is queued, and the probe reports decoded P9 transaction/fast-path traces.
- `--p9-fast-optimistic-wrapper`: restore the older behavior where P9 fast-path wrapper calls succeed without a modeled external-panel response.
- `--p9-fast-path`: shortcut known P9 transfer routines for exploration. Fast-path byte/marker calls now feed the X24164 EEPROM model, and `BFE0/BFFE` wrapper shortcuts perform EEPROM word write-verify/read operations against the modeled banks.
- `--p9-fast-optimistic-wrapper`: legacy fallback for older wrapper experiments; the known `BFE0/BFFE` EEPROM wrappers now use the X24164 model instead.
- `--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.
@@ -221,7 +223,7 @@ python h8536_emulator_rx_probe.py --help
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
- `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.
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, 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`.
## Code Layout
@@ -252,7 +254,7 @@ 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/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 bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding.
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, 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/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
- `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path.
- `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis.

View File

@@ -55,7 +55,7 @@ from .cpu import CPUState
from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent
from .memory import MemoryAccess, MemoryMap, describe_regions
from .peripherals import LCD, P9TraceEvent
from .peripherals import LCD, P9TraceEvent, X24164Bus, X24164Device, X24164TraceEvent
from .runner import H8536Emulator, RunReport
from .sci import SCI1, SciTxEvent
from .uart import UartTiming
@@ -124,6 +124,9 @@ __all__ = [
"VECTOR_SCI1_RXI",
"VECTOR_SCI1_TXI",
"WDT_TCSR_R",
"X24164Bus",
"X24164Device",
"X24164TraceEvent",
"build_arg_parser",
"describe_regions",
"discover_rom_path",

View File

@@ -368,7 +368,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--frt2-ocia-steps", type=int, default=ReplayConfig.frt2_ocia_steps)
parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines")
parser.add_argument("--p9-fast-input", type=lambda text: int(text, 0), default=ReplayConfig.p9_fast_input)
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true", help="make P9 fast-path wrapper calls succeed when no modeled P9 response is queued")
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("--assert-bench-parity", action="store_true", help="exit nonzero if emulator behavior diverges from the bench log")
parser.add_argument("--json", action="store_true", help="emit JSON")
return parser

View File

@@ -44,7 +44,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--frt2-ocia-steps", type=int, default=None, help="legacy step-period override for FRT2 OCIA")
parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine")
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true", help="make P9 fast-path wrapper calls succeed when no modeled P9 response is queued")
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")
return parser

View File

@@ -10,19 +10,22 @@ LOC_BFE0_TRANSFER_WRAPPER = 0xBFE0
LOC_BFFE_TRANSFER_WRAPPER = 0xBFFE
LOC_C08B_P9_WRITE_BYTE = 0xC08B
LOC_C0DB_P9_READ_BYTE = 0xC0DB
LOC_C10C_P9_MASTER_ACK = 0xC10C
LOC_C10C_P9_MARKER = 0xC10C
LOC_C121_P9_MARKER = 0xC121
LOC_C142_P9_MARKER = 0xC142
LOC_C121_P9_START = 0xC121
LOC_C121_P9_MARKER = LOC_C121_P9_START
LOC_C142_P9_STOP = 0xC142
LOC_C142_P9_MARKER = LOC_C142_P9_STOP
@dataclass(frozen=True)
class P9FastPathConfig:
"""Configuration for optional ROM P9 transfer shortcuts.
The helper assumes the CPU PC is exactly at a known routine entry. It
models the routine as if it completed successfully and returned via RTS.
Integration should keep this disabled unless the runner intentionally opts
into skipping these ROM routines.
The helper assumes the CPU PC is exactly at a known routine entry. It feeds
those byte/marker/wrapper operations into the modeled P9/X24164 bus and
returns via RTS. Integration should keep this disabled unless the runner
intentionally opts into skipping these ROM routines.
"""
enabled: bool = False
@@ -100,8 +103,7 @@ class P9FastPath:
elif pc == (self.config.read_byte_pc & 0xFFFF):
self._handle_read_byte(emulator)
elif pc in self.config.marker_pcs:
self.events.append(P9FastPathEvent("marker", pc))
self._return_from_subroutine(emulator)
self._handle_marker(emulator, pc)
elif pc in self.config.wrapper_pcs:
self._handle_wrapper(emulator)
else:
@@ -116,10 +118,12 @@ class P9FastPath:
pc = emulator.cpu.pc & 0xFFFF
value = emulator.cpu.regs[0] & 0xFF
self.output_bytes.append(value)
self.events.append(P9FastPathEvent("write_byte", pc, value))
success = emulator.memory.p9_bus.fast_write_byte(value)
self.events.append(P9FastPathEvent("write_byte", pc, value, source="x24164", success=success))
emulator.cpu.regs[0] = 1
self._set_logic_flags(emulator.cpu, 1, 1)
result = 1 if success else 0
emulator.cpu.regs[0] = result
self._set_logic_flags(emulator.cpu, result, 1)
self._return_from_subroutine(emulator)
def _handle_read_byte(self, emulator: Any) -> None:
@@ -127,6 +131,9 @@ class P9FastPath:
if self.input_bytes:
value = self.input_bytes.pop(0)
source = self.input_sources.pop(0) if self.input_sources else "queued"
elif (x24164_value := emulator.memory.p9_bus.fast_read_byte()) is not None:
value = x24164_value
source = "x24164"
else:
value = self.config.default_input_byte
source = "default_input_byte"
@@ -141,6 +148,26 @@ class P9FastPath:
def _handle_wrapper(self, emulator: Any) -> None:
pc = emulator.cpu.pc & 0xFFFF
success: bool
source: str
queue_depth: int | None
if pc == (LOC_BFE0_TRANSFER_WRAPPER & 0xFFFF):
address = emulator.cpu.regs[4] & 0x0FFF
value = emulator.cpu.regs[5] & 0xFFFF
write_success = emulator.memory.p9_bus.fast_write_word(address, value)
read_success, readback = emulator.memory.p9_bus.fast_read_word(address)
success = write_success and read_success and readback == value
source = "x24164_write_verify"
queue_depth = None
elif pc == (LOC_BFFE_TRANSFER_WRAPPER & 0xFFFF):
address = emulator.cpu.regs[4] & 0x0FFF
read_success, value = emulator.memory.p9_bus.fast_read_word(address)
if read_success:
emulator.cpu.regs[5] = value & 0xFFFF
success = read_success
source = "x24164_read_word"
queue_depth = None
else:
success, source, queue_depth = emulator.memory.p9_bus.consume_wrapper_result()
value = 1 if success else 0
self.events.append(
@@ -163,6 +190,20 @@ class P9FastPath:
emulator.cpu.pc = emulator.memory.read16(sp) & 0xFFFF
emulator.cpu.regs[7] = (sp + 2) & 0xFFFF
def _handle_marker(self, emulator: Any, pc: int) -> None:
if pc == (LOC_C121_P9_START & 0xFFFF):
emulator.memory.p9_bus.fast_start()
self.events.append(P9FastPathEvent("start", pc, source="x24164"))
elif pc == (LOC_C10C_P9_MASTER_ACK & 0xFFFF):
emulator.memory.p9_bus.fast_master_ack(True)
self.events.append(P9FastPathEvent("master_ack", pc, source="x24164"))
elif pc == (LOC_C142_P9_STOP & 0xFFFF):
emulator.memory.p9_bus.fast_stop()
self.events.append(P9FastPathEvent("stop", pc, source="x24164"))
else:
self.events.append(P9FastPathEvent("marker", pc))
self._return_from_subroutine(emulator)
def _set_logic_flags(self, cpu: Any, value: int, size: int) -> None:
value &= mask(size)
cpu.z = value == 0

View File

@@ -2,6 +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
__all__ = [
"LCD_E_CLOCK_DATA",
@@ -13,4 +14,7 @@ __all__ = [
"P9Bus",
"P9StrobeEvent",
"P9TraceEvent",
"X24164Bus",
"X24164Device",
"X24164TraceEvent",
]

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from .x24164 import X24164Bus
P9_ACK_BIT = 0x80
P9_STROBE_BIT = 0x02
@@ -27,6 +29,7 @@ class P9TraceEvent:
source: str | None = None
success: bool | None = None
queue_depth: int | None = None
message: str | None = None
def line(self) -> str:
parts = [self.kind, f"ddr={self.ddr:02X}", f"dr={self.dr:02X}"]
@@ -40,11 +43,18 @@ class P9TraceEvent:
parts.append(f"source={self.source}")
if self.queue_depth is not None:
parts.append(f"queued={self.queue_depth}")
if self.message is not None:
parts.append(self.message)
return " ".join(parts)
class P9Bus:
"""Small model for the ROM's P9 bit-banged serial handshake."""
"""Small model for the ROM's P9 bit-banged serial handshake.
Board tracing ties P91/P97 to X24164 SCL/SDA. The legacy bit queue is
retained for tests and exploratory scripts, while the X24164 model now
drives SDA during recognized EEPROM transactions.
"""
def __init__(
self,
@@ -52,6 +62,7 @@ class P9Bus:
dr: int = 0x00,
input_bits: Iterable[int] = (),
wrapper_results: Iterable[bool] = (),
x24164_bus: X24164Bus | None = None,
) -> None:
self.ddr = ddr & 0xFF
self.dr_latch = dr & 0xFF
@@ -64,6 +75,8 @@ class P9Bus:
self.trace_events: list[P9TraceEvent] = []
self.transmitted_bits: list[int] = []
self.byte_candidates: list[int] = []
self.x24164_bus = x24164_bus if x24164_bus is not None else X24164Bus()
self._x24164_trace_index = 0
def write_ddr(self, value: int) -> int:
self.ddr = value & 0xFF
@@ -72,6 +85,7 @@ class P9Bus:
def write_dr(self, value: int) -> int:
previous = self.dr_latch
previous_ddr = self.ddr
self.dr_latch = value & 0xFF
self.trace_events.append(P9TraceEvent("write_dr", self.ddr, self.dr_latch, value=self.dr_latch))
@@ -86,6 +100,14 @@ class P9Bus:
if edge == "rising" and bit7_output:
self._record_transmitted_bit(data_bit)
self.x24164_bus.observe(
previous_scl=bool(previous & P9_STROBE_BIT),
previous_master_sda=bool(previous & P9_ACK_BIT) if previous_ddr & P9_ACK_BIT else True,
current_scl=bool(self.dr_latch & P9_STROBE_BIT),
current_master_sda=bool(self.dr_latch & P9_ACK_BIT) if self.ddr & P9_ACK_BIT else True,
master_sda_output=bool(self.ddr & P9_ACK_BIT),
)
self._append_x24164_trace()
return self.dr_latch
def read_ddr(self) -> int:
@@ -96,9 +118,13 @@ class P9Bus:
input_bit = None
source = None
if not (self.ddr & P9_ACK_BIT):
x24164_bit = self.x24164_bus.sda_bit()
if self.input_bits:
input_bit = self.input_bits.pop(0)
source = "queued_bit"
elif x24164_bit is not None:
input_bit = x24164_bit
source = "x24164"
else:
input_bit = self.default_input_bit
source = "default_bit"
@@ -145,6 +171,72 @@ class P9Bus:
events = self.trace_events if limit is None else self.trace_events[-limit:]
return [event.line() for event in events]
def fast_start(self) -> None:
self.x24164_bus.fast_start()
self._append_x24164_trace()
def fast_stop(self) -> None:
self.x24164_bus.fast_stop()
self._append_x24164_trace()
def fast_master_ack(self, ack: bool = True) -> None:
self.x24164_bus.fast_master_ack(ack)
self._append_x24164_trace()
def fast_write_byte(self, value: int) -> bool:
success = self.x24164_bus.fast_write_byte(value)
self.trace_events.append(
P9TraceEvent(
"fast_write_byte",
self.ddr,
self.dr_latch,
value=value & 0xFF,
success=success,
source="x24164",
)
)
self._append_x24164_trace()
return success
def fast_read_byte(self) -> int | None:
value = self.x24164_bus.fast_read_byte()
if value is not None:
self.trace_events.append(P9TraceEvent("fast_read_byte", self.ddr, self.dr_latch, value=value, source="x24164"))
self._append_x24164_trace()
return value
def fast_read_word(self, address: int) -> tuple[bool, int]:
success, value = self.x24164_bus.read_linear_word(address)
self.trace_events.append(
P9TraceEvent(
"fast_read_word",
self.ddr,
self.dr_latch,
value=(value >> 8) & 0xFF,
success=success,
source="x24164",
message=f"addr={address & 0x0FFF:03X} word={value & 0xFFFF:04X}",
)
)
self._append_x24164_trace()
return success, value
def fast_write_word(self, address: int, value: int) -> bool:
success = self.x24164_bus.write_linear_word(address, value)
self.trace_events.append(
P9TraceEvent(
"fast_write_word",
self.ddr,
self.dr_latch,
value=(value >> 8) & 0xFF,
success=success,
source="x24164",
message=f"addr={address & 0x0FFF:03X} word={value & 0xFFFF:04X}",
)
)
self._append_x24164_trace()
return success
def _record_transmitted_bit(self, bit: int) -> None:
self.transmitted_bits.append(bit)
self.trace_events.append(P9TraceEvent("tx_bit", self.ddr, self.dr_latch, bit=bit))
@@ -154,3 +246,9 @@ class P9Bus:
byte = (byte << 1) | data_bit
self.byte_candidates.append(byte)
self.trace_events.append(P9TraceEvent("tx_byte", self.ddr, self.dr_latch, value=byte))
def _append_x24164_trace(self) -> None:
new_events = self.x24164_bus.trace_events[self._x24164_trace_index :]
self._x24164_trace_index = len(self.x24164_bus.trace_events)
for event in new_events:
self.trace_events.append(P9TraceEvent("x24164", self.ddr, self.dr_latch, message=event.line()))

View File

@@ -0,0 +1,362 @@
from __future__ import annotations
from dataclasses import dataclass, field
X24164_SIZE = 2048
@dataclass
class X24164Device:
"""Small Xicor X24164 serial EEPROM model.
The ROM uses two control-byte families on the P91/P97 two-wire bus:
H'A0/H'A1 for the low logical half and H'E0/H'E1 for the high logical half.
X24164 has unusual device-select encoding compared with later 24Cxx parts,
so the emulator stores the accepted high-nibble control family directly.
"""
name: str
control_base: int
data: bytearray = field(default_factory=lambda: bytearray([0xFF] * X24164_SIZE))
def __post_init__(self) -> None:
self.control_base &= 0xF0
if len(self.data) < X24164_SIZE:
self.data.extend([0xFF] * (X24164_SIZE - len(self.data)))
elif len(self.data) > X24164_SIZE:
del self.data[X24164_SIZE:]
def matches_control(self, value: int) -> bool:
return (value & 0xF0) == self.control_base
def offset_from_control(self, value: int, word_address: int) -> int:
high_address = (value >> 1) & 0x07
return ((high_address << 8) | (word_address & 0xFF)) & (X24164_SIZE - 1)
def read(self, offset: int) -> int:
return self.data[offset & (X24164_SIZE - 1)]
def write(self, offset: int, value: int) -> None:
self.data[offset & (X24164_SIZE - 1)] = value & 0xFF
@dataclass(frozen=True)
class X24164TraceEvent:
kind: str
device: str | None = None
value: int | None = None
address: int | None = None
bit: int | None = None
ack: bool | None = None
message: str | None = None
def line(self) -> str:
parts = [self.kind]
if self.device is not None:
parts.append(f"device={self.device}")
if self.address is not None:
parts.append(f"addr={self.address:03X}")
if self.value is not None:
parts.append(f"value={self.value:02X}")
if self.bit is not None:
parts.append(f"bit={self.bit}")
if self.ack is not None:
parts.append(f"ack={int(self.ack)}")
if self.message is not None:
parts.append(self.message)
return " ".join(parts)
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.active = False
self.phase = "idle"
self.selected: X24164Device | None = None
self.control_byte = 0
self.address = 0
self._rx_bits: list[int] = []
self._ack_pending: bool | None = None
self._ack_armed_on_current_clock = False
self._read_byte = 0xFF
self._read_bit_index = 0
self._read_prepared_on_current_clock = False
self._awaiting_master_ack = False
def observe(
self,
*,
previous_scl: bool,
previous_master_sda: bool,
current_scl: bool,
current_master_sda: bool,
master_sda_output: bool,
) -> None:
if previous_scl and current_scl and previous_master_sda != current_master_sda:
if previous_master_sda and not current_master_sda:
self.start()
elif not previous_master_sda and current_master_sda:
self.stop()
if not previous_scl and current_scl:
self._scl_rising(current_master_sda, master_sda_output)
elif previous_scl and not current_scl:
self._scl_falling()
def start(self) -> None:
self.active = True
self.phase = "control"
self.selected = None
self._rx_bits.clear()
self._ack_pending = None
self._ack_armed_on_current_clock = False
self._read_prepared_on_current_clock = False
self._awaiting_master_ack = False
self.trace_events.append(X24164TraceEvent("x24164_start"))
def stop(self) -> None:
if self.active:
self.trace_events.append(X24164TraceEvent("x24164_stop", device=self._selected_name()))
self.active = False
self.phase = "idle"
self.selected = None
self._rx_bits.clear()
self._ack_pending = None
self._ack_armed_on_current_clock = False
self._read_prepared_on_current_clock = False
self._awaiting_master_ack = False
def sda_bit(self) -> int | None:
if not self.active:
return None
if self._ack_pending is not None:
return 0 if self._ack_pending else 1
if self.phase == "read_data" and not self._awaiting_master_ack:
return (self._read_byte >> (7 - self._read_bit_index)) & 1
return 1
def fast_start(self) -> None:
self.start()
def fast_stop(self) -> None:
self.stop()
def fast_write_byte(self, value: int) -> bool:
self._accept_byte(value & 0xFF)
ack = bool(self._ack_pending)
self.trace_events.append(
X24164TraceEvent("x24164_fast_write_byte", self._selected_name(), value=value & 0xFF, ack=ack)
)
self._ack_pending = None
self._ack_armed_on_current_clock = False
if self.phase == "read_data" and self.selected is not None:
self._prepare_read_byte()
return ack
def fast_read_byte(self) -> int | None:
if not self.active or self.phase != "read_data" or self.selected is None:
return None
value = self.selected.read(self.address)
self.trace_events.append(
X24164TraceEvent("x24164_fast_read_byte", self.selected.name, value=value, address=self.address)
)
self.address = (self.address + 1) & (X24164_SIZE - 1)
self._prepare_read_byte()
return value
def fast_master_ack(self, ack: bool = True) -> None:
if not self.active:
return
self.trace_events.append(X24164TraceEvent("x24164_fast_master_ack", self._selected_name(), ack=ack))
if ack and self.phase == "read_data" and self.selected is not None:
self._prepare_read_byte()
elif not ack:
self.phase = "idle"
def read_linear_word(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, 0xFFFF
offset = address & (X24164_SIZE - 1)
high = device.read(offset)
low = device.read((offset + 1) & (X24164_SIZE - 1))
value = (high << 8) | low
self.trace_events.append(
X24164TraceEvent("x24164_linear_read_word", device.name, value=high, address=offset, message=f"word={value:04X}")
)
return True, value
def write_linear_word(self, address: int, value: int) -> 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)
device.write(offset, (value >> 8) & 0xFF)
device.write((offset + 1) & (X24164_SIZE - 1), value & 0xFF)
self.trace_events.append(
X24164TraceEvent(
"x24164_linear_write_word",
device.name,
value=(value >> 8) & 0xFF,
address=offset,
message=f"word={value & 0xFFFF:04X}",
)
)
return True
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 _scl_rising(self, master_sda: bool, master_sda_output: bool) -> None:
if not self.active:
return
if self._ack_pending is not None:
self.trace_events.append(X24164TraceEvent("x24164_ack_bit", self._selected_name(), ack=self._ack_pending))
return
if self.phase == "read_data":
if self._awaiting_master_ack:
ack = not master_sda if master_sda_output else False
self.trace_events.append(X24164TraceEvent("x24164_master_ack", self._selected_name(), ack=ack))
self._awaiting_master_ack = False
if ack and self.selected is not None:
self._prepare_read_byte(skip_current_falling=True)
else:
self.phase = "idle"
return
if master_sda_output:
self._rx_bits.append(1 if master_sda else 0)
self.trace_events.append(X24164TraceEvent("x24164_rx_bit", self._selected_name(), bit=self._rx_bits[-1]))
if len(self._rx_bits) == 8:
value = 0
for bit in self._rx_bits:
value = (value << 1) | bit
self._rx_bits.clear()
self._accept_byte(value)
def _scl_falling(self) -> None:
if not self.active:
return
if self._ack_pending is not None:
if self._ack_armed_on_current_clock:
self._ack_armed_on_current_clock = False
return
self._ack_pending = None
if self.phase == "read_data" and self.selected is not None:
self._prepare_read_byte()
return
if self.phase == "read_data" and not self._awaiting_master_ack:
if self._read_prepared_on_current_clock:
self._read_prepared_on_current_clock = False
return
if self._read_bit_index < 7:
self._read_bit_index += 1
else:
self.trace_events.append(
X24164TraceEvent("x24164_tx_byte_done", self._selected_name(), value=self._read_byte)
)
if self.selected is not None:
self.address = (self.address + 1) & (X24164_SIZE - 1)
self._awaiting_master_ack = True
def _accept_byte(self, value: int) -> None:
value &= 0xFF
if self.phase == "control":
self.control_byte = value
self.selected = self._device_for_control(value)
read_mode = bool(value & 1)
self._ack_pending = self.selected is not None
self._ack_armed_on_current_clock = True
self.trace_events.append(
X24164TraceEvent(
"x24164_control",
self._selected_name(),
value=value,
ack=self._ack_pending,
message="read" if read_mode else "write",
)
)
if self.selected is None:
self.phase = "ignore"
elif read_mode:
self.phase = "read_data"
else:
self.phase = "word_address"
return
if self.phase == "word_address":
if self.selected is None:
self._ack_pending = False
self._ack_armed_on_current_clock = True
self.phase = "ignore"
return
self.address = self.selected.offset_from_control(self.control_byte, value)
self._ack_pending = True
self._ack_armed_on_current_clock = True
self.phase = "write_data"
self.trace_events.append(
X24164TraceEvent("x24164_word_address", self.selected.name, value=value, address=self.address, ack=True)
)
return
if self.phase == "write_data":
if self.selected is None:
self._ack_pending = False
self._ack_armed_on_current_clock = True
self.phase = "ignore"
return
self.selected.write(self.address, value)
self.trace_events.append(
X24164TraceEvent("x24164_write_data", self.selected.name, value=value, address=self.address, ack=True)
)
self.address = (self.address + 1) & (X24164_SIZE - 1)
self._ack_pending = True
self._ack_armed_on_current_clock = True
return
self._ack_pending = False
self._ack_armed_on_current_clock = True
self.trace_events.append(X24164TraceEvent("x24164_ignored_byte", self._selected_name(), value=value, ack=False))
def _prepare_read_byte(self, *, skip_current_falling: bool = False) -> None:
if self.selected is None:
self._read_byte = 0xFF
return
self._read_byte = self.selected.read(self.address)
self._read_bit_index = 0
self._read_prepared_on_current_clock = skip_current_falling
self._awaiting_master_ack = False
self.trace_events.append(
X24164TraceEvent("x24164_prepare_read", self.selected.name, value=self._read_byte, address=self.address)
)
def _device_for_control(self, value: int) -> X24164Device | None:
for device in self.devices:
if device.matches_control(value):
return device
return None
def _device_for_linear_address(self, address: int) -> X24164Device | None:
bank = (address >> 11) & 1
wanted_base = 0xA0 if bank == 0 else 0xE0
for device in self.devices:
if device.control_base == wanted_base:
return device
return None
def _selected_name(self) -> str | None:
return self.selected.name if self.selected is not None else None
def default_x24164_devices() -> list[X24164Device]:
return [
X24164Device("x24164_a0_lower_2k", 0xA0),
X24164Device("x24164_e0_upper_2k", 0xE0),
]

View File

@@ -1170,7 +1170,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--stop-on-tx", action="store_true", help="stop when SCI1 TDR emits the first byte")
parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF)
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true", help="make P9 fast-path wrapper calls succeed when no modeled P9 response is queued")
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("--p9-log-limit", type=int, default=80)
parser.add_argument("--sci-log-limit", type=int, default=32)
parser.add_argument("--hot-limit", type=int, default=12)

View File

@@ -270,7 +270,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--frt2-ocia-steps", type=int, default=None, help="legacy step-period override for FRT2 OCIA")
parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine")
parser.add_argument("--p9-fast-optimistic-wrapper", action="store_true", help="make P9 fast-path wrapper calls succeed when no modeled P9 response is queued")
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")
return parser

View File

@@ -2,6 +2,7 @@ import unittest
from h8536.emulator.fast_paths import (
LOC_BFE0_TRANSFER_WRAPPER,
LOC_BFFE_TRANSFER_WRAPPER,
LOC_C08B_P9_WRITE_BYTE,
LOC_C0DB_P9_READ_BYTE,
P9FastPath,
@@ -26,7 +27,7 @@ class P9FastPathTest(unittest.TestCase):
self.assertFalse(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.pc, LOC_C08B_P9_WRITE_BYTE)
def test_c08b_write_byte_logs_r0_sets_success_and_returns_to_caller(self):
def test_c08b_write_byte_logs_r0_sets_timeout_without_active_eeprom_transaction(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE
emulator.cpu.regs[0] = 0x12A5
@@ -41,10 +42,11 @@ class P9FastPathTest(unittest.TestCase):
self.assertEqual(fast_path.output_bytes, [0xA5])
self.assertEqual(fast_path.events[-1].kind, "write_byte")
self.assertEqual(fast_path.events[-1].value, 0xA5)
self.assertEqual(emulator.cpu.regs[0], 1)
self.assertFalse(fast_path.events[-1].success)
self.assertEqual(emulator.cpu.regs[0], 0)
self.assertEqual(emulator.cpu.pc, 0x3456)
self.assertEqual(emulator.cpu.regs[7], 0xFE80)
self.assertFalse(emulator.cpu.z)
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
@@ -106,9 +108,11 @@ class P9FastPathTest(unittest.TestCase):
self.assertEqual(event.queue_depth, 1)
self.assertEqual(fast_path.trace_lines(), ["read_byte pc=C0DB value=00 source=script:idle-panel queued=1"])
def test_wrapper_defaults_to_timeout_when_no_p9_device_response_is_queued(self):
def test_wrapper_writes_and_verifies_against_x24164_model(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_BFE0_TRANSFER_WRAPPER
emulator.cpu.regs[4] = 0x0812
emulator.cpu.regs[5] = 0x3456
emulator.cpu.regs[7] = 0xFE84
emulator.memory.write16(0xFE84, 0x789A)
@@ -116,30 +120,40 @@ class P9FastPathTest(unittest.TestCase):
self.assertTrue(fast_path.try_handle(emulator))
event = fast_path.events[-1]
self.assertEqual(event.kind, "wrapper_timeout")
self.assertFalse(event.success)
self.assertEqual(event.source, "default_timeout")
self.assertEqual(emulator.cpu.regs[0], 0)
self.assertTrue(emulator.cpu.z)
self.assertEqual(event.kind, "wrapper_success")
self.assertTrue(event.success)
self.assertEqual(event.source, "x24164_write_verify")
self.assertEqual(emulator.cpu.regs[0], 1)
self.assertFalse(emulator.cpu.z)
self.assertEqual(emulator.cpu.pc, 0x789A)
self.assertEqual(emulator.memory.p9_bus.fast_read_word(0x0812), (True, 0x3456))
def test_wrapper_can_succeed_from_queued_p9_device_response(self):
def test_read_wrapper_puts_x24164_word_in_r5(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_BFE0_TRANSFER_WRAPPER
emulator.cpu.regs[4] = 0x0010
emulator.cpu.regs[5] = 0xCAFE
emulator.cpu.regs[7] = 0xFE86
emulator.memory.write16(0xFE86, 0x89AB)
emulator.memory.p9_bus.queue_wrapper_results([True], source="panel-script")
fast_path = P9FastPath(P9FastPathConfig(enabled=True))
self.assertTrue(fast_path.try_handle(emulator))
emulator.cpu.pc = LOC_BFFE_TRANSFER_WRAPPER
emulator.cpu.regs[4] = 0x0010
emulator.cpu.regs[5] = 0
emulator.cpu.regs[7] = 0xFE86
emulator.memory.write16(0xFE86, 0x9ABC)
self.assertTrue(fast_path.try_handle(emulator))
event = fast_path.events[-1]
self.assertEqual(event.kind, "wrapper_success")
self.assertTrue(event.success)
self.assertEqual(event.source, "panel-script")
self.assertEqual(event.source, "x24164_read_word")
self.assertEqual(emulator.cpu.regs[5], 0xCAFE)
self.assertEqual(emulator.cpu.regs[0], 1)
self.assertFalse(emulator.cpu.z)
self.assertEqual(emulator.cpu.pc, 0x89AB)
self.assertEqual(emulator.cpu.pc, 0x9ABC)
if __name__ == "__main__":

View File

@@ -4,6 +4,55 @@ from h8536.emulator import MemoryMap, P9DDR, P9DR
from h8536.emulator.peripherals import P9Bus
def p9_start(bus: P9Bus) -> None:
bus.write_ddr(0x93)
bus.write_dr(0x80)
bus.write_dr(0x82)
bus.write_dr(0x02)
bus.write_dr(0x00)
def p9_stop(bus: P9Bus) -> None:
bus.write_ddr(0x93)
bus.write_dr(0x00)
bus.write_dr(0x02)
bus.write_dr(0x82)
bus.write_dr(0x80)
def p9_write_byte(bus: P9Bus, value: int) -> bool:
bus.write_ddr(0x93)
for bit_index in range(7, -1, -1):
bit = (value >> bit_index) & 1
low = 0x80 if bit else 0x00
bus.write_dr(low)
bus.write_dr(low | 0x02)
bus.write_dr(low)
bus.write_ddr(0x13)
bus.write_dr(bus.dr_latch | 0x02)
ack_low = not bool(bus.read_dr() & 0x80)
bus.write_dr(bus.dr_latch & ~0x02)
bus.write_ddr(0x93)
return ack_low
def p9_read_byte(bus: P9Bus, *, master_ack: bool) -> int:
value = 0
bus.write_ddr(0x13)
for _ in range(8):
bus.write_dr(bus.dr_latch | 0x02)
value = (value << 1) | (1 if bus.read_dr() & 0x80 else 0)
bus.write_dr(bus.dr_latch & ~0x02)
bus.write_ddr(0x93)
if master_ack:
bus.write_dr(bus.dr_latch & ~0x80)
else:
bus.write_dr(bus.dr_latch | 0x80)
bus.write_dr(bus.dr_latch | 0x02)
bus.write_dr(bus.dr_latch & ~0x02)
return value
class P9BusTest(unittest.TestCase):
def test_bit7_input_uses_queued_then_default_low_response(self):
memory = MemoryMap(b"\x00" * 4)
@@ -47,6 +96,44 @@ class P9BusTest(unittest.TestCase):
self.assertIn("wrapper_result ddr=00 dr=00 value=01 success=1 source=panel-script queued=0", bus.trace_lines())
self.assertIn("wrapper_result ddr=00 dr=00 value=00 success=0 source=default_timeout queued=0", bus.trace_lines())
def test_x24164_bit_banged_write_acknowledges_and_stores_data(self):
bus = P9Bus()
p9_start(bus)
self.assertTrue(p9_write_byte(bus, 0xA0))
self.assertTrue(p9_write_byte(bus, 0x12))
self.assertTrue(p9_write_byte(bus, 0x34))
p9_stop(bus)
device = bus.x24164_bus.devices[0]
self.assertEqual(device.read(0x12), 0x34)
self.assertTrue(any("x24164_write_data" in line and "addr=012" in line for line in bus.trace_lines()))
def test_x24164_bit_banged_random_read_uses_p91_p97_lines(self):
bus = P9Bus()
bus.x24164_bus.devices[0].write(0x12, 0xAB)
p9_start(bus)
self.assertTrue(p9_write_byte(bus, 0xA0))
self.assertTrue(p9_write_byte(bus, 0x12))
p9_start(bus)
self.assertTrue(p9_write_byte(bus, 0xA1))
self.assertEqual(p9_read_byte(bus, master_ack=False), 0xAB)
p9_stop(bus)
self.assertTrue(any("x24164_prepare_read" in line and "value=AB" in line for line in bus.trace_lines()))
def test_x24164_fast_word_mapping_matches_rom_address_banks(self):
bus = P9Bus()
self.assertTrue(bus.fast_write_word(0x0012, 0x3456))
self.assertTrue(bus.fast_write_word(0x0812, 0xABCD))
self.assertEqual(bus.fast_read_word(0x0012), (True, 0x3456))
self.assertEqual(bus.fast_read_word(0x0812), (True, 0xABCD))
self.assertEqual(bus.x24164_bus.devices[0].read(0x12), 0x34)
self.assertEqual(bus.x24164_bus.devices[1].read(0x12), 0xAB)
if __name__ == "__main__":
unittest.main()