1
0
Files
h8-536-decoder/h8536/emulator/eeprom_image.py
2026-05-26 11:35:21 +10:00

352 lines
12 KiB
Python

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",
]