"""RISC-V MMU (NVMPU) CSR model for the GSP core. Evidence kind: **hardware CSR/field header + firmware use + observed access sequence.** The GSP RISC-V core boots in *bare* mode and then enables a small protection unit through CSRs; the firmware maps resident code/data into TCM and uses a scratch window to copy it there. Without this model the flat-RAM simulator serves those accesses at their virtual addresses and the relocated copy reads the wrong backing (the B2 ceiling in ``docs/36-GSP-DEVICE-MODEL.md``). Modelled CSRs (M-mode values from the hardware CSR header; the firmware writes exactly these - observed at the 570.144 bootstrap ``0x4007000``..``0x4007C00``): ============================================ ====== ========================= CSR value semantics ============================================ ====== ========================= ``MMPUIDX`` (0x7C8) entry INDEX field 5:0 ``MMPUVA`` (0x7C9) VA BASE 63:12, VLD bit 0 ``MMPUPA`` (0x7CA) PA BASE 63:12 ``MMPURNG`` (0x7CB) range RANGE 63:12 (a size) ``MMPUATTR`` (0x7CC) attr recorded, not enforced ``mstatus.VM`` (0x300) mode 0x1E = NVMPU (enabled) ============================================ ====== ========================= Semantics modelled: * an entry is *valid* when ``MMPUVA.VLD`` is set; writes to ``MMPUVA`` with VLD clear disable the selected entry (the firmware's clear sequence); * translation requires ``mstatus.VM == NVMPU`` (``0x1E``, field bits 28:24); before the enable write the CSR entries exist but no access is translated; * lookup is **lowest-index-first** (the firmware's own bootstrap comment: "the MPU gives precedence to lower mappings"); a matching entry maps ``addr`` to ``pa + (addr - va_base)`` when ``va_base <= addr < va_base + range``; * **no match is not a fault here**: the machine treats "no matching entry" as identity. This is deliberately more lenient than silicon (which would fault under the kernel's management) because the simulator keeps local memory flat and the boot path's identity mappings are self-consistent; the choice is recorded in the session and in ``docs/36`` ยง6. Permissions and cache/coherence attributes are recorded for evidence but not enforced; no timing or side-channel claim is made. This module has no device or host dependency and is deterministic. """ from __future__ import annotations #: CSR numbers (M-mode protection unit; hardware CSR header values). MMPUIDX = 0x7C8 MMPUVA = 0x7C9 MMPUPA = 0x7CA MMPURNG = 0x7CB MMPUATTR = 0x7CC #: CSRs this model consumes when written by the guest. WATCHED_CSRS = (MMPUIDX, MMPUVA, MMPUPA, MMPURNG, MMPUATTR) MSTATUS = 0x300 MSTATUS_VM_SHIFT = 24 MSTATUS_VM_MASK = 0x1F << MSTATUS_VM_SHIFT #: mstatus.VM value that enables the protection unit (header addendum). MSTATUS_VM_NVMPU = 0x1E #: Field masks (header: BASE/RANGE fields start at bit 12, 4 KiB granularity). _PAGE_MASK = ~0xFFF & ((1 << 64) - 1) _INDEX_MASK = 0x3F _MAX_ENTRIES = 64 class RiscvMpu: """MMPU entry table + mstatus enable state (deterministic, serialisable).""" def __init__(self) -> None: self.index = 0 self.mstatus = 0 self.entries: dict[int, dict] = {} #: Ascending [(va_base, pa, range)] of valid entries; kept in sync on #: every CSR write so translation is a short loop. self._active: list[tuple[int, int, int]] = [] self.enabled = False # -- guest CSR writes -------------------------------------------------- def on_csr_write(self, csr: int, value: int) -> None: """Apply one guest CSR write (``value`` already masked to 64 bits).""" if csr == MMPUIDX: self.index = value & _INDEX_MASK elif csr == MMPUVA: entry = self.entries.setdefault(self.index, self._blank()) entry["va"] = value & _PAGE_MASK entry["valid"] = bool(value & 1) self._refresh() elif csr == MMPUPA: entry = self.entries.setdefault(self.index, self._blank()) entry["pa"] = value & _PAGE_MASK elif csr == MMPURNG: entry = self.entries.setdefault(self.index, self._blank()) entry["range"] = value & _PAGE_MASK elif csr == MMPUATTR: entry = self.entries.setdefault(self.index, self._blank()) entry["attr"] = value elif csr == MSTATUS: self.mstatus = value & ((1 << 64) - 1) self.enabled = ((self.mstatus & MSTATUS_VM_MASK) >> MSTATUS_VM_SHIFT == MSTATUS_VM_NVMPU) @staticmethod def _blank() -> dict: return {"va": 0, "pa": 0, "range": 0, "attr": 0, "valid": False} def _refresh(self) -> None: self._active = [ (e["va"], e["pa"], e["range"]) for _, e in sorted(self.entries.items()) if e.get("valid") and e.get("range") ] # -- translation ------------------------------------------------------- @property def active(self) -> bool: """True when a matching entry could translate (fast-path switch).""" return self.enabled and bool(self._active) def translate(self, addr: int) -> int | None: """Virtual -> physical for ``addr``, or ``None`` when unmatched.""" if not self.enabled: return None for va, pa, rng in self._active: off = addr - va if 0 <= off < rng: return pa + off return None # -- evidence / snapshot ---------------------------------------------- def state(self) -> dict: return { "enabled": self.enabled, "mstatus": self.mstatus, "index": self.index, "entries": [ {"index": i, **{k: e[k] for k in ("va", "pa", "range", "attr", "valid")}} for i, e in sorted(self.entries.items()) if e.get("valid") or e.get("pa") or e.get("range") or e.get("attr") ], } def reset(self) -> None: self.index = 0 self.mstatus = 0 self.entries.clear() self._active = [] self.enabled = False