Skip to main content

The Memory Protection Unit

A null-pointer write on a Cortex-M does not crash. Address 0x0000_0000 is real memory โ€” it is the start of flash, or the boot alias โ€” so *(uint32_t *)0 = 42 on a fresh chip silently does nothing at all, and the program carries on. A stack that overflows its intended region does not crash either; it grows down into .bss and corrupts variables that belong to something else, and the failure surfaces minutes later in code that is entirely innocent. Both are the same problem: on a bare Cortex-M, memory has no permissions, so a wrong access is indistinguishable from a right one.

The MPU is what changes that. It is a small block โ€” eight regions on this part โ€” that checks every load, store and instruction fetch against a table of address ranges and permissions, in parallel with the access itself, at no cycle cost. When an access does not have permission, the processor takes a MemManage fault at the instruction that made it, with the offending address in a register.

The mental model to keep separate from the start: an MPU is not an MMU. There is no address translation, no page table, no virtual memory, no swapping. Addresses go to the bus unchanged; the MPU's only power is to say "no" and raise a fault. Virtual Memory and Paging describes what the other thing does โ€” everything there about translation, page faults and demand paging is absent here.

Which means the value proposition is narrower than "protection" suggests, and better than most people assume: the MPU turns a class of silent corruption into a loud, precise, immediate fault. That is worth having in a single-task bare-metal firmware, before any question of an RTOS or untrusted code arises.

Prerequisites

The Cortex-M Memory Map covers the default memory map the MPU overrides, and the memory types (Normal, Device, Strongly-ordered) that its attribute fields select. Privilege Modes and the Two Stacks covers the privileged/unprivileged distinction that half the permission encodings depend on.

What the hardware checksโ€‹

Three things in that diagram are the ones that trip people up, so they are worth stating flatly:

  • Overlap is resolved by region number, highest wins. This is a feature, not a hazard: you define a large permissive background region at a low number and carve exceptions out of it at higher numbers.
  • A region-less address is a fault, unless PRIVDEFENA rescues it. With MPU_CTRL.PRIVDEFENA = 1, privileged code falls back to the default memory map for any address no region covers. With it clear, privileged code can only touch memory you explicitly described โ€” which is stricter, correct for a locked-down design, and the reason most first attempts at MPU configuration fault immediately.
  • A MemManage fault you never enabled arrives as a HardFault. SHCSR.MEMFAULTENA resets to 0, so out of the box every MPU violation escalates. See Exceptions and the Vector Table for the escalation rules; enabling the handler is one line and turns a generic HardFault into a fault with an address attached.

The registersโ€‹

Five registers at 0xE000_ED90, and a programming model that is deliberately indexed: you select a region in MPU_RNR, then write its base and its attributes.

RegisterOffsetPurpose
MPU_TYPER0x00Read-only. DREGION = number of regions implemented; 0 means no MPU.
MPU_CTRL0x04ENABLE, HFNMIENA, PRIVDEFENA.
MPU_RNR0x08Which region the next RBAR/RASR write configures.
MPU_RBAR0x0CRegion base address, plus a VALID+REGION shortcut that sets RNR in the same write.
MPU_RASR0x10Size, permissions, memory attributes, subregion disables, enable bit.

On the STM32F411, MPU_TYPER reads 0x0000_0800: DREGION = 8, IREGION = 0, SEPARATE = 0 โ€” eight unified regions covering both data and instruction accesses (PM0214 Rev 10 ยง4.2.5). Reading this register rather than assuming eight is the portable habit; Armv7-M allows 0, 8 or 16.

MPU_RASR is where the actual configuration lives.

BitsFieldMeaning
0ENABLERegion enable.
5:1SIZERegion size is 2^(SIZE+1) bytes. Minimum SIZE = 4 (32 bytes); maximum 31 (4 GB).
7:6โ€”Reserved.
15:8SRDSubregion disable โ€” one bit per eighth of the region. Only usable for regions of 256 bytes and above.
16BBufferable.
17CCacheable.
18SShareable.
21:19TEXType extension โ€” with C, B and S, selects the memory type.
23:22โ€”Reserved.
26:24APAccess permissions, below.
27โ€”Reserved.
28XNExecute Never. 1 forbids instruction fetches from the region.

Field definitions are PM0214 Rev 10 ยง4.2 (MPU_RASR) and Armv7-M ARM ยงB3.5.

Access permissions, from the same sources:

APPrivilegedUnprivilegedTypical use
000No accessNo accessA guard region. Any touch faults.
001Read/writeNo accessKernel data, RTOS control blocks.
010Read/writeRead-onlyShared state a task may observe but not modify.
011Read/writeRead/writeOrdinary RAM.
100โ€”โ€”Reserved, unpredictable.
101Read-onlyNo accessPrivileged constants.
110Read-onlyRead-onlyFlash, .rodata.
111Read-onlyRead-onlySame as 110.

Memory attributes are the TEX/C/B/S combinations. In practice four presets cover almost everything on a single-core MCU:

MemoryTEXCBSResulting type
Internal flash000100Normal, write-through cacheable
Internal SRAM000110Normal, write-back
Peripheral registers000011Device, shareable
Anything needing strict ordering00000โ€”Strongly-ordered

The Cortex-M Memory Map explains what those types actually promise about reordering and buffering. On a Cortex-M4 with no cache the C bit changes little in practice; on a Cortex-M7 it changes everything, which is why MPU configuration is a much bigger topic on that core.

The alignment rule, which is not optionalโ€‹

A region's base address must be aligned to its own size. A 1 KB region can start at 0x2000_0400 but not at 0x2000_0200. This falls out of the register format โ€” MPU_RBAR only stores the address bits above the region size โ€” and it is the single most annoying property of the v7-M MPU, because it means you cannot simply protect "this array" unless the linker put the array on a suitable boundary.

The consequences you plan around:

  • Region sizes are powers of two from 32 bytes up. There is no 3 KB region; you use 2 KB plus 1 KB, or a 4 KB region with two subregions disabled.
  • Anything you intend to protect must be placed, with a linker-script section or a compiler alignment attribute, not merely declared.
  • SRD softens this. An 8 KB region with the top three subregions disabled covers 5 KB โ€” still on an 8 KB-aligned base, but with a finer end boundary.
/* A stack guard: a 32-byte no-access region at the low end of the stack.
Region 7, so it overrides the permissive RAM region underneath it.
PM0214 Rev 10, section 4.2. */
extern uint32_t _stack_guard; /* linker: . = ALIGN(32); _stack_guard = .; . += 32; */

MPU->RNR = 7u;
MPU->RBAR = (uint32_t)&_stack_guard; /* 32-byte aligned */
MPU->RASR = (0u << MPU_RASR_AP_Pos) /* AP = 000, no access at all */
| (1u << MPU_RASR_XN_Pos)
| (4u << MPU_RASR_SIZE_Pos) /* 2^(4+1) = 32 bytes */
| MPU_RASR_ENABLE_Msk;

SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; /* real MemManage, not HardFault */
MPU->CTRL = MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;
__DSB();
__ISB();

That is the highest-value MPU configuration in bare-metal firmware and it is about fifteen lines: one region, one linker symbol, and stack overflow becomes a MemManage fault at the pushing instruction, with MMFAR pointing into the guard, instead of corrupted globals discovered later. The DSB/ISB pair matters โ€” the barriers ensure the new configuration is in force before the next instruction is fetched (Armv7-M ARM ยงB3.5.3).

Reading the faultโ€‹

When a violation happens, CFSR's low byte โ€” the MemManage Fault Status Register โ€” says what kind:

BitNameMeaning
0IACCVIOLInstruction fetch from a region with XN, or with no execute permission.
1DACCVIOLLoad or store violated the region's AP.
3MUNSTKERRThe fault happened while unstacking on exception return.
4MSTKERRThe fault happened while stacking on exception entry โ€” the classic stack-guard hit.
5MLSPERRFault during lazy floating-point state preservation.
7MMARVALIDMMFAR holds a valid faulting address.

MSTKERR with MMARVALID set and MMFAR inside your guard region is a stack overflow, stated as precisely as hardware can state anything. Note that MMARVALID is not always set โ€” an instruction-fetch violation often has nothing useful to put in MMFAR โ€” so check the bit before believing the address.

Armv8-M is a different MPUโ€‹

The Cortex-M23, M33 and M55 have an MPU with the same name and a different programming model: regions are described by a base and a limit address rather than a base and a power-of-two size, alignment is to 32 bytes regardless of length, memory attributes are indirected through MAIR0/MAIR1 instead of encoded per region, and there are no subregions. The alignment rule that dominates this page simply does not exist there. Configuration code does not port; the concepts โ€” regions, permissions, XN, background map, MemManage โ€” do.

The three configurations that turn the MPU into a brick

Enabling the MPU with PRIVDEFENA clear and no region covering your code. The instant MPU_CTRL.ENABLE is written, every address outside a defined region is off-limits โ€” including the flash the next instruction is being fetched from and the stack the fault handler needs. The result is an immediate MemManage that escalates to HardFault, whose handler cannot stack a frame, which is lockup. The board is dead in the instruction after the enable, and single-stepping through it looks like the write itself crashed the chip. Set PRIVDEFENA while you are learning, and only take it away once every region you need is defined and tested.

A misaligned base address, which does not fault โ€” it moves your region. MPU_RBAR ignores the address bits below the region size. Configure a 4 KB region at 0x2000_0800 and the hardware stores 0x2000_0000: your region is 2 KB earlier than you asked for, protecting something you never intended and leaving the real target unprotected. There is no error. Verify by reading MPU_RBAR back and comparing โ€” the same two-line assertion that catches a misaligned VTOR.

Forgetting the barriers after reconfiguration. The MPU is consulted by the memory system and by the instruction fetch path; without a DSB after the last register write and an ISB before relying on it, an access already in flight โ€” or an instruction already fetched โ€” can be checked against the previous configuration. The symptom is a fault that happens once, on the first run after a reconfiguration, and never reproduces when you step through it.

And one that is not a configuration error but a design one: an MPU region does not protect against DMA. The MPU sits in the processor's access path only. A DMA controller is a separate bus master and never consults it, so a rogue DMA descriptor will happily write into your guard region, your stack, or your vector table with no fault at all. Protecting against that requires the vendor's bus-level facilities, if the part has any โ€” it is not something the MPU can do.

See alsoโ€‹

Referencesโ€‹

  • STMicroelectronics โ€” PM0214, STM32 Cortex-M4 MCUs and MPUs programming manual, consulted at Rev 10 (March 2020). ยง4.2 "Memory protection unit (MPU)" for the whole register set โ€” MPU_TYPER (and ยง4.2.5's 0x0000 0800 reset value giving DREGION = 8 on this part), MPU_CTRL with HFNMIENA and PRIVDEFENA, MPU_RNR, MPU_RBAR and MPU_RASR field definitions, the access-permission encodings, the subregion rules, the update procedure and the design hints; ยง4.4 for SHCSR.MEMFAULTENA, CFSR's MemManage byte and MMFAR.
  • Arm โ€” Armv7-M Architecture Reference Manual, consulted at DDI 0403E.e (ID021621). ยงB3.5 "Protected Memory System Architecture, PMSAv7" is the normative definition: region matching and the highest-numbered-region rule, the DefaultPermissions() background map, the alignment requirement implied by the MPU_RBAR format, the TEX/C/B/S memory-type table, and ยงB3.5.3 on the barriers required around an MPU update. Also the rule that System space is always XN and an enabled MPU cannot change it.
  • Arm โ€” Armv8-M Architecture Reference Manual (DDI 0553). Consulted only to characterise the difference: the v8-M PMSA uses MPU_RBAR/MPU_RLAR base-and-limit pairs with 32-byte granularity and indirect attributes via MPU_MAIR0/MPU_MAIR1. Relevant if you expect this page's configuration code to move to a Cortex-M33; it does not.
  • Arm โ€” CMSIS-Core(M), core_cm4.h and mpu_armv7.h. MPU_Type, the MPU_RASR_*_Pos/_Msk macros used above, and the ARM_MPU_RASR() and ARM_MPU_SetRegion() helpers, which are worth preferring to hand-assembled register values precisely because the size and alignment arithmetic is where the mistakes are.