Post

Introduction to Memory Protection Unit

Introduction to Memory Protection Unit

MPU is an optional component for the memory protection. The MPU must be programmed and enabled before using it. If the MPU is not enabled, there is no change in the memory system behavior. And this series describe how to manage MPU in these STM32 products. Now, let’s dive into the very first section.


Overview

The MPU can be used to make the system more robust and secure by:

  • prohibiting the user applications from corrupting data used by critical tasks (such as the operating system kernel)
  • defining the SRAM memory region as a non-executable (execute never XN) to prevent code injection attacks
  • changing the memory access attributes

The MPU can be used to protect up to 16 memory regions. In Armv6 and Armv7 architect (Cortex-M0+, M3, M4 and M7), these regions in turn can have eights subregions, if the region is at least 256 bytes. The exact amount of regions protected can vary between core and devices. The sub-regions are always of equal size, and can be enabled or disabled by a sub-regions number. Because the minimum region size is driven by the cache line length (32 bytes), eight subregions of 32 bytes correspond to a 256-byte size. The regions are numbered 0 to 15. In addition, there is a region called the default region with an id of -1. All the 0-15 memory regions take priority over the default region. The regions can overlap, and can be nested. The region 15 has the highest priority and the region 0 has the lowest one and this governs how overlapping the regions behave. The priorities are fixed, and cannot be changed. In Armv8 architecture (Cortex-M33 and M55) the regions are defined using a base and a limit address offering flexibility and simplicity to the developer on the way to organize them. Additionally, Cortex-M33 and M55 do not include subregions as the region size is now more flexible. The figure below shows an example with six regions. This example shows the region 4 overlapping the regions 0 and 1. The region 5 is enclosed completely within the region 3. Since the priority is in an ascending order, the overlap regions (in orange) have the priority. So, if the region 0 is writeable and the region 4 is not, an address falling in the overlap between 0 and 4 is not writeable.

mpu

What is MPU

The MPU is an optional piece of hardware built into the Cortex-M core itself (present on Cortex-M7 and on some Cortex-M4/M33 variants). It sits between the core and the bus matrix, watching every instruction fetch and data access the CPU makes. For each transaction it checks the target address against the regions that have been programmed, and:

  • Blocks accesses to memory that hasn’t been explicitly allocated to the current context, turning a silent, hard-to-debug memory corruption into an immediate, catchable fault.
  • Enforces privilege separation, so unprivileged (user) code can be denied access to regions reserved for privileged (kernel/OS) code.
  • Applies the access attributes (read/write permissions, executable or not, cacheable/bufferable or not) that were assigned to each region.
  • Raises a MemManage fault as soon as a violating access is attempted, rather than letting it silently corrupt data or execute unintended code.

In short, the MPU doesn’t make memory faster — it makes the system safer, by turning “this shouldn’t happen” into “this can’t happen without the CPU knowing about it.”

Reasons to Use MPU

Speculative Access

Modern Cortex-M7 cores don’t strictly execute one instruction at a time and wait for each memory access to resolve — they speculate, trying to stay ahead of the program so the pipeline is never left waiting on memory. This shows up in a few forms:

  • Speculative reads: the core can issue a read for data it predicts the program will need soon, before the instruction that actually uses it has been reached.
  • Instruction prefetch and branch prediction: the core fetches instructions ahead of the current execution point, and on a branch it predicts which path (taken or not-taken) is more likely, filling the pipeline with that path’s instructions ahead of time.
  • Cache linefills: because the cache always loads a full line on a miss (not just the requested word), a single access can end up pulling in neighboring addresses the program never explicitly asked for.

This is great for performance — it hides memory latency and keeps the pipeline full — but it has a side effect: the core may touch memory addresses that the program logic never actually reaches, or that don’t behave like normal RAM (a peripheral register that clears-on-read, for instance). Left unconstrained, speculative access can therefore:

  • Trigger reads on memory-mapped peripherals as a side effect of a mispredicted branch or an aggressive prefetch, even though the program never intended to access that peripheral.
  • Undermine cache coherency, since speculatively-filled cache lines can hold data that a DMA transfer elsewhere has already made stale.
  • Cross into memory that isn’t safe to access at all (unmapped regions, regions with side effects on read).

This is exactly the kind of behavior the MPU is used to contain: by marking a region as non-cacheable, non-bufferable, or device/strongly-ordered memory, the core is told not to speculate into it, so accesses only happen when the program explicitly performs them.

DMA Issues

As covered in the cache coherency discussion earlier in this series, a DMA controller and the CPU’s DCACHE can end up with two different views of the same SRAM: the CPU may have modified data that only lives in its cache, or the DMA may have written fresh data to SRAM that the CPU still has a stale copy of in cache. Cache maintenance (clean/invalidate) is one way to resolve this on a per-transfer basis. The MPU offers a complementary, structural fix: buffers that are shared with DMA can be placed in a region marked non-cacheable, so the CPU always reads and writes that buffer directly from SRAM. This removes the coherency problem for that memory entirely, at the cost of losing the cache’s speed benefit for accesses to that specific region — a worthwhile trade for buffers that are DMA targets anyway.

Task Management

In an RTOS, the MPU can be configured per-task so that each task only has visibility into its own stack, data, and any resources it’s explicitly granted — everything else faults. This is useful for:

  • Containing a misbehaving or buggy task so a stray pointer write can’t silently corrupt another task’s memory or the kernel’s own data structures.
  • Enforcing that only trusted (privileged) tasks can touch critical peripherals or kernel structures, while ordinary tasks run unprivileged.
  • Making failures visible early: a task that oversteps its bounds raises a fault immediately, instead of producing a hard-to-trace bug much later.

This turns the MPU into a lightweight process-isolation mechanism, similar in spirit to what an MMU provides on larger systems, just scoped to fixed regions rather than full virtual memory.

Summary

Across all three reasons above, the pattern is the same: the MPU takes something that used to be an implicit assumption about how memory is used, and turns it into a rule the hardware itself enforces. Speculative access, DMA-shared buffers, and RTOS tasks each introduce a way for the core to touch memory that the programmer didn’t explicitly intend at that exact moment, and the MPU is the mechanism that keeps those implicit touches from becoming silent corruption.

Now that the “why” is covered, the next post in this series moves on to the “how”: walking through the MPU registers on STM32 Cortex-M7/M4 devices and configuring regions, attributes, and priorities in practice.


References

This post is licensed under CC BY 4.0 by the author.