Skip to main content

Memory protection unit

The MPU assigns access permissions and memory-type attributes to address ranges. It is configured before the caches are enabled, because the caches obey the attributes it publishes.

MPU_Config

/**
* @brief Configure the MPU attributes as Write Through for SDRAM.
* @note The Base Address is SDRAM_DEVICE_ADDR.
* The Region Size is 32MB.
* @param None
* @retval None
*/
static void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct = {0};

/* Disable the MPU */
HAL_MPU_Disable();

/* Configure the MPU as Strongly ordered for not defined regions */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x00;
MPU_InitStruct.Size = MPU_REGION_SIZE_4GB;
MPU_InitStruct.AccessPermission = MPU_REGION_NO_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x87;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* Configure the MPU attributes as WT for SDRAM */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = SDRAM_DEVICE_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* Configure the MPU QSPI flash */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x90000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_128MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER2;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x0;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* Enable the MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

Zero-initialise the struct. MPU_Region_InitTypeDef MPU_InitStruct = {0}; is not optional housekeeping. The struct is a local, so without the initialiser its members hold whatever was on the stack, and HAL_MPU_ConfigRegion writes every member into the hardware. It happens to work here only because each field is assigned before the first call — add a field in a later HAL version, or reorder the code, and it silently misconfigures a region.

Disable before configuring. HAL_MPU_Disable() prevents the MPU from acting on half-written configuration. It also executes a DMB so outstanding memory transfers complete first.

Configure each region. The members map one-to-one onto the MPU registers:

MemberMeaning
EnableEnables this region
BaseAddressStart address. Must be aligned to the region size
SizeRegion size, as a MPU_REGION_SIZE_* constant
AccessPermissionPrivileged and unprivileged read/write rights
IsBufferableB bit — writes may be buffered
IsCacheableC bit — data may be cached
IsShareableS bit — coherency with other bus masters
NumberWhich of the eight (or sixteen) region slots to program
TypeExtFieldTEX bits; with C and B, selects the memory type
SubRegionDisableBitmask of subregions to exclude
DisableExecXN bit — forbid instruction fetches from the region

Region priority is by number, highest wins. Where regions overlap, the highest-numbered enabled region decides the attributes. This is what makes the pattern above work: region 0 is a deny-everything background covering the whole address space, and regions 1 and 2 punch permitted areas through it.

Enable the MPU. HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT) sets PRIVDEFENA, so privileged code falls back to the default system memory map wherever no region matches. Pass MPU_HFNMI_PRIVDEF_NONE instead to make every unmapped access fault, including privileged ones — stricter, and much noisier to bring up.

The net effect is that unauthorised or accidental accesses to critical memory raise a MemManage fault instead of silently corrupting state.

The CubeMX-generated version

Ticking Enable Memory Protection Unit (MPU) when creating a project produces a single-region version of the same function, with the fields in CubeMX's order rather than ST's example order:

/* MPU Configuration */

void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};

/* Disables the MPU */
HAL_MPU_Disable();

/** Initializes and configures the Region and the memory to be protected
*/
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x0;
MPU_InitStruct.Size = MPU_REGION_SIZE_4GB;
MPU_InitStruct.SubRegionDisable = 0x87; // 0b10000111
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_NO_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* Enables the MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

This is only the background region — the deny-everything catch-all. It protects nothing on its own beyond faulting on genuinely unmapped addresses, because there are no higher-numbered regions granting access to anything specific. It is a starting point, not a finished configuration: add the regions your board actually needs, as in the example above.

CubeMX regenerates this function, so put additions between the USER CODE markers or move the whole thing into a file CubeMX does not own.

Alignment and size constraints

Two rules that cause most first-attempt failures:

  • The base address must be a multiple of the region size. A 32 MB region cannot start at 0xC0001000.
  • The size is a power of two, at least 32 bytes.

HAL_MPU_ConfigRegion does not validate either, and returns void in older HAL versions. A misaligned region simply does not take effect where you expect.

SubRegionDisable set to 0x87

An MPU region of 256 bytes or more is divided into eight equal subregions. SubRegionDisable is an 8-bit mask in which a 1 excludes the corresponding subregion from the region — the region's attributes then do not apply there, and the next-highest matching region (or the background map) takes over.

0x87 is 1000 0111 in binary. Counting from bit 0, the least significant:

BitValueSubregion
01Disabled
11Disabled
21Disabled
30Enabled
40Enabled
50Enabled
60Enabled
71Disabled

So subregions 0, 1, 2 and 7 are excluded and 3, 4, 5 and 6 remain covered.

Each subregion is one eighth of the region. For the 4 GB background region that is 512 MB apiece, so the mask leaves the bottom 1.5 GB (0x00000000–0x5FFFFFFF) and the top 512 MB (0xE0000000–0xFFFFFFFF) outside the deny-everything rule. Those ranges hold the flash, SRAM and the private peripheral bus, which need the default memory map rather than strongly-ordered no-access attributes.

Subregions do not exist for regions smaller than 256 bytes; the field must be zero there.

TypeExtField set to MPU_TEX_LEVEL0

TEX does not select a memory type by itself. The type comes from TEX together with the C (cacheable) and B (bufferable) bits, so MPU_TEX_LEVEL0 means different things depending on how IsCacheable and IsBufferable are set:

TEXCBResulting memory type
000Strongly ordered
001Device, shareable
010Normal, write-through, no write allocate
011Normal, write-back, no write allocate
100Normal, non-cacheable
111Normal, write-back, write and read allocate
200Device, non-shareable

Reading the three regions in the code with that table:

  • Region 0 — TEX 0, C 0, B 0: strongly ordered. Every access is in program order, nothing is buffered or cached. Combined with MPU_REGION_NO_ACCESS this is a catch-all barrier.
  • Region 1 (SDRAM) — TEX 0, C 1, B 0: normal memory, write-through, which is what the function's doc comment promises. Write-through keeps SDRAM contents consistent with the D-Cache for other bus masters reading it, at the cost of write bandwidth.
  • Region 2 (QSPI flash) — TEX 0, C 1, B 0: also write-through. For memory-mapped read-only flash, caching reads is the point; the write policy is largely academic.

TEX level 0 is therefore the normal choice. Higher TEX levels exist to describe non-cacheable normal memory and to set separate inner and outer cache policies, which matters mainly when a second bus master or a cache-coherent interconnect is involved.

Size set to MPU_REGION_SIZE_4GB

The 4 GB figure has nothing to do with how much memory the part has. It covers the entire 32-bit address space, most of which is unimplemented on any STM32.

That is deliberate. Region 0 is a background region: deny everything everywhere, then allow specific ranges in higher-numbered regions, which take priority. An access to an address nobody explicitly mapped hits region 0 and faults, instead of reaching whatever the default memory map would have allowed.

The opposite approach — sizing each region to the physical memory behind it — applies to regions 1 and above, where the size should match the real device. Region 1 covers 32 MB because that is the SDRAM part fitted to the board; check the schematic and the reference manual rather than copying the constant.

Effects of enabling the MPU

  • Access control. Regions can be read-only, no-access, or writable only from privileged code. Violations raise a MemManage fault.
  • Protection against stray writes. Marking firmware, vector tables and configuration read-only turns a wild pointer into a fault at the point of the bug rather than corruption discovered much later.
  • Separation of code and data. DisableExec (XN) prevents instruction fetches from data regions, blocking whole classes of buffer-overflow exploitation.
  • Task isolation. Under an RTOS, reprogramming regions on context switch keeps one task from reaching another's memory. FreeRTOS-MPU and CMSIS-RTOS2 both support this.
  • Performance. The attribute check itself is not a bottleneck, but the cacheability and bufferability you assign very much are — a region wrongly marked non-cacheable can cost more than everything else here gains.
  • Fault handling. Install a MemManage_Handler. SCB->CFSR and SCB->MMFAR identify the faulting access; without a handler the fault escalates to HardFault and the original cause is much harder to recover.

Debugging a MemManage fault

The fault status registers hold everything needed, but only until the next fault overwrites them. Read them first:

void MemManage_Handler(void) {
uint32_t cfsr = SCB->CFSR; // bits [7:0] are the MemManage flags
uint32_t addr = SCB->MMFAR; // valid only when MMARVALID is set
(void)cfsr; (void)addr;
__BKPT(0); // halt here under a debugger
while (1) { }
}
  • MMARVALID (bit 7) — MMFAR holds the faulting address.
  • DACCVIOL (bit 1) — a data access was refused.
  • IACCVIOL (bit 0) — an instruction fetch was refused, typically an XN region or a jump to a bad pointer.

MMFAR gives the address; the instruction is in the stacked return address, which sits at offset 24 in the exception frame pointed at by MSP or PSP depending on which stack was in use.