1
0

EEPROM layout

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

730
h8536/eeprom_layout.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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