summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/mpu.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
commit1f2073d4fbb1d412d9bbf373312e3c51b83db92a (patch)
treea661537d7ec9510324e0a40a5a65e4b8c853f01b /debug/risc-v-gsp-rm-vm/sim/mpu.py
parentc1adf2727983f5cace8ca0f5d1119ac2256c348e (diff)
downloadopen-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.tar.gz
open-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.zip
greeneye: GMMU/FBIF observability, PTE decoder and oracle, MPU/libos simulator wave
Simulator development wave (docs 42-44): GMMU page-table entry decode with its oracle/workflow harness, FBIF observability, and MPU/LIBOS device surfaces in the device model, plus refreshed boot/machine/profile data (tu104-570.144), the qemu-line results, and the new unit tests. Docs 00/06/35-41 updated to match; harness helpers and .gitignore included.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/mpu.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/mpu.py154
1 files changed, 154 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/mpu.py b/debug/risc-v-gsp-rm-vm/sim/mpu.py
new file mode 100644
index 0000000..3f24358
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/mpu.py
@@ -0,0 +1,154 @@
+"""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