EEPROM layout
This commit is contained in:
730
h8536/eeprom_layout.py
Normal file
730
h8536/eeprom_layout.py
Normal 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",
|
||||
]
|
||||
@@ -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
|
||||
|
||||
351
h8536/emulator/eeprom_image.py
Normal file
351
h8536/emulator/eeprom_image.py
Normal 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",
|
||||
]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user