diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/devices')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/__init__.py | 163 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/base.py | 263 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/csb.py | 89 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/dma.py | 117 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/falcon.py | 228 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py | 80 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/irq.py | 58 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/pmc.py | 90 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/pmu.py | 46 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/priscv.py | 113 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/devices/timer.py | 86 |
11 files changed, 1333 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py new file mode 100644 index 0000000..caf22b3 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py @@ -0,0 +1,163 @@ +"""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 .pmc import PmcBlock +from .pmu import PmuStub +from .priscv import PriscvBlock +from .timer import PtimerBlock + +PROFILE_SCHEMA = "greeneye-sim-profile/1" + +__all__ = [ + "Device", + "DeviceRegistry", + "Window", + "SNAPSHOT_SCHEMA", + "PROFILE_SCHEMA", + "PmcBlock", + "FalconBlock", + "PriscvBlock", + "PtimerBlock", + "CsbWindow", + "Wpr2Block", + "PmuStub", + "DmaTranslator", + "IrqRouter", + "load_profile", + "registry_from_profile", +] + + +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() + 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 diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/base.py b/debug/risc-v-gsp-rm-vm/sim/devices/base.py new file mode 100644 index 0000000..877109c --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/base.py @@ -0,0 +1,263 @@ +"""Device-model core: address-range registry and device base classes. + +The registry decodes a guest MMIO address to at most one modelled device +window (``base <= addr < base + size``) and asks that device for read/write +semantics. Modeled windows take precedence over the lenient device-map / +unknown fallback in :class:`sim.machine.Machine`; accesses inside a known +window that the device does not model are *not* silently zeroed here -- they +return ``unhandled`` so the machine still logs them as unknowns. + +Every modelled access is recorded in :attr:`DeviceRegistry.accesses` with the +PC, width, direction and value so a session can explain exactly what the +device model served. All state is exposed as JSON-serialisable data +(:meth:`Device.state` / :meth:`DeviceRegistry.snapshot`) so snapshot support +can be layered on later without changing this API. + +Evidence rule (SIMULATOR-WORK-BOUNDARY / docs/35): a register is only given +semantics when it is backed by at least one of + +* a hardware register/field header (offset, width, access, reset/field docs); +* a firmware/source use (the code that reads or writes it); +* an observed access pattern (address, width, direction, PC, value sequence). + +Each device class documents the evidence kind for every register it models. +Anything else stays ``unhandled`` and is reported by the machine's unknown +log; we never invent a read-back value. +""" +from __future__ import annotations + +import json + +#: Schema tag for :meth:`DeviceRegistry.snapshot`. +SNAPSHOT_SCHEMA = "greeneye-sim-devices/1" + + +class Device: + """One modelled register block at one or more address windows. + + Subclasses implement :meth:`read`/:meth:`write` (guest side, side effects + allowed) and :meth:`peek` (debugger side, no side effects). Returning + ``None`` from ``read``/``peek`` or ``False`` from ``write`` means "not + modelled"; the registry then reports the access as unhandled so it is + still logged by the caller. + """ + + name = "device" + + def __init__(self, base: int = 0, size: int = 0): + self.base = base + self.size = size + + # -- guest-visible semantics ------------------------------------------ + + def read(self, offset: int, size: int) -> int | None: + return None + + def write(self, offset: int, size: int, value: int) -> bool: + return False + + # -- debugger-visible semantics (no side effects) ---------------------- + + def peek(self, offset: int, size: int) -> int | None: + return self.read(offset, size) + + # -- serialisable state ------------------------------------------------ + + def state(self) -> dict: + return {} + + def load_state(self, data: dict) -> None: + pass + + def reset(self) -> None: + self.load_state({}) + + # -- helpers ----------------------------------------------------------- + + @staticmethod + def _mask(size: int) -> int: + return (1 << (8 * size)) - 1 + + def __repr__(self) -> str: # pragma: no cover - cosmetic + return f"{type(self).__name__}({self.name} @ {self.base:#x}+{self.size:#x})" + + +class Window: + """One (base, size) -> :class:`Device` mapping.""" + + __slots__ = ("base", "size", "device") + + def __init__(self, base: int, size: int, device: Device): + self.base = base + self.size = size + self.device = device + + def contains(self, addr: int, size: int) -> bool: + return self.base <= addr and addr + size <= self.base + self.size + + def to_dict(self) -> dict: + return { + "base": self.base, + "size": self.size, + "device": self.device.name, + } + + +class DeviceRegistry: + """Ordered set of device windows plus an access log. + + Lookup is last-registered-first so a narrow window may override a wider + one without hiding it: when the last matching window does not implement the + access (``read`` returns ``None`` / ``write`` returns ``False``), the + registry continues to the next matching window (safe fallback). If every + matching window refuses, the access is recorded once as unhandled. + Accesses are matched by exact containment (``addr + size`` inside the + window) so a straddling access is never half-served. + """ + + def __init__(self, *, max_access_log: int = 100_000): + self.windows: list[Window] = [] + self.devices: list[Device] = [] + self.services: list[object] = [] + self.dma = None # optional DmaTranslator aperture seam + self.accesses: list[dict] = [] + self.dropped_accesses = 0 + self.unhandled = 0 + self.max_access_log = max(0, int(max_access_log)) + + def add_service(self, service) -> object: + """Attach a non-window service (e.g. an IRQ router) for state export.""" + self.services.append(service) + return service + + # -- construction ------------------------------------------------------ + + def add(self, device: Device, base: int | None = None, size: int | None = None) -> Device: + """Register ``device`` for one window (defaults: device.base/size).""" + b = device.base if base is None else base + s = device.size if size is None else size + if s <= 0: + raise ValueError(f"device window size must be positive, got {s}") + if device not in self.devices: + self.devices.append(device) + self.windows.append(Window(b, s, device)) + return device + + def device_by_name(self, name: str) -> Device | None: + for dev in self.devices: + if dev.name == name: + return dev + return None + + # -- lookup ------------------------------------------------------------ + + def _candidates(self, addr: int, size: int): + """Matching windows, last-registered-first (fallback order).""" + for win in reversed(self.windows): + if win.contains(addr, size): + yield win + + def _record(self, addr: int, size: int, write: bool, value: int | None, + handled: bool, pc: int) -> None: + entry = { + "addr": addr, + "size": size, + "rw": "w" if write else "r", + "handled": bool(handled), + "pc": pc, + } + if write and value is not None: + entry["value"] = value & ((1 << (8 * size)) - 1) + if handled and not write: + entry["value"] = value + if len(self.accesses) < self.max_access_log: + self.accesses.append(entry) + else: + self.dropped_accesses += 1 + + def read(self, addr: int, size: int, pc: int = 0) -> tuple[bool, int]: + """Guest read: ``(handled, value)``. Unhandled accesses are logged. + + The last matching window is asked first; a window that does not model + the offset defers to the next matching window. + """ + matched = False + for win in self._candidates(addr, size): + matched = True + value = win.device.read(addr - win.base, size) + if value is not None: + self._record(addr, size, False, value, True, pc) + return True, value + self.unhandled += 1 + if matched: + self._record(addr, size, False, None, False, pc) + return False, 0 + + def write(self, addr: int, size: int, value: int, pc: int = 0) -> bool: + """Guest write: True when handled. Unhandled accesses are logged. + + The last matching window is asked first; a window that does not model + the offset defers to the next matching window. + """ + matched = False + for win in self._candidates(addr, size): + matched = True + if win.device.write(addr - win.base, size, value): + self._record(addr, size, True, value, True, pc) + return True + self.unhandled += 1 + if matched: + self._record(addr, size, True, value, False, pc) + return False + + def peek(self, addr: int, size: int) -> bytes | None: + """Debugger read of device space: bytes, or None when unmapped.""" + for win in self._candidates(addr, size): + value = win.device.peek(addr - win.base, size) + if value is not None: + return int(value).to_bytes(size, "little") + return None + + # -- state ------------------------------------------------------------- + + def state(self) -> dict: + return { + "schema": SNAPSHOT_SCHEMA, + "devices": [{"name": d.name, "base": d.base, "size": d.size, + "state": d.state()} for d in self.devices], + "services": [{"name": getattr(s, "name", type(s).__name__), + "state": s.state()} for s in self.services], + "counters": { + "accesses": len(self.accesses), + "dropped_accesses": self.dropped_accesses, + "unhandled": self.unhandled, + }, + } + + def load_state(self, data: dict) -> None: + if data.get("schema") != SNAPSHOT_SCHEMA: + raise ValueError(f"not a {SNAPSHOT_SCHEMA} snapshot") + for entry in data.get("devices", []): + dev = self.device_by_name(entry.get("name", "")) + if dev is not None: + dev.load_state(entry.get("state", {})) + counters = data.get("counters", {}) + self.dropped_accesses = int(counters.get("dropped_accesses", 0)) + self.unhandled = int(counters.get("unhandled", 0)) + + def snapshot_json(self) -> str: + return json.dumps(self.state(), sort_keys=True) + + def reset(self) -> None: + self.accesses.clear() + self.dropped_accesses = 0 + self.unhandled = 0 + for dev in self.devices: + dev.reset() + for service in self.services: + reset = getattr(service, "reset", None) + if callable(reset): + reset() + + def describe(self) -> list[dict]: + return [w.to_dict() for w in self.windows] diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/csb.py b/debug/risc-v-gsp-rm-vm/sim/devices/csb.py new file mode 100644 index 0000000..818a263 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/csb.py @@ -0,0 +1,89 @@ +"""CSB / TRANSCOM message-window seam (B3 landing point). + +Evidence kind: **hardware register/field header + observed access pattern** +(once the firmware reaches the message loop). At the time of writing the +570.144 boot has not been observed polling the GSP message-queue CSB window, +so this module deliberately models **no register semantics by default**: a +window is only served when a profile declares it *and* lists the individual +registers with their widths, reset values and semantics (``rw``, ``w1c``, +``set``, ``clear``). Undeclared offsets stay unhandled and are logged. + +The CSB/TRANSCOM window carries the host<->GSP message queues and doorbells +(head/tail pointers, interrupt set/clear); B3 requires the first such poll to +return the documented read-back, which will be added here together with the +observed access evidence and a test. +""" +from __future__ import annotations + +from .base import Device + +#: Per-register semantics accepted by a profile. +SEMANTICS = ("rw", "ro", "wo", "w1c", "set", "clear") + + +class CsbWindow(Device): + """Profile-declared CSB/mailbox register window (empty by default).""" + + name = "csb" + + def __init__(self, *, base: int = 0, size: int = 0x1000, registers=None): + super().__init__(base=base, size=size) + # registers: {offset: {"width": 4, "value": int, "semantics": "rw"}} + self.registers: dict[int, dict] = {} + for off, spec in (registers or {}).items(): + self.declare(int(off), spec) + self.reset() + + def declare(self, offset: int, spec: dict) -> None: + semantics = spec.get("semantics", "rw") + if semantics not in SEMANTICS: + raise ValueError(f"unknown CSB semantics {semantics!r}") + width = int(spec.get("width", 4)) + if width not in (1, 2, 4, 8): + raise ValueError(f"unsupported CSB register width {width}") + self.registers[offset] = { + "width": width, + "reset": int(spec.get("value", 0)), + "semantics": semantics, + } + + def reset(self) -> None: + self.values = {off: spec["reset"] for off, spec in self.registers.items()} + + def _spec(self, offset: int, size: int) -> dict | None: + spec = self.registers.get(offset) + if spec is None or spec["width"] != size: + return None + return spec + + def read(self, offset: int, size: int) -> int | None: + spec = self._spec(offset, size) + if spec is None or spec["semantics"] == "wo": + return None + return self.values.get(offset, 0) + + def write(self, offset: int, size: int, value: int) -> bool: + spec = self._spec(offset, size) + if spec is None or spec["semantics"] == "ro": + return False + mask = (1 << (8 * size)) - 1 + value &= mask + sem = spec["semantics"] + if sem == "w1c": + self.values[offset] = self.values.get(offset, 0) & ~value & mask + elif sem == "set": + self.values[offset] = (self.values.get(offset, 0) | value) & mask + elif sem == "clear": + self.values[offset] = (self.values.get(offset, 0) & ~value) & mask + else: + self.values[offset] = value + return True + + def state(self) -> dict: + return {"values": {str(k): v for k, v in sorted(self.values.items())}, + "declared": sorted(self.registers)} + + def load_state(self, data: dict) -> None: + values = {int(k): int(v) for k, v in data.get("values", {}).items()} + self.values = {off: values.get(off, spec["reset"]) + for off, spec in self.registers.items()} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/dma.py b/debug/risc-v-gsp-rm-vm/sim/devices/dma.py new file mode 100644 index 0000000..45d03e5 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/dma.py @@ -0,0 +1,117 @@ +"""DMA / address-aperture translation seam. + +Evidence kind: **hardware address-map header + firmware use + observed +access pattern (through the falcon DMA registers).** + +The GSP RISC-V address map (Turing) defines apertures the firmware and its +DMA engine address in GPU physical space: + +* ``IMEM`` ``0x0_0000_0000`` (4 GiB window) -- local instruction memory +* ``DMEM`` ``0x1_0000_0000`` (4 GiB window) -- local data memory +* ``EMEM`` ``0x2_0000_0000`` (4 GiB window) -- external memory window +* ``FBGPA`` ``0x6180_0000_0000_0000`` -- framebuffer aperture +* ``SYSGPA````0x8180_0000_0000_0000`` -- system-memory aperture +* ``PRIV`` ``0x2000_0000_0000_0000`` -- register aperture + +The simulator keeps local memory in one flat RAM region; framebuffer and +system memory only exist as *profile-declared windows* that map a GPU +physical aperture onto a simulator RAM address. A window not declared in the +profile is not modelled: the transfer is refused and recorded (never +silently zero-filled). This class is the single place where that mapping +lives, so a future IOMMU model can replace it without touching devices. +""" +from __future__ import annotations + + +class DmaTranslator: + """Translate GPU physical addresses to simulator RAM through windows.""" + + def __init__(self, *, ram, ram_base: int, ram_size: int, windows=None): + self.ram = ram + self.ram_base = ram_base + self.ram_size = ram_size + self.windows: list[tuple[int, int, int]] = [] # (pa_base, sim_base, size) + self.transfers = 0 + self.refused = 0 + self.events: list[dict] = [] + for win in windows or (): + self.add_window(int(win["pa"]), int(win["sim"]), int(win["size"])) + + def add_window(self, pa_base: int, sim_base: int, size: int) -> None: + if size <= 0: + raise ValueError("DMA window size must be positive") + self.windows.append((pa_base, sim_base, size)) + + def translate(self, pa: int) -> int | None: + """GPU physical address -> simulator address, or None if unmodelled.""" + for pa_base, sim_base, size in reversed(self.windows): + if pa_base <= pa < pa_base + size: + return sim_base + (pa - pa_base) + return None + + def read(self, pa: int, size: int) -> bytes | None: + sim = self.translate(pa) + if sim is None: + self._refuse(pa, size, "r") + return None + return self._read_sim(sim, size) + + def write(self, pa: int, data: bytes) -> bool: + sim = self.translate(pa) + if sim is None: + self._refuse(pa, len(data), "w") + return False + self._write_sim(sim, data) + return True + + def _in_ram(self, addr: int, size: int) -> bool: + off = addr - self.ram_base + return 0 <= off and off + size <= self.ram_size + + def _read_sim(self, sim: int, size: int) -> bytes | None: + if not self._in_ram(sim, size): + self._refuse(sim, size, "r-sim") + return None + off = sim - self.ram_base + return bytes(self.ram[off:off + size]) + + def _write_sim(self, sim: int, data: bytes) -> bool: + if not self._in_ram(sim, len(data)): + self._refuse(sim, len(data), "w-sim") + return False + off = sim - self.ram_base + self.ram[off:off + len(data)] = data + return True + + def copy(self, src_pa: int, dst_pa: int, size: int) -> bool: + """Copy ``size`` bytes between GPU physical addresses. + + A transfer is counted only when both the source read and the + destination write complete; a refused destination write leaves the + transfer in :attr:`refused` and not in :attr:`transfers`. + """ + data = self.read(src_pa, size) + if data is None: + return False + ok = self.write(dst_pa, data) + if ok: + self.transfers += 1 + return ok + + def _refuse(self, pa: int, size: int, kind: str) -> None: + self.refused += 1 + if len(self.events) < 4096: + self.events.append({"pa": pa, "size": size, "kind": kind}) + + def state(self) -> dict: + return { + "windows": [{"pa": p, "sim": s, "size": n} for p, s, n in self.windows], + "transfers": self.transfers, + "refused": self.refused, + "events": list(self.events), + } + + def reset(self) -> None: + self.transfers = 0 + self.refused = 0 + self.events.clear() diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/falcon.py b/debug/risc-v-gsp-rm-vm/sim/devices/falcon.py new file mode 100644 index 0000000..8841906 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/falcon.py @@ -0,0 +1,228 @@ +"""GSP falcon register block (boot polling + handshake + DMA). + +Evidence kind: **hardware register/field header + firmware use + observed +access pattern.** + +The block is reached through the PRIV aperture (``PRIV + 0x110000``) and, in +the libos kernel's private mapping, at virtual base ``0x3_0000_0000`` (the +firmware accesses both forms during boot). Modelled registers, all with +header offsets relative to the block: + +====================================== ====== ===== ========================= +register offset access semantics +====================================== ====== ===== ========================= +``FALCON_IRQSSET`` 0x000 W ``irq_status |= value`` +``FALCON_IRQSCLR`` 0x004 W ``irq_status &= ~value`` +``FALCON_IRQSTAT`` 0x008 R ``irq_status`` +``FALCON_MAILBOX0`` 0x040 RW storage +``FALCON_MAILBOX1`` 0x044 RW storage +``FALCON_OS`` 0x080 RW storage +``FALCON_DEBUGINFO`` 0x094 RW storage (+ host shim) +``FALCON_CPUCTL`` 0x100 RW storage +``FALCON_BOOTVEC`` 0x104 RW storage +``FALCON_DMACTL`` 0x10c RW storage +``FALCON_DMATRFBASE`` 0x110 RW storage +``FALCON_DMATRFMOFFS`` 0x114 RW storage +``FALCON_DMATRFCMD`` 0x118 RW transfer on write +``FALCON_DMATRFFBOFFS`` 0x11c RW storage +``FALCON_DMATRFBASE1`` 0x128 RW storage +``FBIF_TRANSCFG(0..7)`` 0x600 RW storage +``FBIF_CTL`` 0x624 RW storage +====================================== ====== ===== ========================= + +Observed firmware use (570.144): the boot code writes MAILBOX0 (progress +code), MAILBOX1, DEBUGINFO (boot-stage marker ``0x11DEAD``) and IRQSSET +``HALT`` (bit 4), then polls DEBUGINFO bits ``22:20`` until they clear; it +reads back DMACTL/DMATRFCMD/DMATRFBASE/DMATRFBASE1/DMATRFMOFFS/ +DMATRFFBOFFS and IRQSTAT. The DMA transfer semantics (size encoding, IMEM/ +WRITE direction, ``((base1<<32|base)<<8)+fboffs`` addressing) follow the +register header fields and are exercised only through the translation seam +(:mod:`sim.devices.dma`); an unmapped transfer is refused and recorded, never +zero-filled. + +The DEBUGINFO "host consume" behaviour is a **boot shim**, not a device +claim: the firmware's own write-then-poll loop has no self-clearing path, and +the documented debug channel is write-then-wait-for-zero (consumer is the +host side). It is enabled per profile and recorded as an assumption. +""" +from __future__ import annotations + +from .base import Device + +# Register offsets (header values, relative to the falcon block base). +IRQSSET = 0x000 +IRQSCLR = 0x004 +IRQSTAT = 0x008 +MAILBOX0 = 0x040 +MAILBOX1 = 0x044 +OS = 0x080 +DEBUGINFO = 0x094 +CPUCTL = 0x100 +BOOTVEC = 0x104 +DMACTL = 0x10C +DMATRFBASE = 0x110 +DMATRFMOFFS = 0x114 +DMATRFCMD = 0x118 +DMATRFFBOFFS = 0x11C +DMATRFBASE1 = 0x128 +FBIF_TRANSCFG0 = 0x600 +FBIF_TRANSCFG_STRIDE = 4 +FBIF_TRANSCFG_COUNT = 8 +FBIF_CTL = 0x624 + +# DMATRFCMD fields (header). +CMD_FULL = 1 << 0 +CMD_IDLE = 1 << 1 +CMD_IMEM = 1 << 4 +CMD_WRITE = 1 << 5 +CMD_SIZE_SHIFT = 8 +CMD_SIZE_MASK = 0x7 +CMD_CTXDMA_SHIFT = 12 +CMD_CTXDMA_MASK = 0x7 +CMD_ERROR = 1 << 20 + +#: IRQSSET/IRQSCLR bit 4 is HALT in the header's field list; the firmware's +#: boot handshake sets it after writing MAILBOX0/DEBUGINFO. +IRQ_HALT = 1 << 4 + +# IMEM/DMEM apertures of the GSP RISC-V address map (header constants). +AMAP_IMEM_START = 0x0000000000000000 +AMAP_DMEM_START = 0x0000000100000000 + +_RW_REGISTERS = { + MAILBOX0, MAILBOX1, OS, DEBUGINFO, CPUCTL, BOOTVEC, DMACTL, DMATRFBASE, + DMATRFMOFFS, DMATRFCMD, DMATRFFBOFFS, DMATRFBASE1, FBIF_CTL, +} + + +class FalconBlock(Device): + """Falcon/GSP register block with boot-handshake and DMA semantics.""" + + name = "falcon" + + def __init__(self, *, base: int = 0, size: int = 0x1000, dma=None, + debuginfo_host_consume: bool = False, + debuginfo_stage_mask: int = 0x700000, + debuginfo_stage_shift: int = 20): + super().__init__(base=base, size=size) + self.dma = dma + self.debuginfo_host_consume = bool(debuginfo_host_consume) + self.debuginfo_stage_mask = debuginfo_stage_mask + self.debuginfo_stage_shift = debuginfo_stage_shift + self.reset() + + def reset(self) -> None: + self.irq_status = 0 + self.regs: dict[int, int] = {off: 0 for off in _RW_REGISTERS} + self.regs[DMATRFCMD] = CMD_IDLE + self.transcfg = [0] * FBIF_TRANSCFG_COUNT + self.dma_transfers = 0 + self.dma_refused = 0 + self.debuginfo_consumed = 0 + + # -- access ------------------------------------------------------------ + + def read(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == IRQSTAT: + return self.irq_status + if offset == DEBUGINFO: + value = self.regs.get(DEBUGINFO, 0) + if self.debuginfo_host_consume: + stage = (value & self.debuginfo_stage_mask) >> self.debuginfo_stage_shift + if stage: + # Boot shim: the host consumes the stage marker (documented + # write-then-wait-for-zero debug channel); the rest of the + # register is preserved. + self.regs[DEBUGINFO] = value & ~self.debuginfo_stage_mask + self.debuginfo_consumed += 1 + return value + if offset in self.regs: + return self.regs[offset] + if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: + return self.transcfg[(offset - FBIF_TRANSCFG0) // 4] + return None + + def write(self, offset: int, size: int, value: int) -> bool: + if size != 4: + return False + value &= 0xFFFFFFFF + if offset == IRQSSET: + self.irq_status = (self.irq_status | value) & 0xFFFFFFFF + return True + if offset == IRQSCLR: + self.irq_status = (self.irq_status & ~value) & 0xFFFFFFFF + return True + if offset == IRQSTAT: + return False # read-only + if offset == DMATRFCMD: + self.regs[DMATRFCMD] = value + self._dma_transfer(value) + return True + if offset in self.regs: + self.regs[offset] = value + return True + if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: + self.transcfg[(offset - FBIF_TRANSCFG0) // 4] = value + return True + return False + + def peek(self, offset: int, size: int) -> int | None: + # No shim side effects for debugger reads. + if size != 4: + return None + if offset == IRQSTAT: + return self.irq_status + if offset in self.regs: + return self.regs[offset] + if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: + return self.transcfg[(offset - FBIF_TRANSCFG0) // 4] + return None + + # -- DMA --------------------------------------------------------------- + + def _dma_transfer(self, cmd: int) -> None: + """Execute one falcon DMA transfer (header field semantics).""" + if self.dma is None: + self.dma_refused += 1 + return + size = 1 << (((cmd >> CMD_SIZE_SHIFT) & CMD_SIZE_MASK) + 2) + tcm = AMAP_IMEM_START if (cmd & CMD_IMEM) else AMAP_DMEM_START + base = ((self.regs.get(DMATRFBASE1, 0) & 0x1FF) << 32) | self.regs.get(DMATRFBASE, 0) + fb = (base << 8) + self.regs.get(DMATRFFBOFFS, 0) + tcm_off = self.regs.get(DMATRFMOFFS, 0) & 0xFFFF + if cmd & CMD_WRITE: + ok = self.dma.copy(tcm + tcm_off, fb, size) + else: + ok = self.dma.copy(fb, tcm + tcm_off, size) + if ok: + self.dma_transfers += 1 + self.regs[DMATRFCMD] = (cmd | CMD_IDLE) & ~CMD_ERROR + else: + self.dma_refused += 1 + self.regs[DMATRFCMD] = (cmd & ~CMD_IDLE) | CMD_ERROR + + # -- state ------------------------------------------------------------- + + def state(self) -> dict: + return { + "irq_status": self.irq_status, + "regs": {str(k): v for k, v in sorted(self.regs.items())}, + "transcfg": list(self.transcfg), + "dma_transfers": self.dma_transfers, + "dma_refused": self.dma_refused, + "debuginfo_consumed": self.debuginfo_consumed, + "debuginfo_host_consume": self.debuginfo_host_consume, + } + + def load_state(self, data: dict) -> None: + self.irq_status = int(data.get("irq_status", 0)) + regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} + self.regs = {off: regs.get(off, 0) for off in _RW_REGISTERS} + self.transcfg = [int(v) for v in data.get("transcfg", [0] * FBIF_TRANSCFG_COUNT)] + self.dma_transfers = int(data.get("dma_transfers", 0)) + self.dma_refused = int(data.get("dma_refused", 0)) + self.debuginfo_consumed = int(data.get("debuginfo_consumed", 0)) + self.debuginfo_host_consume = bool( + data.get("debuginfo_host_consume", self.debuginfo_host_consume)) diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py new file mode 100644 index 0000000..8dcf633 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py @@ -0,0 +1,80 @@ +"""WPR2 / FWSEC state registers. + +Evidence kind: **hardware register/field header + firmware use** (the boot +flow locates the firmware write-protected region from these registers on +pre-Hopper parts; the host/booter writes them, the firmware reads them). + +Modelled registers (offsets in the FB/PFB register aperture): + +* ``NV_PFB_PRI_MMU_WPR2_ADDR_LO`` ``0x1FA824``: WPR2 start, bits ``31:4`` + shifted by ``NV_PFB_PRI_MMU_WPR2_ADDR_LO_ALIGNMENT = 12`` (4 KiB). +* ``NV_PFB_PRI_MMU_WPR2_ADDR_HI`` ``0x1FA828``: points at the start of the + last 128 KiB block of the WPR, same alignment; the end is + ``(value << 12) + 0x20000``. + +A profile that wants the firmware to see a WPR declares ``wpr2_start`` / +``wpr2_end``; without a declaration the window is unhandled and logged. +FWSEC-specific state is not modelled (no proven register use yet). +""" +from __future__ import annotations + +from .base import Device + +WPR2_ADDR_LO = 0x1FA824 +WPR2_ADDR_HI = 0x1FA828 +WPR2_ALIGNMENT = 12 +WPR2_BLOCK = 0x20000 + + +class Wpr2Block(Device): + """WPR2 address pair as seen by the firmware (host/booter-written).""" + + name = "fwsec" + + def __init__(self, *, base: int = 0, size: int = 0x1000, + wpr2_start: int | None = None, wpr2_end: int | None = None): + super().__init__(base=base, size=size) + self.reset() + if wpr2_start is not None and wpr2_end is not None: + self.set_range(wpr2_start, wpr2_end) + + def set_range(self, start: int, end: int) -> None: + if end <= start: + raise ValueError("WPR2 end must be greater than start") + self.regs[WPR2_ADDR_LO] = (start >> WPR2_ALIGNMENT) & 0xFFFFFFFF + self.regs[WPR2_ADDR_HI] = ((end - WPR2_BLOCK) >> WPR2_ALIGNMENT) & 0xFFFFFFFF + + @property + def start(self) -> int | None: + """WPR2 start derived from ADDR_LO (``None`` when unset).""" + lo = self.regs.get(WPR2_ADDR_LO) + return None if lo is None else lo << WPR2_ALIGNMENT + + @property + def end(self) -> int | None: + """WPR2 end derived from ADDR_HI (``None`` when unset).""" + hi = self.regs.get(WPR2_ADDR_HI) + return None if hi is None else (hi << WPR2_ALIGNMENT) + WPR2_BLOCK + + def reset(self) -> None: + self.regs: dict[int, int] = {} + + def read(self, offset: int, size: int) -> int | None: + if size == 4 and offset in self.regs: + return self.regs[offset] + return None + + def write(self, offset: int, size: int, value: int) -> bool: + if size == 4 and offset in (WPR2_ADDR_LO, WPR2_ADDR_HI): + self.regs[offset] = value & 0xFFFFFFFF + return True + return False + + def state(self) -> dict: + return {"regs": {str(k): v for k, v in sorted(self.regs.items())}, + "start": self.start, "end": self.end} + + def load_state(self, data: dict) -> None: + # start/end are derived from regs; serialised copies are ignored so a + # restored block can never report a stale range. + self.regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/irq.py b/debug/risc-v-gsp-rm-vm/sim/devices/irq.py new file mode 100644 index 0000000..7ce63fb --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/irq.py @@ -0,0 +1,58 @@ +"""IRQ routing seam. + +Evidence kind: **hardware register/field header + observed access pattern.** + +The GSP boot handshake sets the falcon ``IRQSSET`` HALT bit (bit 4) after +writing MAILBOX0/DEBUGINFO, and the boot code reads ``FALCON_IRQSTAT``; the +libos kernel owns the RISC-V-local ``RISCV_IRQMSET``/``IRQMCLR``/``IRQMASK`` +group. This module aggregates those two sources behind one object so a +future core-interrupt model can consume a single ``pending()`` surface. + +It does **not** deliver interrupts to the machine's hart: mstatus/mie/mip/ +mtvec trap entry is not modelled (documented unknown). It only makes the +routing state explicit and serialisable. +""" +from __future__ import annotations + +# Falcon IRQSSET/IRQSCLR bit positions used by the boot handshake (header +# field list; only the ones with proven firmware use are named). +FALCON_IRQ_GPTMR = 1 << 0 +FALCON_IRQ_WDTMR = 1 << 1 +FALCON_IRQ_MTHD = 1 << 2 +FALCON_IRQ_CTXSW = 1 << 3 +FALCON_IRQ_HALT = 1 << 4 +FALCON_IRQ_EXTERR = 1 << 5 +FALCON_IRQ_SWGEN0 = 1 << 6 +FALCON_IRQ_SWGEN1 = 1 << 7 +FALCON_IRQ_DMA = 1 << 16 +FALCON_IRQ_SHA = 1 << 17 + + +class IrqRouter: + """Aggregate falcon + RISC-V-local interrupt state.""" + + name = "irq" + + def __init__(self, *, falcon=None, priscv=None): + self.falcon = falcon + self.priscv = priscv + + def falcon_status(self) -> int: + return self.falcon.irq_status if self.falcon is not None else 0 + + def riscv_mask(self) -> int: + return self.priscv.irq_mask if self.priscv is not None else 0 + + def pending(self) -> dict: + """Raw pending state; delivery is deliberately not modelled.""" + return { + "falcon_irq_status": self.falcon_status(), + "falcon_halt": bool(self.falcon_status() & FALCON_IRQ_HALT), + "riscv_irq_mask": self.riscv_mask(), + } + + def state(self) -> dict: + return self.pending() + + def reset(self) -> None: + pass diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/pmc.py b/debug/risc-v-gsp-rm-vm/sim/devices/pmc.py new file mode 100644 index 0000000..1ccf1cd --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/pmc.py @@ -0,0 +1,90 @@ +"""PMC / master register block (first-touch device). + +Evidence kind: **hardware register/field header + firmware use + observed +access pattern.** + +* ``NV_PMC_BOOT_42`` lives at offset ``0xA00`` of the master (PMC) block and is + read by the GSP boot code through the PRIV window at + ``0x2000_0000_0000_0A00`` (observed 4-byte read, first MMIO of the real + 570.144 image; the QEMU baseline faults there). +* The firmware extracts bits ``28:20`` (``NV_PMC_BOOT_42_CHIP_ID``) and + compares against the TU10x IDs ``0x162..0x168``; only a match continues the + boot (observed branch chain at firmware vaddr ``0x4009020``). +* ``NV_PMC_BOOT_42`` field layout: ARCHITECTURE ``28:24`` (TU100 = ``0x16``), + IMPLEMENTATION ``23:20``, MAJOR_REVISION ``19:16``, MINOR_REVISION + ``15:12``, MINOR_EXTENDED_REVISION ``11:8``, CHIP_ID ``28:20``. + +Only ``0xA00`` is modelled; every other offset in the window stays unhandled +and is logged by the machine. +""" +from __future__ import annotations + +from .base import Device + +BOOT_42_OFFSET = 0xA00 +CHIP_ID_SHIFT = 20 +CHIP_ID_MASK = 0x1FF +ARCHITECTURE_MASK = 0x1F +IMPLEMENTATION_MASK = 0xF +MAJOR_MASK = 0xF +MINOR_MASK = 0xF +MINOR_EXT_MASK = 0xF + +#: Chip IDs seen in the firmware's comparison chain (header field values). +#: TU105 (0x165) is kept because it appears in the observed chain, but its +#: product mapping is unverified. The former ``TU107 = 0x167`` entry was a +#: transcription error (0x167 is TU117) and has been removed. +TU102 = 0x162 +TU104 = 0x164 +TU105 = 0x165 # unverified product mapping (observed in the chain only) +TU106 = 0x166 +TU116 = 0x168 +TU117 = 0x167 + + +def boot_42_value(chip_id: int, *, major: int = 0, minor: int = 0, + minor_ext: int = 0) -> int: + """Assemble an ``NV_PMC_BOOT_42`` value from its documented fields. + + ARCHITECTURE and IMPLEMENTATION are derived from ``chip_id`` (they are the + upper/lower nibbles of the same bits ``28:20``), matching the header's + ``CHIP_ID_TU104 = 0x164`` / ``ARCHITECTURE_TU100 = 0x16`` pair. + """ + chip_id &= CHIP_ID_MASK + arch = (chip_id >> 4) & ARCHITECTURE_MASK + impl = chip_id & IMPLEMENTATION_MASK + value = (chip_id << CHIP_ID_SHIFT) + value |= (arch << 24) | (impl << 20) + value |= (major & MAJOR_MASK) << 16 + value |= (minor & MINOR_MASK) << 12 + value |= (minor_ext & MINOR_EXT_MASK) << 8 + return value + + +class PmcBlock(Device): + """Master/PMC block: ``NV_PMC_BOOT_42`` chip identification.""" + + name = "pmc" + + def __init__(self, *, chip_id: int = TU104, major: int = 0, minor: int = 0, + minor_ext: int = 0, boot_42: int | None = None, + base: int = 0, size: int = 0x1000): + super().__init__(base=base, size=size) + self.chip_id = chip_id + self.boot_42 = boot_42 if boot_42 is not None else boot_42_value( + chip_id, major=major, minor=minor, minor_ext=minor_ext) + + def read(self, offset: int, size: int) -> int | None: + if offset == BOOT_42_OFFSET and size == 4: + return self.boot_42 + return None + + def peek(self, offset: int, size: int) -> int | None: + return self.read(offset, size) + + def write(self, offset: int, size: int, value: int) -> bool: + # NV_PMC_BOOT_42 is read-only (R--4R). + return False + + def state(self) -> dict: + return {"chip_id": self.chip_id, "boot_42": self.boot_42} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/pmu.py b/debug/risc-v-gsp-rm-vm/sim/devices/pmu.py new file mode 100644 index 0000000..b241c7e --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/pmu.py @@ -0,0 +1,46 @@ +"""PMU stub. + +Evidence kind: **none yet** (no PMU register has been observed in the +570.144 boot runs, and no PMU register semantics are needed to reach the +rungs achieved so far). The class exists so a profile can attach a PMU +window without inventing behaviour: every access stays unhandled and is +reported by the machine's unknown log, and the stub records the accesses it +saw for later identification. + +When PMU accesses are observed, each register is added here with its header +field evidence and a test, exactly like the falcon block. +""" +from __future__ import annotations + +from .base import Device + + +class PmuStub(Device): + """A PMU window with no proven semantics: observe, never fake.""" + + name = "pmu" + + def __init__(self, *, base: int = 0, size: int = 0x1000): + super().__init__(base=base, size=size) + self.reset() + + def reset(self) -> None: + self.seen: dict[str, int] = {} + + def read(self, offset: int, size: int) -> int | None: + self._note(offset, size, "r") + return None + + def write(self, offset: int, size: int, value: int) -> bool: + self._note(offset, size, "w") + return False + + def peek(self, offset: int, size: int) -> int | None: + return None + + def _note(self, offset: int, size: int, rw: str) -> None: + key = f"{rw}:{offset:#x}:{size}" + self.seen[key] = self.seen.get(key, 0) + 1 + + def state(self) -> dict: + return {"seen": dict(sorted(self.seen.items()))} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/priscv.py b/debug/risc-v-gsp-rm-vm/sim/devices/priscv.py new file mode 100644 index 0000000..62a8fea --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/priscv.py @@ -0,0 +1,113 @@ +"""RISC-V local (PRISCV) register block. + +Evidence kind: **hardware register/field header + observed access pattern.** + +Reached through the PRIV aperture at ``PRIV + 0x111000`` and through the libos +kernel's private mapping at ``0x3_0000_1000`` (the firmware reads the trace +and error-status registers there during boot). Modelled registers: + +================================ ====== ===== ============================== +register offset access semantics +================================ ====== ===== ============================== +``RISCV_IRQMSET`` 0x2ac W ``irq_mask |= value`` +``RISCV_IRQMCLR`` 0x2b0 W ``irq_mask &= ~value`` +``RISCV_IRQMASK`` 0x2b4 R ``irq_mask`` +``RISCV_IRQDEST`` 0x2b8 RW storage +``RISCV_IRQTYPE`` 0x2bc RW storage +``RISCV_TRACECTL`` 0x344 RW storage +``RISCV_TRACE_RDIDX`` 0x348 RW storage +``RISCV_TRACE_WTIDX`` 0x34c RW storage +``RISCV_PRIV_ERR_STAT`` 0x360 RW storage +``RISCV_HUB_ERR_STAT`` 0x36c RW storage +================================ ====== ===== ============================== + +The IRQ mask semantics (set/clear/read) follow the header access types and the +observed boot reads; core interrupt *delivery* (mtvec/mip) is not part of this +device and remains unmodelled (documented unknown). +""" +from __future__ import annotations + +from .base import Device + +IRQMSET = 0x2AC +IRQMCLR = 0x2B0 +IRQMASK = 0x2B4 +IRQDEST = 0x2B8 +IRQTYPE = 0x2BC +TRACECTL = 0x344 +TRACE_RDIDX = 0x348 +TRACE_WTIDX = 0x34C +PRIV_ERR_STAT = 0x360 +PRIV_ERR_INFO = 0x364 +PRIV_ERR_ADDR = 0x368 +HUB_ERR_STAT = 0x36C + +_RW = {IRQDEST, IRQTYPE, TRACECTL, TRACE_RDIDX, TRACE_WTIDX, + PRIV_ERR_STAT, HUB_ERR_STAT} +#: Read-only error capture registers (header R-I4R); the boot panic path +#: reads them after a trap (observed reads at PC 0x40106BC). +_RO = {PRIV_ERR_INFO, PRIV_ERR_ADDR} + + +class PriscvBlock(Device): + """RISC-V-local control/trace/error registers.""" + + name = "priscv" + + def __init__(self, *, base: int = 0, size: int = 0x1000): + super().__init__(base=base, size=size) + self.reset() + + def reset(self) -> None: + self.irq_mask = 0 + self.regs: dict[int, int] = {off: 0 for off in _RW} + self.error: dict[int, int] = {off: 0 for off in _RO} + + def read(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == IRQMASK: + return self.irq_mask + if offset in self.regs: + return self.regs[offset] + if offset in self.error: + return self.error[offset] + return None + + def write(self, offset: int, size: int, value: int) -> bool: + if size != 4: + return False + value &= 0xFFFFFFFF + if offset == IRQMSET: + self.irq_mask = (self.irq_mask | value) & 0xFFFFFFFF + return True + if offset == IRQMCLR: + self.irq_mask = (self.irq_mask & ~value) & 0xFFFFFFFF + return True + if offset == IRQMASK: + return False # read-only + if offset in self.regs: + self.regs[offset] = value + return True + return False + + def peek(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == IRQMASK: + return self.irq_mask + if offset in self.error: + return self.error[offset] + return self.regs.get(offset) + + def state(self) -> dict: + return {"irq_mask": self.irq_mask, + "regs": {str(k): v for k, v in sorted(self.regs.items())}, + "error": {str(k): v for k, v in sorted(self.error.items())}} + + def load_state(self, data: dict) -> None: + self.irq_mask = int(data.get("irq_mask", 0)) + regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} + self.regs = {off: regs.get(off, 0) for off in _RW} + err = {int(k): int(v) for k, v in data.get("error", {}).items()} + self.error = {off: err.get(off, 0) for off in _RO} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/timer.py b/debug/risc-v-gsp-rm-vm/sim/devices/timer.py new file mode 100644 index 0000000..c57ee06 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/timer.py @@ -0,0 +1,86 @@ +"""PTIMER block (modeled virtual time; not a silicon timing claim). + +Evidence kind: **hardware register/field header** (offsets and field layout) +plus a *modeled* time source. The firmware has not yet been observed reading +this block in the 570.144 boot runs (the observed time-like reads were on +falcon MAILBOX1); the device is therefore opt-in: it is registered only when +a profile declares a ``timer`` section, and a section with +``"enabled": false`` is skipped. Its values are explicitly labelled +*modeled*. + +Header facts used: + +* ``NV_PTIMER`` block base ``0x9000`` in the register aperture. +* ``NV_PTIMER_TIME_0`` at aperture offset ``0x9400``: nanoseconds bits + ``31:5``. +* ``NV_PTIMER_TIME_1`` at aperture offset ``0x9410``: nanoseconds bits + ``28:0``. +* Reads of TIME_0 latch TIME_1 (paired read), as on the hardware register + pair; the simulator derives nanoseconds from the machine's instruction + count times the profile's ``ns_per_insn`` (default 1.0, i.e. 1 ns/insn -- + a placeholder, not calibrated). + +Register offsets below are **relative to the block window base** the profile +declares (``PRIV + 0x9000``), i.e. aperture offset minus ``PTIMER_BASE``: +``0x9400 - 0x9000 = 0x400``. Using the aperture-absolute numbers here would +place the registers outside the declared window and serve nothing. +""" +from __future__ import annotations + +from .base import Device + +PTIMER_BASE = 0x9000 +#: Block-relative offsets of NV_PTIMER_TIME_0/1 (aperture 0x9400/0x9410). +TIME_0 = 0x400 +TIME_1 = 0x410 + + +class PtimerBlock(Device): + """Two-register nanosecond timer with a modeled tick source.""" + + name = "timer" + + def __init__(self, *, base: int = 0, size: int = 0x1000, + ns_per_insn: float = 1.0, clock=None): + super().__init__(base=base, size=size) + self.ns_per_insn = float(ns_per_insn) + self.clock = clock # callable returning virtual instruction count + self._latch = 0 + self.reads = 0 + + def _now_ns(self) -> int: + insns = self.clock() if self.clock is not None else 0 + return int(insns * self.ns_per_insn) & 0xFFFFFFFFFFFFFFFF + + def read(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == TIME_0: + self._latch = self._now_ns() + self.reads += 1 + return (self._latch >> 5) & 0xFFFFFFFF # NSEC field 31:5 + if offset == TIME_1: + self.reads += 1 + return self._latch & 0x1FFFFFFF # NSEC field 28:0 + return None + + def peek(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == TIME_0: + return (self._now_ns() >> 5) & 0xFFFFFFFF + if offset == TIME_1: + return self._now_ns() & 0x1FFFFFFF + return None + + def write(self, offset: int, size: int, value: int) -> bool: + return False + + def state(self) -> dict: + return {"ns_per_insn": self.ns_per_insn, "latch": self._latch, + "reads": self.reads} + + def load_state(self, data: dict) -> None: + self.ns_per_insn = float(data.get("ns_per_insn", self.ns_per_insn)) + self._latch = int(data.get("latch", 0)) + self.reads = int(data.get("reads", 0)) |
