Physical Memory Protection
Physical Memory Protection (PMP) is the optional RISC-V hardware unit that lets machine-mode (M-mode) software fence off ranges of physical memory with read, write, and execute permissions, with no address translation involved. Up to 64 PMP entries, each a comparator plus an 8-bit configuration byte, are checked in parallel with the Physical Memory Attribute (PMA) checks on every load, store, and instruction fetch performed from S-mode or U-mode (per the privileged spec, section “Physical Memory Protection”). The unit is cheap (a few hundred gates per entry), trap-precise, and exactly the right primitive for a small microkernel or a separation-kernel firmware: it isolates processes from each other on a CPU that has no Memory Management Unit (MMU) at all, and it isolates M-mode firmware from a Linux kernel on a CPU that does have one. This note walks the CSR layout, the four address-matching modes, the priority and lock semantics, the Smepmp extension, the relationship to paging, and a worked microkernel example. It closes with a comparison to the ARM Cortex-M Memory Protection Unit (MPU), which solves the same problem with a different encoding.
PMP belongs to the memory protection and virtual memory family of mechanisms. Where an MMU translates virtual addresses through page tables, PMP filters already-resolved physical addresses; it adds permission checking without adding indirection. For the definitely-not-esp32 v1.0 microkernel, PMP is the sole isolation primitive; the design is sized so each user process maps to a small bank of PMP entries that M-mode reprograms on context switch.
Mental Model
Think of PMP as a small content-addressable bank of “physical-address tripwires.” Each entry holds an address pattern plus three permission bits. On every memory access from a less-privileged mode, the hardware presents the physical address to all enabled entries simultaneously; the lowest-numbered entry that matches all bytes of the access decides whether the access is permitted, denied, or trapped. The entries with lower indices win; entries with no match at all fall through.
flowchart LR CPU["Load / store / fetch<br/>(S or U mode)"] -->|"physical address"| CHK{"PMP check<br/>(parallel, all entries)"} CHK -->|"lowest matching idx<br/>has R/W/X for op"| OK["Access proceeds"] CHK -->|"lowest matching idx<br/>denies, or all match-fail in S/U"| FAULT["Access-fault exception<br/>raises to M-mode"] subgraph BANK["64 PMP entries (per hart)"] E0["pmp0cfg + pmpaddr0"] E1["pmp1cfg + pmpaddr1"] E2["pmp2cfg + pmpaddr2"] ED["..."] E63["pmp63cfg + pmpaddr63"] end CHK -.->|"first-match priority"| BANK
The PMP decision path. What it shows: every memory access from S/U mode is compared in parallel against all enabled PMP entries; the lowest-indexed match supplies the R/W/X verdict. The insight to take: PMP is a tiny fully-associative permission lookup that runs alongside, not in series with, address translation. It costs nothing on hit and traps precisely on miss.
The cost model matters. PMP adds one parallel compare per entry per access. With 16 entries, that is 16 comparators in the load-store path; with 64 entries, 64 comparators. On a small in-order RV32 core this is a measurable area cost but not a critical-path cost: the checks finish inside the existing memory-access cycle. This is why PMP scales down to microcontrollers in a way that a full MMU does not.
CSR Layout
Each PMP entry is described by two CSRs: an 8-bit configuration byte and an address register that holds bits [33:2] of a 34-bit physical address on RV32, or bits [55:2] of a 56-bit physical address on RV64 (per machine.adoc, section “Physical Memory Protection CSRs”). The bottom two bits of any PMP address are implicit zero, fixing the granularity at four bytes minimum.
The configuration bytes are packed for efficiency. On RV32, sixteen CSRs pmpcfg0 through pmpcfg15 hold the configurations for the 64 entries, four bytes per CSR. On RV64, eight even-numbered CSRs (pmpcfg0, pmpcfg2, …, pmpcfg14) hold the same data, eight bytes per CSR; the odd-numbered configuration CSRs are illegal on RV64. The address registers are flat: pmpaddr0 through pmpaddr63, MXLEN bits wide.
Each 8-bit pmpcfg byte is laid out (per the spec’s bit-field figure, pmpcfg.edn):
bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| L | 0 0 | A1 A0| X | W | R |
+---+---+---+---+---+---+---+---+
- R, W, X (bits 2, 1, 0 are R, W, X). When set, the entry grants read, write, or execute permission to the matching range. The combination
R=0, W=1is reserved (per the WARL rules in machine.adoc). - A (bits 4:3). The address-matching mode:
00=OFF,01=TOR,10=NA4,11=NAPOT. - L (bit 7). The lock bit; once set, both
pmpcfgandpmpaddrfor that entry become read-only until the hart resets, and the R/W/X permissions then also apply to M-mode (more in the lock section below). - Bits 5 and 6 are reserved-as-zero.
An implementation may implement zero, sixteen, or sixty-four PMP entries; the lowest-numbered are filled first. All PMP CSR fields are Write-Any-Read-Legal (WARL), which means software discovers what is actually implemented by writing a probe value and reading it back. OpenSBI’s sbi_hart.c uses exactly this trick at boot: writing PMP_ADDR_MASK to pmpaddr0, then reading it back, reveals both the count of implemented entries and the platform’s effective granularity.
The Four Matching Modes
The A field selects how pmpaddr[i], and sometimes pmpaddr[i-1], define the byte range that entry i covers.
A=0, OFF. The entry is disabled and matches nothing. This is the reset state.
A=1, TOR (Top Of Range). The current entry’s pmpaddr register is the exclusive upper bound of the range; the previous entry’s pmpaddr[i-1] is the inclusive lower bound. The spec states it directly: “If PMP entry i’s A field is set to TOR, the entry matches any address y such that pmpaddr[i-1] <= y < pmpaddr[i]” (per machine.adoc). Entry 0 in TOR mode takes zero as its lower bound. TOR is the only PMP mode that can describe an arbitrary-size region; it costs two consecutive entries to express one region.
A=2, NA4 (Naturally Aligned 4-byte). A special case of NAPOT: the address register names exactly a 4-byte word. NA4 only exists when the platform granularity G equals one (4 bytes); larger grain platforms drop NA4 from the encoding.
A=3, NAPOT (Naturally Aligned Power-of-Two). This is the clever one. The size of the region is encoded in the trailing-ones pattern of the address register. The spec gives the table directly (per machine.adoc, table “NAPOT range encoding”):
pmpaddr low bits | meaning |
|---|---|
...yyyy yyy0 | 8-byte NAPOT |
...yyyy yy01 | 16-byte NAPOT |
...yyyy y011 | 32-byte NAPOT |
| … | … |
0111 1111 | 2^9-byte NAPOT |
1111 1111 | 2^10-byte NAPOT |
The leading y bits are the base address (shifted right by 2 because pmpaddr encodes physical-address bits [33:2]); the trailing-ones pattern says “this region is naturally aligned and is 2^(k+3) bytes wide for k trailing ones.” NAPOT lets a single entry describe a power-of-two region of any size from 8 bytes to the full address space. This is why a microkernel can fit four user processes into four PMP entries, one NAPOT region each, instead of paying eight TOR entries.
The granularity G is the same across all entries on a given platform. It is 2^(G+2) bytes; an implementation that uses 4 KiB pages will typically set G such that the grain is 4 KiB or finer. Software discovers G by writing 0 to pmp0cfg, writing all-ones to pmpaddr0, and noting the lowest bit that reads back as one; that bit position is G. OpenSBI does exactly this in hart_pmp_get_allowed_addr() (see sbi_hart.c).
Priority, Default-Deny, and Lock
PMP’s matching rule is lowest-numbered match wins, irrespective of permission outcome (per machine.adoc, section “Priority and Matching Logic”). If entry 5 grants read/write to a region that entry 0 has disabled, an S-mode access to that region fails: entry 0 matches first and supplies its (none) verdict. This priority discipline gives M-mode an unambiguous override pattern: lock down a critical region with a low-numbered entry, then layer per-process regions on top.
The matching is also all-or-nothing: “The matching PMP entry must match all bytes of a memory operation, or the operation fails” (per machine.adoc). A four-byte store that straddles two NAPOT regions, even if both grant write, fails because no single entry covers all four bytes. The spec gives the canonical example: an entry covering 0xC..0xF cannot satisfy an 8-byte access to 0x8..0xF.
The default behavior splits cleanly by privilege:
- No PMP entry matches an M-mode access. The access succeeds. M-mode has full access by default; PMP only revokes permissions from M-mode (and only when entries are locked, see below).
- No PMP entry matches an S- or U-mode access, but at least one entry is implemented. The access fails (per machine.adoc). Default deny.
The asymmetry is deliberate: in S/U mode you must explicitly grant access; in M-mode you must explicitly deny it. The L (lock) bit is the bridge. Setting L on an entry locks the entry against further writes until the hart is reset, and it additionally makes the R/W/X permissions enforced even on M-mode accesses to that range. Without L, a matching entry’s permissions only affect S/U; M-mode passes through.
This is how M-mode firmware protects itself. OpenSBI (per docs/domain_support.md) sets up at boot a locked PMP entry covering its own code and data with R/W/X all clear, which traps any S/U attempt to touch firmware memory; the lock means even a compromised M-mode driver running later cannot easily reach in either. Once locked, that entry stays until reset.
A PMP violation always traps precisely at the offending instruction (per machine.adoc, “PMP violations are always trapped precisely at the processor”). The resulting exception is instruction access-fault (cause 1), load access-fault (cause 5), or store access-fault (cause 7), distinguishing PMP violations from MMU page-faults (causes 12, 13, 15), so the M-mode trap handler can tell them apart.
Mechanical Walk-through: A Microkernel Context Switch
Imagine a small RV32IMC microkernel with two user processes, P0 and P1, each given a 64 KiB chunk of RAM. Process P0 occupies 0x8001_0000..0x8001_FFFF; process P1 occupies 0x8002_0000..0x8002_FFFF. The kernel sits at 0x8000_0000..0x8000_FFFF. The kernel reserves three PMP entries:
- Entry 0, locked, NAPOT covering
0x8000_0000..0x8000_FFFF, no R/W/X. Protects kernel image from any user access; locked so it survives malicious or buggy kernel code. - Entry 1, unlocked, NAPOT covering one user process’s RAM, R/W (no X, since the kernel may make code pages a separate entry).
- Entry 2 reserved for the second NAPOT user code region.
The NAPOT encoding for a 64 KiB region at 0x8001_0000 is constructed as follows. pmpaddr encodes physical bits [33:2], so the base is right-shifted by 2, giving 0x2000_4000. A 64 KiB region is 2^16 bytes, so the NAPOT tag wants (k+3)=16, i.e. 13 trailing ones below the size bit. The bits below the size bit must all be ones, and the size bit (the next bit up) must be zero. Concretely the value is 0x2000_4000 | ((0x10000 >> 3) - 1) = 0x2000_5FFF. Writing this to pmpaddr1, and writing 0b00011111 (L=0, A=NAPOT, X=0, W=1, R=1) to the first byte of pmpcfg0, enables read/write for entry 1.
On context switch from P0 to P1, the kernel:
- Writes the new NAPOT
pmpaddr1for P1’s RAM. - (Optionally) updates the per-process
pmpcfgbyte if P1’s RAM has different permissions. - Executes
sfence.vma x0, x0only if the hart implements paging — see the qualification below; on an M/U-mode core with no MMU this step is unnecessary and may itself be illegal. Where it does apply, it is because “when the PMP settings are modified, M-mode software must synchronize the PMP settings with the virtual memory system and any PMP or address-translation caches” (per machine.adoc, “Physical Memory Protection and Paging”). - Executes
mretto return to U-mode at P1’s saved PC.
The full switch is a handful of CSR writes, plus a fence only where paging exists. There is no page-table walk, no TLB miss, no shootdown across cores (PMP is per-hart and there is only one in this design). This is the whole appeal of PMP for a small system: a cheap, deterministic isolation primitive that supports the operations a microkernel actually performs.
The Smepmp Extension
The base PMP design has a known weakness in the “M-mode is its own OS” use case. On a hart with no S-mode, the OS runs in M-mode and U-mode runs the applications. Without Smepmp, every memory region that is not covered by a locked PMP entry is fully accessible to M-mode, including any RAM containing user code, so a successful confused-deputy attack on the M-mode OS can be coerced into executing user-controlled code or reading user data through a high-privilege channel. The Smepmp specification document spells out this threat directly: “without the Smepmp extension, it is not possible for a PMP rule to be enforced only on non-Machine modes and denied on Machine mode” (per smepmp.adoc).
Smepmp (“ePMP”) adds the mseccfg CSR with three bits, MML, MMWP, RLB:
- MML (Machine Mode Lockdown), when set, re-interprets every PMP entry’s L/R/W/X combination according to a new truth table (reproduced in smepmp.adoc) where unlocked rules become S/U-only and locked rules carry stricter M-mode semantics. The encoding
R=0, W=1(reserved in base PMP) becomes a “shared region” indicator;L=1, R=1, W=1, X=1becomes a locked shared-data region readable by all modes. The effect is to give M-mode software a deny mode for U-mode memory. - MMWP (Machine Mode Whitelist Policy) turns the M-mode default from “allow if no entry matches” to “deny if no entry matches.” Once set, M-mode software must explicitly cover every region it intends to touch.
- RLB (Rule Locking Bypass) lets early M-mode firmware temporarily ignore the L bit so it can rewrite locked rules during boot, then clear RLB to make the lockdown final.
MML and MMWP are sticky-set: once you write a 1, the bit is locked at 1 until reset. RLB is sticky-clear: once you write a 0, it stays at 0. This asymmetry is the discovery mechanism. As the spec notes, “BootROM will set mseccfg.MMWP and/or mseccfg.MML during early boot … so that the firmware will be able to determine the presence of Smepmp by reading mseccfg” (per smepmp.adoc). OpenSBI uses Smepmp where available; its sbi_hart_smepmp_configure() in sbi_hart_pmp.c makes two passes over domain regions, programming M-only regions first under mseccfg.RLB, then shared and S/U-only regions after setting mseccfg.MML.
For the definitely-not-esp32 v1.0 microkernel, Smepmp is desirable but not in scope; the simpler base PMP suffices for two user processes plus a kernel.
PMP and Paging
PMP composes with virtual memory (Sv32 on RV32, Sv57 on RV64). When paging is enabled, the page-table walk itself generates implicit memory accesses to fetch PTEs; PMP applies to each of those implicit accesses, with effective privilege “S.” This is critical for correctness: it means M-mode firmware can deny S-mode access to the firmware’s own data even when the OS attempts to use it as a page-table.
The page-walker is also permitted to translate speculatively and to cache PMP outcomes alongside translations, which is why modifying any PMP CSR requires an sfence.vma x0, x0 to invalidate caches (per machine.adoc, “PMP settings … must be synchronized … by executing an SFENCE.VMA instruction with rs1=x0 and rs2=x0, after the PMP CSRs are written”). On a hart with no paging, PMP checks are synchronous; the fence is unnecessary, since there is no translation cache to invalidate.
A subtle interaction: misaligned accesses may be decomposed into multiple memory operations, and each operation is PMP-checked independently. A misaligned store that straddles a permitted region and a denied region can leave a partial result visible before the access-fault exception is taken (per machine.adoc). The implication for software is that exception handlers cannot assume the store had no effect at all; recovery must be designed to tolerate partial visibility.
Configuration Example
A worked snippet showing how OpenSBI writes a NAPOT entry, distilled from the inline assembly in sbi_hart_pmp.c:
/* set a PMP NAPOT region covering [base, base+(1<<order))
with permissions pmp_flags (R/W/X bits, plus A=NAPOT, L) */
static int pmp_set(unsigned int n, unsigned long pmp_flags,
unsigned long base, unsigned long order)
{
unsigned long addr;
/* base must be aligned to 2^order; order >= log2(grain) */
if (base & ((1UL << order) - 1))
return -1;
/* NAPOT encoding: addr = (base >> 2) | ((1 << (order-3)) - 1)
The (-3) accounts for the (-2) shift built into pmpaddr
(low two physical-address bits implicit zero) plus the
trailing-ones convention starting at the third-lowest bit. */
addr = (base >> PMP_SHIFT) | ((1UL << (order - PMP_SHIFT - 1)) - 1);
/* Write address first, then config; A=NAPOT in pmp_flags */
csr_write_num(CSR_PMPADDR0 + n, addr);
pmp_csr_set_bits(n, pmp_flags << ((n & 3) * 8));
/* Synchronize PMP/TLB caches. Required only on a hart that implements
* paging; machine.adoc scopes the requirement to the virtual-memory
* system. On an M/U-mode core with no MMU (e.g. definitely-not-esp32)
* omit this — there is no translation cache, and SFENCE.VMA may trap. */
#if defined(__riscv_supervisor) || defined(PMP_HART_HAS_MMU)
asm volatile("sfence.vma x0, x0");
#endif
return 0;
}The shift arithmetic is the one error-prone piece. PMP_SHIFT = 2 (the implicit low-bit drop). order is log2(region_size); for a 64 KiB region, order = 16, so (1UL << (16-2-1)) - 1 = 0x1FFF, the trailing-ones pattern that says “16-byte-shifted base, 64 KiB span.” Get this wrong and the region either covers half what you intended or covers far more.
Failure Modes
Forgetting the fence. A modified PMP entry that takes effect “later” usually means the page-walker cached the old PMP result for an in-flight translation, and the new entry is not yet enforced. The fix is the unconditional sfence.vma x0, x0 after any PMP CSR write on a hart with paging enabled.
Trying to grant W without R. R=0, W=1 is reserved (per machine.adoc). The WARL behavior is implementation-defined; some implementations clear W on the write, some leave the entry effectively disabled.
Region not naturally aligned for NAPOT. A NAPOT region’s base address must be a multiple of its size. Writing a misaligned base will read back with the low bits masked off; the resulting region is not what was intended. Always assert (base & (size - 1)) == 0 before encoding.
Locking out the bootloader. Setting L on an entry that excludes the M-mode code region itself is a permanent foot-gun: until reset, M-mode cannot fetch from its own code. The lock cannot be cleared; only a hart reset clears it. Smepmp’s RLB bit exists partly to mitigate this during early boot.
Counting PMP entries wrong. The number of implemented PMP entries is platform-specific (0, 16, or 64). Software that hard-codes “use entries 7 through 15” works on the SiFive U54’s 8-entry PMP unit (SiFive U54 manual documents “8 regions and a minimum granularity of 4 bytes”) only by accident: entries 7 through 15 do not exist there. Always discover the count by probing.
Alternatives and When to Choose Them
Full MMU with page tables. A MMU gives per-process virtual address spaces, demand paging, copy-on-write, file-backed mmap, swap. The cost is large: a multi-level page table per process, a TLB, TLB-flush logic, page-fault handling in software, area for the walker. For a microkernel without virtual memory ambitions, PMP gets you the isolation half of that without the rest. For a Unix-style kernel, an MMU is unavoidable.
No isolation (everything in one address space). Some bare-metal RTOS systems run all threads with full access to all memory. This is fine for trusted firmware and small, single-purpose devices; it is not safe for running mutually untrusted user code. PMP is the smallest step up from “no isolation” that preserves the ability to run untrusted user processes.
IOPMP and SiFive Shield. PMP only governs accesses by the CPU itself. DMA-capable devices bypass it. To extend the same per-region permission model to bus masters, RISC-V defines IOPMP, and SiFive ships a proprietary equivalent (Shield). These are out of scope for a single-core hobby SoC but matter for any real product. OpenSBI mentions them in docs/domain_support.md as alternative or supplementary isolation primitives.
ARM Cortex-M MPU (PMSAv7 / PMSAv8). The closest cousin in the ARM ecosystem. Architecturally similar: a small bank of region descriptors checking physical (or in Cortex-M, the single-address-space) accesses. The differences are illuminating.
| Feature | RISC-V PMP | ARM Cortex-M MPU (PMSAv7, Armv7-M) |
|---|---|---|
| Region count | 0, 16, or 64 (implementation choice) | MPU_TYPE.DREGION, IMPLEMENTATION DEFINED: 8 on Cortex-M3, 8 or 16 on Cortex-M7 (DDI 0489 r1p2, DDI 0337 r2p0) |
| Region encoding | NAPOT, NA4, TOR, OFF | Base + size; naturally aligned power of two, RegionBaseAddress[(N-1):0] = 0 where N = log2(SizeofRegion_in_bytes) (DDI 0403E.e §B3.5) |
| Size range | 4 B to the full address space | 2^N for 5 ≤ N ≤ 32, i.e. 32 bytes to 4 GB (DDI 0403E.e §B3.5) |
| Granularity (min) | 4 bytes | 32 bytes (MPU_RASR.SIZE values below 4 are reserved) |
| Overlap priority | Lowest index wins | Highest index wins — “Where there is an overlap between two regions, the register with the highest region number takes priority” (DDI 0403E.e §B3.5) |
| Privilege protection | L bit promotes rule to M-mode | MPU_RASR.AP[2:0], a joint privileged/unprivileged permission encoding |
| Execute permission | Independent X bit in pmpcfg | Single MPU_RASR.XN bit; execution additionally requires read permission for the current privilege level |
| Sub-regions | none | 8 sub-region disable bits (MPU_RASR.SRD) for regions ≥ 256 bytes |
| Address space | Physical | Physical (no virtual addressing in M-profile) |
Verified field-by-field against the Armv7-M Architecture Reference Manual, ARM DDI 0403E.e, sections B3.5 to B3.5.9, pages B3-632 to B3-642.
The priority polarities are inverted. On ARM, you put the most specific region last so it overrides the broader background regions; on RISC-V, you put the most specific region first. This is a frequent source of porting bugs.
PMSAv7 register layout, from the architecture manual
The Armv7-M Memory Protection Unit is programmed through five registers in the System Control Space, plus three aliases. MPU_TYPE at 0xE000ED90 is read-only and reports the region count in DREGION, bits[15:8]; IREGION, bits[23:16], and SEPARATE, bit[0], both read as zero because “Armv7-M only supports a unified MPU”. MPU_CTRL at 0xE000ED94 holds three bits: ENABLE (bit[0]), HFNMIENA (bit[1], whether the MPU stays on for HardFault, NMI and FAULTMASK handlers), and PRIVDEFENA (bit[2], whether the default system memory map acts as a background region for privileged accesses — the manual describes it as acting as “region number -1”, so every configured region overrides it). MPU_RNR at 0xE000ED98 selects which region the two configuration registers address, in REGION, bits[7:0].
The two per-region registers are the ones that matter:
MPU_RBAR (0xE000ED9C) — Region Base Address Register
31 5 4 3 0
+---------------------------------------+-----+-------+
| ADDR (base address) |VALID|REGION |
+---------------------------------------+-----+-------+
ADDR bits[31:5] base address of the region; must be aligned to the region size
VALID bit[4] write 1 to also set MPU_RNR.REGION from this write's REGION field
REGION bits[3:0] region number for the VALID shortcut (0-15 only); reads back MPU_RNR.REGION[3:0]
MPU_RASR (0xE000EDA0) — Region Attribute and Size Register
31 29 28 27 26 24 23 22 21 19 18 17 16 15 8 7 6 5 1 0
+-----+--+-----+----+---+------+--+--+--+---------+---+--------+--+
| Rsv |XN| Rsv | AP | R | TEX | S| C| B| SRD |Rsv| SIZE |EN|
+-----+--+-----+----+---+------+--+--+--+---------+---+--------+--+
XN bit[28] execute never
AP bits[26:24] access permissions (see table below)
TEX bits[21:19] type extension; with C and B selects the memory type
S bit[18] shareable
C bit[17] cacheable
B bit[16] bufferable
SRD bits[15:8] sub-region disable, bit[8] = lowest sub-region, bit[15] = highest
SIZE bits[5:1] region size in bytes is 2^(SIZE+1); values < 4 are reserved
ENABLE bit[0] region enable
Rsv / R reserved: bits[31:29], bit[27], bits[23:22], bits[7:6]ASCII box diagrams are used here rather than mermaid packet-beta because these are two separate 32-bit registers whose fields need per-bit annotation in the same view; the layouts and every field position are transcribed from ARM DDI 0403E.e pages B3-639 and B3-640. The insight to take: unlike RISC-V’s pmpcfg byte, which carries three orthogonal R/W/X bits, ARM packs read and write permission plus the privilege dimension into a single three-bit AP field, and puts execute permission in a separate XN bit that is only meaningful when AP already grants read.
The AP[2:0] encoding is where the two designs diverge most visibly. RISC-V gives you R, W and X as three independent bits and applies the entry to S-mode and U-mode alike (with the L bit extending it to M-mode). ARM enumerates the useful combinations of privileged and unprivileged permission in one field (ARM DDI 0403E.e, Table B3-15, page B3-642):
AP[2:0] | Privileged access | Unprivileged access | Notes |
|---|---|---|---|
000 | No access | No access | any access generates a permission fault |
001 | Read/write | No access | privileged access only |
010 | Read/write | Read-only | any unprivileged write generates a fault |
011 | Read/write | Read/write | full access |
100 | UNPREDICTABLE | UNPREDICTABLE | reserved |
101 | Read-only | No access | privileged read-only |
110 | Read-only | Read-only | privileged and unprivileged read-only |
111 | Read-only | Read-only | same as 110 |
What it shows: the eight AP encodings and the permission pair each one grants. The insight to take: ARM cannot express “unprivileged write, privileged no-access” at all, because the encoding is a fixed enumeration rather than a bit vector. RISC-V PMP has the mirror-image limitation for a different reason — a PMP entry without the L bit is simply not consulted for M-mode accesses, so M-mode is unconstrained rather than separately encodable.
PMSAv8 is a different machine, not a tweak
PMSAv8-M (the Armv8-M protected memory system architecture, implemented by Cortex-M23, M33 and M55 among others) is not a refinement of PMSAv7’s encoding — it replaces it. Verified against the Armv8-M Architecture Reference Manual, ARM DDI 0553B.z, section B10.1 and the register descriptions in D1.2.169 to D1.2.175:
- Base and limit, not base and size. A region is defined by the inequality
Address >= MPU_RBAR.BASE:'00000' && Address <= MPU_RLAR.LIMIT:'11111'(DDI 0553B.z, ruleRJVCN).MPU_RBARat0xE000ED9CholdsBASEin bits[31:5],SH(shareability) in bits[4:3],AP[2:1]in bits[2:1] andXNin bit[0].MPU_RLARat0xE000EDA0holdsLIMITin bits[31:5],PXN(privileged execute-never, Armv8.1-M onward) in bit[4],AttrIndxin bits[3:1] andENin bit[0]. The power-of-two size constraint is gone; what remains is that “All MPU regions are aligned to a multiple of 32 bytes” (ruleIGLXD), which follows mechanically from both address fields starting at bit 5. - Access permissions shrink to two bits.
MPU_RBAR.AP[2:1]encodes exactly four cases:00read/write privileged only,01read/write any privilege level,10read-only privileged only,11read-only any privilege level. The read/write/privilege matrix that PMSAv7 spread over eightAP[2:0]encodings collapses into a clean two-axis product of {read-only, read-write} × {privileged only, any}. - Memory attributes move behind an indirection.
MPU_RLAR.AttrIndxis a three-bit index into eight byte-wide attribute slots held inMPU_MAIR0at0xE000EDC0andMPU_MAIR1at0xE000EDC4. Each register packs four attributes:Attrmoccupies bits[8m+7:8m] for m = 0 to 3, soMPU_MAIR0holdsAttr0in bits[7:0] throughAttr3in bits[31:24], andMPU_MAIR1holdsAttr4throughAttr7. Cacheability is therefore described once, in a shared table, rather than re-encoded in every region descriptor the way PMSAv7’sTEX/C/B/Sbits are. - Overlap is a fault, not a priority. This is the correction that matters most for anyone porting between the three architectures. PMSAv7 resolves overlap by highest region number; PMSAv8-M does not resolve it at all. DDI 0553B.z rule
RLLLPstates that an MPU check “will generate a precise MemManage Fault if … the address accessed by the load, store or instruction fetch transaction matches more than one MPU region”, and the architecture pseudocode confirms it by clearing the hit and invalidating the permissions the moment a second region matches. Overlapping regions are a programming error on Armv8-M, whereas they are the normal idiom on both PMSAv7 (specific-over-general) and RISC-V PMP (lowest index wins).
So the three designs occupy three distinct points: RISC-V PMP resolves overlap by lowest index, PMSAv7 by highest index, and PMSAv8-M forbids it outright. Only the RISC-V TOR mode and PMSAv8-M share the base/limit shape; PMSAv7’s naturally aligned power-of-two regions are the closer analogue of RISC-V NAPOT.
Resolved 2026-08-08
The earlier flag reported that
developer.arm.comfetches returned only a generic page title. That is still true of the human-facing URLs —https://developer.arm.com/documentation/ddi0403/latest/returns HTTP 200 but the body is a 9,852-byte React single-page-application shell whose only<title>is “Documentation – Arm Developer”, which is exactly the generic title the earlier round saw. The documents themselves are reachable through the backing content API:https://documentation-service.arm.com/documentation/ddi0403/latestreturns HTTP 200 with a JSON record whose_links.resources[]array carries a direct PDF href, and that href served DDI0403E_e_armv7m_arm.pdf at HTTP 200, 5,957,561 bytes, and DDI0553B_z_armv8m_arm.pdf at HTTP 200, 16,439,994 bytes. Cortex-M technical reference manuals came down the same way (DDI 0337 r2p0 for Cortex-M3, DDI 0489 r1p2 for Cortex-M7, both HTTP 200); the Cortex-M33 TRM, document 100230 r1p0, is HTML rather than PDF and its “Processor configuration options” topic was read through the same API.Every figure in the comparison table, both register layouts, the
APencoding table, and the PMSAv8-M facts above are now transcribed from those primary manuals — Armv7-M Architecture Reference Manual, ARM DDI 0403E.e (ID021621), sections B3.5 through B3.5.9, and Armv8-M Architecture Reference Manual, ARM DDI 0553B.z (ID06022026), section B10.1 and register descriptions D1.2.169 through D1.2.175. The secondary sources previously relied on (Feabhas blog, Wadix articles, Wikipedia’s MPU page) are no longer load-bearing for any claim in this section. Two corrections fell out of the exercise: the region count was stated as “typically 8, sometimes 16” on the authority of blog posts and is in factMPU_TYPE.DREGION, IMPLEMENTATION DEFINED per part (8 on Cortex-M3; 8 or 16 on Cortex-M7; 0, 4, 8, 12 or 16 for each Security state on Cortex-M33), and the claim that PMSAv8 merely “drops the alignment constraint” understated the change — PMSAv8-M makes overlapping regions a MemManage fault rather than a priority question.
Production Notes
OpenSBI as the reference. Every shipping RISC-V Linux system goes through OpenSBI, which programs PMP at boot to fence the firmware from S-mode. The docs/domain_support.md document is the canonical guide to the “domain” abstraction: each domain is a set of memory regions plus a set of harts, and the firmware reprograms PMP whenever a hart migrates between domains.
ESP32-C3. Espressif’s RISC-V microcontroller (the reference point for definitely-not-esp32) uses PMP plus a vendor-specific PMA (called “World Controller”) to support a dual-world model for secure boot. The PMP entry count on the C3 is implementation-defined but documented in the ESP32-C3 Technical Reference Manual.
SiFive U54-MC (HiFive Unleashed). The U54-MC’s PMP unit has 8 regions per hart and 4-byte granularity (SiFive U54-MC manual). Linux on this board relies on OpenSBI to set up an outer fence for firmware; everything else runs inside paged S-mode.
definitely-not-esp32 v1.0. The plan is 16 PMP entries, NAPOT-encoded, with one locked low-numbered entry for the kernel image, then up to eight per-process regions reprogrammed on context switch. Smepmp is not implemented in v1.0; revisiting it is a Phase 6 stretch.