summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/mpu.py
blob: 3f243583db9a7c17b5dd786523ad1c9511046ed3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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