diff --git a/README.md b/README.md index a3028d1..7756608 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/h8536/emulator/__init__.py b/h8536/emulator/__init__.py index cb8acfc..b336716 100644 --- a/h8536/emulator/__init__.py +++ b/h8536/emulator/__init__.py @@ -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", diff --git a/h8536/emulator/bench_replay.py b/h8536/emulator/bench_replay.py index 2249106..c9e8fee 100644 --- a/h8536/emulator/bench_replay.py +++ b/h8536/emulator/bench_replay.py @@ -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 diff --git a/h8536/emulator/cli.py b/h8536/emulator/cli.py index 8a42f89..86eb0e7 100644 --- a/h8536/emulator/cli.py +++ b/h8536/emulator/cli.py @@ -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 diff --git a/h8536/emulator/fast_paths.py b/h8536/emulator/fast_paths.py index 010bcf8..a63407d 100644 --- a/h8536/emulator/fast_paths.py +++ b/h8536/emulator/fast_paths.py @@ -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,7 +148,27 @@ class P9FastPath: def _handle_wrapper(self, emulator: Any) -> None: pc = emulator.cpu.pc & 0xFFFF - success, source, queue_depth = emulator.memory.p9_bus.consume_wrapper_result() + 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( P9FastPathEvent( @@ -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 diff --git a/h8536/emulator/peripherals/__init__.py b/h8536/emulator/peripherals/__init__.py index cdd6f71..3e0b0f3 100644 --- a/h8536/emulator/peripherals/__init__.py +++ b/h8536/emulator/peripherals/__init__.py @@ -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", ] diff --git a/h8536/emulator/peripherals/p9_bus.py b/h8536/emulator/peripherals/p9_bus.py index 96e8d91..bb9af41 100644 --- a/h8536/emulator/peripherals/p9_bus.py +++ b/h8536/emulator/peripherals/p9_bus.py @@ -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())) diff --git a/h8536/emulator/peripherals/x24164.py b/h8536/emulator/peripherals/x24164.py new file mode 100644 index 0000000..47c1b4e --- /dev/null +++ b/h8536/emulator/peripherals/x24164.py @@ -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), + ] diff --git a/h8536/emulator/probe.py b/h8536/emulator/probe.py index 291b20b..00c3387 100644 --- a/h8536/emulator/probe.py +++ b/h8536/emulator/probe.py @@ -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) diff --git a/h8536/emulator/rx_probe.py b/h8536/emulator/rx_probe.py index bb90492..b3718c8 100644 --- a/h8536/emulator/rx_probe.py +++ b/h8536/emulator/rx_probe.py @@ -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 diff --git a/tests/test_emulator_fast_paths.py b/tests/test_emulator_fast_paths.py index 74dd63a..6cac702 100644 --- a/tests/test_emulator_fast_paths.py +++ b/tests/test_emulator_fast_paths.py @@ -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__": diff --git a/tests/test_p9_bus.py b/tests/test_p9_bus.py index acfcbc5..5517e46 100644 --- a/tests/test_p9_bus.py +++ b/tests/test_p9_bus.py @@ -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()