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

@@ -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),
]