"""GSP device model: registry + per-block modules. Public surface: * :class:`~sim.devices.base.DeviceRegistry` -- address-range lookup, access log and serialisable state. * :func:`registry_from_profile` -- build a registry from a versioned ``sim/profiles/*.json`` document. * per-block devices: :mod:`sim.devices.pmc`, :mod:`sim.devices.falcon`, :mod:`sim.devices.priscv`, :mod:`sim.devices.timer`, :mod:`sim.devices.csb`, :mod:`sim.devices.fwsec`, :mod:`sim.devices.pmu`, plus the :mod:`sim.devices.dma` translation seam and :mod:`sim.devices.irq` router. Evidence and limits for every modelled register are documented in the module docstrings and in ``docs/36-GSP-DEVICE-MODEL.md``; unmodelled offsets always stay unhandled so the machine's unknown-MMIO log remains the source of truth for what is still missing. """ from __future__ import annotations import json from .base import Device, DeviceRegistry, SNAPSHOT_SCHEMA, Window from .csb import CsbWindow from .dma import DmaTranslator from .falcon import FalconBlock from .fwsec import Wpr2Block from .irq import IrqRouter from .observe import ( TRACE_SCHEMA, ObservedWindow, build_trace, collect_observations, parse_window_spec, windows_from_section, ) from .pmc import PmcBlock from .pmu import PmuStub from .priscv import PriscvBlock from .timer import PtimerBlock PROFILE_SCHEMA = "greeneye-sim-profile/1" #: Profile device sections that declare observe-only/log-only windows. OBSERVE_SECTIONS = ("gmmu", "fbif") __all__ = [ "Device", "DeviceRegistry", "Window", "SNAPSHOT_SCHEMA", "PROFILE_SCHEMA", "TRACE_SCHEMA", "PmcBlock", "FalconBlock", "PriscvBlock", "PtimerBlock", "CsbWindow", "Wpr2Block", "PmuStub", "DmaTranslator", "IrqRouter", "ObservedWindow", "load_profile", "registry_from_profile", "windows_from_section", "parse_window_spec", "collect_observations", "build_trace", ] def load_profile(path) -> dict: """Load and minimally validate a ``greeneye-sim-profile/1`` document.""" with open(path, "r", encoding="utf-8") as fh: data = json.load(fh) if data.get("schema") != PROFILE_SCHEMA: raise ValueError(f"{path}: expected schema {PROFILE_SCHEMA!r}") if "devices" not in data or not isinstance(data["devices"], dict): raise ValueError(f"{path}: missing 'devices' object") return data def _int(value, default=None): """Profile numbers may be JSON integers or ``0x``/decimal strings.""" if value is None: return default if isinstance(value, str): return int(value, 0) return int(value) def _windows(section: dict, default_size: int = 0x1000): out = [] for win in section.get("windows", ()): out.append((_int(win["base"]), _int(win.get("size", default_size)))) return out def registry_from_profile(profile: dict, *, machine=None, max_access_log: int = 100_000) -> DeviceRegistry: """Build a :class:`DeviceRegistry` from a profile document. ``machine`` (optional) supplies RAM to the DMA translation seam; without it DMA transfers are still modelled structurally but refuse to move data (recorded, never faked). """ if profile.get("schema") != PROFILE_SCHEMA: raise ValueError(f"expected schema {PROFILE_SCHEMA!r}") sections = profile.get("devices", {}) reg = DeviceRegistry(max_access_log=max_access_log) dma_section = sections.get("dma", {}) dma_windows = [ {"pa": _int(w["pa"]), "sim": _int(w["sim"]), "size": _int(w["size"])} for w in dma_section.get("windows", ()) ] if machine is not None: dma = DmaTranslator( ram=machine.ram, ram_base=machine.ram_base, ram_size=machine.ram_size, windows=dma_windows, ) else: dma = DmaTranslator(ram=bytearray(), ram_base=0, ram_size=0, windows=()) falcon = None priscv = None for name, section in sections.items(): if not isinstance(section, dict): continue if name == "dma": continue if name == "pmc": dev = PmcBlock( chip_id=_int(section.get("chip_id", 0x164)), major=_int(section.get("major", 0)), minor=_int(section.get("minor", 0)), minor_ext=_int(section.get("minor_ext", 0)), boot_42=(_int(section["boot_42"]) if "boot_42" in section else None), ) elif name == "falcon": shims = section.get("shims", {}) dev = FalconBlock( dma=dma, debuginfo_host_consume=bool(shims.get("debuginfo_host_consume", False)), debuginfo_stage_mask=_int(section.get("debuginfo_stage_mask", 0x700000)), debuginfo_stage_shift=_int(section.get("debuginfo_stage_shift", 20)), ) falcon = dev elif name == "priscv": dev = PriscvBlock() priscv = dev elif name == "timer": if not section.get("enabled", True): continue dev = PtimerBlock( ns_per_insn=float(section.get("ns_per_insn", 1.0)), clock=(machine.insn_count if machine is not None else None), ) elif name == "csb": dev = CsbWindow(registers=section.get("registers", {})) elif name == "fwsec": dev = Wpr2Block( wpr2_start=(_int(section["wpr2_start"]) if "wpr2_start" in section else None), wpr2_end=(_int(section["wpr2_end"]) if "wpr2_end" in section else None), ) elif name == "pmu": dev = PmuStub() elif name in OBSERVE_SECTIONS: for win in windows_from_section(section): reg.add(ObservedWindow( base=win["base"], size=win["size"], label=win["label"], mode=win["mode"], registers=win["registers"], evidence=section.get("evidence", ""), clock=(machine.insn_count if machine is not None else None), )) continue else: raise ValueError(f"unknown device section {name!r}") for base, size in _windows(section): reg.add(dev, base, size) reg.add_service(IrqRouter(falcon=falcon, priscv=priscv)) reg.dma = dma return reg