STM32 ยท MPU ยท Cortex-M ยท Memory Protection ยท Embedded ยท Firmware

STM32 MPU Configuration:
Protecting Memory Regions in Production Firmware

2026-06-23 ยท Davide Carrese

Nothing worse than a firmware crash caused by a stack overflow silently corrupting a critical configuration structure โ€” or a rogue pointer in a DMA callback overwriting the task control block of a running FreeRTOS task. The Cortex-M Memory Protection Unit (MPU) exists precisely to catch these violations at the moment they happen, before data corruption spreads.

On most STM32 projects, the MPU sits disabled by default. Many developers treat it as an optional safety feature for high-integrity or automotive code. In reality, any Cortex-M3/M4/M7/M33 part from STM32 can run the MPU, and configuring it takes fewer than twenty lines of C once you understand the register model. This article walks through region configuration, subregion masking, privilege separation, and the gotchas I have encountered on client projects.

The MPU Model in One Paragraph

The Cortex-M MPU divides the 4 GB address space into up to eight (or sixteen on Cortex-M33/M55) independently configurable regions. Each region has a base address, a size from 32 bytes to 4 GB, and a set of access permissions: read/write/execute for privileged versus unprivileged software. When code running in unprivileged mode accesses an address not covered by any enabled region, or violates the region's permissions, the MPU raises a MemManage fault โ€” caught by MemManage_Handler in your vector table.

That is the entire abstraction. There are no page tables, no TLB maintenance, and no OS dependency. The MPU is a pure hardware gatekeeper that operates on physical addresses.

Region Configuration โ€” Register by Register

The MPU control registers live in the System Control Block (SCB) address space, accessed through the CMSIS-Core macros or by direct pointer dereference.

Step 1 โ€” Enable the MPU and define the background region.

The MPU_CTRL register (0xE000ED94) has three relevant bits:

/* Enable MPU, no default privileged region, MPU active in HardFault/NMI */
MPU->CTRL = MPU_CTRL_ENABLE_Msk;
/* OR: Enable with default privileged region */
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;

Step 2 โ€” Configure a region with RNR, RBAR, and RASR.

Each region is programmed through three memory-mapped registers:

void MPU_ConfigRegion(uint8_t region, uint32_t base, uint32_t size, uint32_t rasr) {
    /* Ensure MPU is disabled while programming regions */
    MPU->CTRL = 0;

    /* Select region */
    MPU->RNR  = region;
    MPU->RBAR = base | MPU_RBAR_VALID_Msk | (region << MPU_RBAR_REGION_Pos);
    MPU->RASR = rasr;

    /* Re-enable */
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
    __DSB();
    __ISB();
}

The size parameter in RASR is encoded as Size = log2(region_size) - 1. A 32 KB region gets Size = 14 (2^(14+1) = 32768).

Encoding the RASR Attribute Field

The RASR layout is the most error-prone part of the configuration. Here is the field breakdown:

RASR bits:
 [31:29] TEX  โ€” Type Extension, combined with C/B/S for cache behaviour
 [28]    S    โ€” Shareable
 [27]    C    โ€” Cacheable
 [26]    B    โ€” Bufferable
 [24]    SRD  โ€” SubRegion Disable (8 bits, one per subregion)
 [23:16] Reserved
 [15:8]  SRD  โ€” actually bits [15:8] are for the legacy mapping
 [7:6]   Reserved
 [5:3]   AP   โ€” Access Permission
 [2]     Reserved
 [1]     XN   โ€” Execute Never
 [0]     EN   โ€” Region Enable

The key combinations for typical embedded memory types:

Memory typeTEXCBSRASR mask
Flash (Normal, WT)01000x03000000
SRAM (Normal, WBWA)11110x07000000
Peripherals (Device)00010x01000000
Strongly-Ordered00000x00000000

The AP field (bits [5:3]) controls who can do what:

APPrivilegedUnprivileged
000No accessNo access
001RWNo access
010RWRO
011RWRW
101RONo access
110RORO

Practical Example: Guarding SRAM With Three Regions

Consider a typical FreeRTOS project on STM32G474. The SRAM layout is:

#define MPU_REGION_FLASH     0
#define MPU_REGION_SRAM_DATA 1
#define MPU_REGION_SRAM_RTOS 2
#define MPU_REGION_SRAM_TASK 3
#define MPU_REGION_PERIPH    4

void MPU_Init_Production(void) {
    /* Disable during config */
    MPU->CTRL = 0;
    __DSB(); __ISB();

    /* Region 0: Flash (512 KB) โ€” privileged RO, unprivileged RO, XN cleared */
    MPU->RNR  = MPU_REGION_FLASH;
    MPU->RBAR = 0x08000000 | MPU_RBAR_VALID_Msk | (MPU_REGION_FLASH << MPU_RBAR_REGION_Pos);
    MPU->RASR = (18 << 1)              /* SIZE: log2(512KB)-1 = 18 */
              | (0x03 << 3)             /* AP: RW/RW */
              | (0x01 << 5)             /* TEX=0, C=1 (Write-Through) */
              | (1 << MPU_RASR_C_Pos)   /* Cacheable */
              | (1 << MPU_RASR_ENABLE_Pos);

    /* Region 1: .data + .bss (32 KB) โ€” privileged RW, unprivileged no access */
    MPU->RNR  = MPU_REGION_SRAM_DATA;
    MPU->RBAR = 0x20000000 | MPU_RBAR_VALID_Msk | (MPU_REGION_SRAM_DATA << MPU_RBAR_REGION_Pos);
    MPU->RASR = (14 << 1)              /* SIZE: log2(32KB)-1 = 14 */
              | (0x01 << 3)             /* AP: privileged RW, unprivileged no access */
              | (1 << MPU_RASR_TEX_Pos) /* TEX=1, C=1, B=1, S=1 (WBWA) */
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos);

    /* Region 2: RTOS kernel structures (16 KB) โ€” privileged RW only */
    MPU->RNR  = MPU_REGION_SRAM_RTOS;
    MPU->RBAR = 0x20008000 | MPU_RBAR_VALID_Msk | (MPU_REGION_SRAM_RTOS << MPU_RBAR_REGION_Pos);
    MPU->RASR = (13 << 1)              /* SIZE: log2(16KB)-1 = 13 */
              | (0x01 << 3)             /* AP: privileged RW only */
              | (1 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos);

    /* Region 3: Task stacks (8 KB) โ€” unprivileged RW, XN enabled */
    MPU->RNR  = MPU_REGION_SRAM_TASK;
    MPU->RBAR = 0x2000C000 | MPU_RBAR_VALID_Msk | (MPU_REGION_SRAM_TASK << MPU_RBAR_REGION_Pos);
    MPU->RASR = (12 << 1)              /* SIZE: log2(8KB)-1 = 12 */
              | (0x03 << 3)             /* AP: RW/RW */
              | (1 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos)
              | (1 << 0);              /* XN โ€” no code execution from stack */

    /* Region 4: Peripherals (0x40000000โ€“0x5FFFFFFF) โ€” privileged RW, Device, XN */
    MPU->RNR  = MPU_REGION_PERIPH;
    MPU->RBAR = 0x40000000 | MPU_RBAR_VALID_Msk | (MPU_REGION_PERIPH << MPU_RBAR_REGION_Pos);
    MPU->RASR = (29 << 1)              /* SIZE: log2(512MB)-1 = 29 */
              | (0x01 << 3)             /* AP: privileged RW only */
              | (0 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_S_Pos)  /* Device, Shareable */
              | (1 << MPU_RASR_ENABLE_Pos)
              | (1 << 0);              /* XN */

    /* Enable MPU with privileged background region */
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
    __DSB(); __ISB();
}

With this configuration, an unprivileged task that accidentally writes through a wild pointer to 0x20000004 (in the .data region) immediately triggers MemManage_Handler. The same task trying to execute code from its own stack region is also caught โ€” the XN bit prevents execution from the stack area.

Subregion Disable โ€” Finer Granularity Without Extra Regions

Each MPU region is divided into eight equal subregions. By setting bits in the SubRegion Disable (SRD) field of RASR, you can carve out holes within a region without consuming another region slot. This is invaluable when the linker places a read-only data table inside a writable .bss area, or when a memory-mapped FPGA region overlaps with a peripheral aperture.

/* Example: 16 KB region at 0x20000000, disable subregions 4 and 5
   (bytes 0x20008000โ€“0x2000BFFF) so they become no-access */
uint32_t srd_mask = (1 << 4) | (1 << 5);   /* subregions 4 and 5 disabled */
uint32_t rasr = (13 << 1)                    /* SIZE = 16 KB */
              | (0x01 << 3)                   /* AP: privileged RW */
              | (srd_mask << 8)               /* SRD field at bits [15:8] */
              | (1 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos);

Each subregion covers region_size / 8 bytes. For a 16 KB region, each subregion is 2 KB.

Practical Checklist

  1. Set PRIVDEFENA = 1 during development โ€” the background privileged region avoids surprise faults from library code that was not written with MPU awareness.
  2. Always pair region writes with __DSB() and __ISB() โ€” the MPU configuration registers are buffered writes. Without a barrier, the next instruction may execute with stale region state.
  3. Disable the MPU before changing regions โ€” writing to RASR while the MPU is enabled produces UNPREDICTABLE behaviour on Cortex-M3/M4. Disable, configure, re-enable.
  4. Set XN on SRAM regions unless you explicitly need to execute code from RAM (which is rare and usually a sign of a bootloader or self-modifying pattern that should be an explicit exception).
  5. Verify with a deliberate fault โ€” write a short test that triggers a MemManage fault and confirm MemManage_Handler is called before trusting the configuration in the field.
  6. Remember the MPU resets to disabled โ€” any code that runs before MPU_Init_Production() (startup code, SystemInit, board-level init) executes without protection. If you need coverage from the first instruction, configure the MPU at the very top of Reset_Handler.
  7. Watch out for MPU regions on Cortex-M7 with D-Cache โ€” regions covering the same physical memory must share the same cache attributes (TEX/C/B/S) or the cache may exhibit coherency issues. This is a common source of "works without MPU, fails with MPU" bugs on STM32H7.

How I Would Approach This on a Client Project

On a recent STM32H723 industrial controller project, I divided the MPU regions into three categories: core-kernel (privileged RW only), application-data (privileged RW, unprivileged RO), and stack (unprivileged RW, XN). The linker script was adjusted to place each category at a known base address with an alignment equal to the region size โ€” this is critical because the MPU enforces alignment of base % size == 0.

The biggest time-saver was keeping PRIVDEFENA enabled during development. It let peripheral HAL libraries (which typically run in privileged mode) access the entire memory map without explicit region coverage, while catching precisely the application-level violations I cared about. Only at production release did I tighten the regions to cover every memory type explicitly โ€” including a dedicated Device region for the peripheral bus.

I also added a #define MPU_ENABLE 1 compile-time switch so the same binary can be debugged on a bench unit (MPU disabled) and run in production (MPU enabled) without changing source code. The only difference is whether the MemManage fault handler halts or performs a safe reset.

Sources

๐Ÿ“ฌ Comments / discussion

Prefer email: [email protected] โ€” include the article URL so I can follow up. For corrections or deeper questions, I typically reply within 48 hours.