armhub.dev
Cortex-M · 24 pages
Cortex-M

MPU on Cortex-M

The Memory Protection Unit — how Cortex-M defines memory regions, what attributes they carry, how privileged and unprivileged code see different views, and the rules for stack overflow detection, RTOS task isolation, and execute-never enforcement.

6 min read · 1,210 wordsupdated · May 13, 2026

The MPU is the Cortex-M's lightweight memory protection mechanism — a small table of regions, each with an address range, size, and a set of access permissions. It is not a full MMU: there is no virtual-to-physical translation, no page tables, no TLB. Every region maps a contiguous slice of the 4 GB physical address space, and the MPU's only job is to check whether the current access is allowed and to raise a fault if it isn't.

This is plenty for what Cortex-M parts need: stack-overflow detection, RTOS task isolation, write-protecting flash from runaway pointer bugs, marking RAM as execute-never to defeat shellcode, and partitioning a single chip between privileged kernel and unprivileged application code. The MPU is also a hard requirement for security-certified profiles (IEC 61508, ISO 26262, Common Criteria) — without it, you can't make a credible argument that your bootloader and your application don't interfere.

MPU at a glance
Architecture variants
ARMv7-M MPU (M3, M4, M7) and ARMv8-M MPU (M23, M33, M55, M85) — different register layouts and region rules
Number of regions
Typically 8 or 16 (implementation-defined). MPU_TYPE.DREGION gives the actual count at runtime
Region size constraints (v7-M)
Power-of-two, ≥32 B, ≤4 GB, aligned to its own size
Region size constraints (v8-M)
Any 32-byte-aligned start and limit — no power-of-two restriction
Sub-regions (v7-M only)
8 equal subregions per region, individually disable-able (for sizes ≥256 B)
Default behaviour
MPU disabled at reset. Until enabled, all memory is accessible per the architectural default memory map
Privilege levels
Two: privileged (handler mode + thread mode with CONTROL.nPRIV=0), unprivileged (thread mode with CONTROL.nPRIV=1)
Fault on violation
MemManage fault (synchronous). Faulting address goes to MMFAR

What the MPU does — and doesn't

The MPU compares every load and store address (and instruction fetch) against its region table. If the access falls inside a region, the region's permissions decide whether it's allowed. If the access falls outside every region, the architectural default applies — and, importantly, on Cortex-M the default is "allowed only if executing privileged; deny if unprivileged."

The MPU does not translate addresses. A physical address stays a physical address. There is no per-process address space; every "process" (RTOS task) sees the same flat 32-bit memory map. What the MPU does provide is per-context permission masks: by reprogramming the region table at every context switch, an RTOS can give each task its own view of which memory it's allowed to touch.

The MPU is not the same as TrustZone. TrustZone-M partitions memory between secure and non-secure worlds via the SAU/IDAU. The MPU partitions memory between privileged and unprivileged execution within a single world. Both can be present on an ARMv8-M part; they enforce orthogonal policies.

Region descriptors — ARMv7-M

On ARMv7-M, each region is described by a triple of registers:

MPU_RNR
32-bit · reset 0x0000_0000
28
24
20
16
12
8
4
0
b24
b16
b8
b0
BitsNameAccResetDescription
31:8-RO0Reserved.
7:0REGIONRW0Selects which region the next RBAR/RASR access targets. Write before reading or writing region descriptors.
MPU_RNR — region number selector. Index for the indirect access pattern.
MPU_RBAR
32-bit · reset 0x0000_0000
28
24
20
16
12
8
4
0
b24
b16
b8
b0
BitsNameAccResetDescription
31:5ADDRRW0Region base address. Must be aligned to the region size (power of 2).
4VALIDWO0When 1, REGION field in this write overrides MPU_RNR. Lets you select + write base in a single MMIO.
3:0REGIONRW0Region number when VALID=1. Up to 16 regions.
MPU_RBAR (v7-M) — region base address. The alignment requirement means a 1 KB region must start on a 1 KB boundary, a 64 KB region on a 64 KB boundary, etc.
MPU_RASR
32-bit · reset 0x0000_0000
28
24
20
16
12
8
4
0
b24
b16
b8
b0
BitsNameAccResetDescription
31:29-RO0Reserved.
28XNRW0Execute-never. When 1, instruction fetches from this region fault.
27-RO0Reserved.
26:24APRW0Access permissions: 000=no access, 001=priv RW, 010=priv RW/unpriv RO, 011=full access, 101=priv RO, 110=priv RO/unpriv RO.
23:22-RO0Reserved.
21:19TEXRW0Type extension — selects cacheability/shareability scheme together with S/C/B.
18SRW0Shareable.
17CRW0Cacheable.
16BRW0Bufferable.
15:8SRDRW0Subregion disable — 8-bit mask. Each bit disables 1/8th of the region (only for sizes ≥256 B).
7:6-RO0Reserved.
5:1SIZERW0log2(size) - 1. SIZE=4 → 32 bytes, SIZE=7 → 256 bytes, SIZE=31 → 4 GB.
0ENABLERW0Region enable.
MPU_RASR (v7-M) — attributes and size. Dense register; the CMSIS macros help you build sane values.

Setting up a region on v7-M typically uses ARM_MPU_SetRegion:

// 32 KB of RAM at 0x20000000, full access, no execute, normal memory
ARM_MPU_SetRegion(
    ARM_MPU_RBAR(0, 0x20000000),                       // region 0, base
    ARM_MPU_RASR(1, ARM_MPU_AP_FULL, 0, 0, 1, 0, 0,    // XN=1, AP=full, normal mem
                 ARM_MPU_REGION_SIZE_32KB));

Region descriptors — ARMv8-M

ARMv8-M reworked the region descriptors to be simpler and less restrictive:

MPU_RBAR (v8-M)
32-bit · reset 0x0000_0000
28
24
20
16
12
8
4
0
b24
b16
b8
b0
BitsNameAccResetDescription
31:5BASERW0Region base address (32-byte aligned).
4:3SHRW0Shareability: 00=non-shareable, 10=outer shareable, 11=inner shareable.
2:1APRW000=priv RW/unpriv no, 01=priv RW/unpriv RW, 10=priv RO/unpriv no, 11=priv RO/unpriv RO.
0XNRW0Execute-never.
MPU_RBAR on ARMv8-M — base address and core permissions in one register.
MPU_RLAR (v8-M)
32-bit · reset 0x0000_0000
28
24
20
16
12
8
4
0
b24
b16
b8
b0
BitsNameAccResetDescription
31:5LIMITRW0Region limit address (inclusive, 32-byte aligned to 0x...0x1F).
4:1AttrIndxRW0Index into MAIR0/MAIR1 — selects the memory-attribute set.
0ENRW0Region enable.
MPU_RLAR on ARMv8-M — region limit and attribute index.

The v8-M MPU replaces v7-M's per-region cacheability bits with a Memory Attribute Indirection Register (MAIR0/MAIR1). You define up to 8 memory-attribute types in MAIR, then each region picks one by index. This separation makes it cheaper to change attributes for several regions consistently.

Crucially, v8-M drops the power-of-two-and-aligned constraint. A region can start at any 32-byte boundary and extend to any 32-byte limit. This is the single biggest win for software: you can carve a 13 KB region (not a power of two) without burning an extra region descriptor for the awkward bit.

Memory attributes — cacheability and shareability

The MPU also tells the cache hierarchy how to treat each region. The most common combinations on a Cortex-M7 system:

TypeUseTEX/C/B (v7-M)MAIR (v8-M)
Normal memory, write-back cacheableFlash, internal SRAMTEX=001, C=1, B=10xFF (inner+outer WB)
Normal, write-through cacheableRead-mostly dataTEX=000, C=1, B=00xAA
Normal, non-cacheableDMA-shared buffersTEX=001, C=0, B=00x44
Strongly orderedSystem control regsTEX=000, C=0, B=00x04 (device)
DevicePeripheralsTEX=000, C=0, B=10x04 (device)

The most common bug here is forgetting to mark DMA buffers as non-cacheable on Cortex-M7. The CPU writes data through the cache, but DMA writes go straight to SRAM — the cache holds stale copies. Either invalidate the cache after DMA completes, or mark the buffer region as non-cacheable from the start.

The two privilege levels

Cortex-M has exactly two privilege levels: privileged (handler mode + thread mode with CONTROL.nPRIV = 0) and unprivileged (thread mode with CONTROL.nPRIV = 1). Boot starts privileged. To drop privilege:

__set_CONTROL(__get_CONTROL() | CONTROL_nPRIV_Msk);
__ISB();  // ensure the change takes effect before the next instruction

Once unprivileged, the thread cannot:

  • Access privileged-only MPU regions (per AP bits)
  • Write to system registers like SCB->VTOR, NVIC_ISER, MPU registers
  • Execute CPS instructions to change PRIMASK/FAULTMASK
  • Access the SCB / NVIC / SysTick directly

The thread can come back to privileged only via an exception (SVCall or any IRQ — handler mode is always privileged). RTOSes use this to enforce that task code cannot diddle kernel data: tasks run unprivileged, syscalls escalate via SVC, IRQs are kernel-trusted.

Stack overflow detection

The headline MPU use-case in an RTOS is stack overflow detection. Each task's stack ends at a known address; place an MPU region just below the stack with "no access" attributes. If a function's stack frame grows beyond the boundary, the next push faults — MemManage — and the RTOS can pin the task or reset the system.

For a 4 KB stack starting at 0x20010000 and growing down:

// Region: 32 bytes at 0x2000FFE0 (just below the stack), no access
ARM_MPU_SetRegion(
    ARM_MPU_RBAR(0, 0x2000FFE0),
    ARM_MPU_RASR(1, ARM_MPU_AP_NONE, 0, 0, 0, 0, 0,
                 ARM_MPU_REGION_SIZE_32B));

When a task SP drops below 0x20010000, the next push hits the guard region and faults synchronously. The fault address is in MMFAR — log it, then vTaskDelete the offender. Without this, stack overflow corrupts the next region of RAM silently and you get bizarre, late-binding crashes that are nearly impossible to debug.

RTOS task isolation

Beyond stack overflow, the MPU lets you compartmentalize tasks. The typical pattern:

  1. At boot, set up static regions: flash as RO-execute, peripheral region as RW-no-execute, kernel data as priv-only RW.
  2. On context switch, swap in per-task regions that grant unprivileged R/W to the task's stack and any task-private data, plus shared regions for IPC.
  3. The kernel itself runs privileged and is not constrained by the task regions.

FreeRTOS MPU support is gated on the MPU port. Tasks declared with xTaskCreateRestricted get a per-task region list; switching to that task reprograms the MPU. Zephyr's USERSPACE feature is the same idea, more fully baked. ThreadX has "modules" with MPU-enforced boundaries.

The cost is a handful of MMIO writes per context switch — a few microseconds on a 100 MHz part. The benefit is that one buggy task cannot scribble over another task's stack, the kernel, or peripheral registers.

Common bugs

XN forgotten on RAM. Without XN=1, RAM is executable. A buffer overflow that overwrites a function pointer can then jump into attacker-controlled bytes. Set XN on every RAM region; only flash should be executable.

Default-deny background. If MPU_CTRL.PRIVDEFENA = 1, privileged code uses the architectural default memory map for any address not covered by a region. If = 0, even privileged code faults on uncovered regions. Pick deliberately; 1 is more permissive and easier to start with.

Region order matters. Higher-numbered regions take priority. A "deny" region at index 1 overridden by an "allow" region at index 4 means the address is allowed. Counter-intuitive; sort guard regions to the high end of the table.

Forgot the DSB+ISB. After programming the MPU and before relying on the new permissions, you need a memory barrier:

MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
__DSB();
__ISB();

Without these, the pipeline may have prefetched an instruction from a region whose permissions are about to change, and you'll get spurious faults.

Cross-references