
Zephyr RTOS provides a Memory Protection Unit (MPU) to enforce memory access controls, enhancing system reliability by preventing tasks from accessing unauthorized memory regions. Proper MPU configuration is essential for safety-critical embedded systems where memory corruption can lead to catastrophic failures. This article explains how to configure the Zephyr MPU, defines memory regions with appropriate permissions, and provides working code examples to protect critical system memory.
The Zephyr MPU abstraction layer provides a unified interface to configure hardware MPUs across different architectures (ARM Cortex-M, RISC-V, x86). The MPU divides the memory address space into regions, each with configurable base address, size, and access permissions (read, write, execute for privileged and unprivileged modes).
Each MPU region requires:
Zephyr supports up to 8 MPU regions by default, configurable via CONFIG_MPU_REGION_NUM. Regions are configured in priority order (region 0 highest priority), with overlapping regions requiring careful priority management.
Consider a scenario where multiple threads share memory resources without protection. A buggy thread could accidentally overwrite critical kernel data structures, stack of another thread, or read-only code sections, leading to unpredictable behavior, security vulnerabilities, or system crashes.
Without MPU protection:
First, enable MPU support in your Zephyr configuration:
CONFIG_MPU=yCONFIG_MPU_REQUIRES_NONOVERLAPPING_REGIONS=y
Define your memory regions based on your application’s memory layout. Typical regions include:
Use the z_arm_mpu_configure_region() function (ARM Cortex-M) or architecture-specific equivalent to configure each region. The function takes region index, base address, size, and attribute flags.
Configure threads to run in privileged or unprivileged mode via thread creation options:
k_thread_create(&thread_data, thread_stack, K_THREAD_STACK_SIZEOF(thread_stack),thread_func, NULL, NULL, NULL,K_PRIO_COOP(7), 0, K_NO_WAIT);
To create an unprivileged thread, add K_USER to the priority:
K_PRIO_COOP(7) | K_USER
Finally, enable the MPU with z_arm_mpu_enable().
Here’s a complete example configuring the MPU for an STM32F407Zephyr application:
#include <zephyr.h>#include <arm/mpu.h>/* MPU configuration stack */static struct arm_mpu_region mpu_regions[] = {/* Region 0: Flash (code) - read only, privileged only */MPU_REGION_ENTRY("FLASH_0",0x08000000,REGION_FLASH_SIZE,REGION_RO_EXECUTE_PRIV),/* Region 1: SRAM - read/write, privileged and unprivileged */MPU_REGION_ENTRY("SRAM_0",0x20000000,REGION_SRAM_SIZE,REGION_RW_DATA_PRIV_U),/* Region 2: Peripherals - read/write, privileged only */MPU_REGION_ENTRY("PERIPHERALS",0x40000000,REGION_PERIPH_SIZE,REGION_RW_DEVICE_PRIV),/* Region 3: Background region - no access */MPU_REGION_ENTRY_BACKGROUND(REGION_NO_ACCESS)};void configure_mpu(void){/* Disable MPU during configuration */z_arm_mpu_disable();/* Configure MPU regions */for (int i = 0; i < ARRAY_SIZE(mpu_regions); i++) {z_arm_mpu_configure_region(&mpu_regions[i], i);}/* Enable MPU */z_arm_mpu_enable();}void main(void){configure_mpu();/* Create unprivileged thread */k_thread_create(&unpriv_thread_data, unpriv_thread_stack,K_THREAD_STACK_SIZEOF(unpriv_thread_stack),unpriv_thread_func, NULL, NULL, NULL,K_PRIO_COOP(7) | K_USER, 0, K_NO_WAIT);/* Start scheduler */k_start();}
The MPU_REGION_ENTRY macros use predefined attribute constants:
REGION_RO_EXECUTE_PRIV: Read-only, execute-only, privileged access onlyREGION_RW_DATA_PRIV_U: Read-write data, accessible by both privileged and unprivilegedREGION_RW_DEVICE_PRIV: Read-write device memory, privileged only (strongly ordered)REGION_NO_ACCESS: No access (triggers fault on any access)To verify your MPU configuration works correctly:
void unpriv_thread_func(void *p1, void *p2, void *p3){ARG_UNUSED(p1);ARG_UNUSED(p2);ARG_UNUSED(p3);/* Attempt to write to flash (should trigger MPU fault) */volatile uint32_t *flash_ptr = (uint32_t *)0x08000000;*flash_ptr = 0xDEADBEEF; /* This should cause a fault *//* If we reach here, MPU is not working correctly */printk("MPU test failed: write to flash succeeded\n");}
Configure a memory management fault handler to catch and log the fault:
void MemManage_Handler(void){printk("Memory management fault occurred!\n");/* Extract fault status registers for diagnosis *//* ... */while (1) {/* Halt or perform recovery */}}
The following diagram illustrates typical MPU region configuration for a Zephyr application:
+--------------------------------------------------------------+| 0x08000000 - 0x080FFFFF || FLASH (Code Region) || Attributes: RO, Execute, Privileged Only |+--------------------------------------------------------------+| 0x20000000 - 0x2001FFFF || SRAM (Data Region) || Attributes: RW, Privileged & Unprivileged |+--------------------------------------------------------------+| 0x40000000 - 0x400FFFFF || PERIPHERALS (Memory-Mapped I/O) || Attributes: RW, Privileged Only, Strongly Ordered |+--------------------------------------------------------------+| 0x60000000 - 0x9FFFFFFF || BACKGROUND REGION (Default) || Attributes: NO ACCESS (Triggers Fault on Any Access) |+--------------------------------------------------------------+
For complex systems, basic MPU configuration may not be sufficient. Consider these advanced techniques:
In some applications, you may need to change MPU settings at runtime, for example, to temporarily grant access to a memory region for a specific operation. Zephyr allows dynamic reconfiguration of MPU regions, but care must be taken to ensure system stability.
To reconfigure an MPU region at runtime:
Note: Disabling the MPU leaves the system unprotected during the reconfiguration window. Ensure that no memory access violations can occur during this critical section.
Zephyr’s memory domain API allows grouping memory partitions and assigning them to threads. When used with MPU, memory domains can provide an additional layer of flexibility.
However, note that memory domains are primarily for user mode threads and do not replace the need for proper MPU configuration for system memory.
Example of defining a memory domain and assigning a partition:
When configuring MPU regions, it’s important to consider the cache attributes. Incorrect cache settings can lead to stale data or unexpected behavior.
For example, marking a region as cacheable when it should be device memory (non-cacheable) can cause issues with memory-mapped peripherals.
Always refer to the processor’s documentation for the correct cache and buffer settings for each memory region.
For complex systems, basic MPU configuration may not be sufficient. Consider these advanced techniques:
In some applications, you may need to change MPU settings at runtime, for example, to temporarily grant access to a memory region for a specific operation. Zephyr allows dynamic reconfiguration of MPU regions, but care must be taken to ensure system stability.
To reconfigure an MPU region at runtime:
void reconfigure_mpu_region(int region_index, uint32_t base_addr, uint32_t size, uint32_t attr){/* Disable MPU during reconfiguration */z_arm_mpu_disable();/* Update the region configuration */struct arm_mpu_region region;region.base = base_addr;region.size = size;region.attr = attr;z_arm_mpu_configure_region(®ion, region_index);/* Re-enable MPU */z_arm_mpu_enable();}
Note: Disabling the MPU leaves the system unprotected during the reconfiguration window. Ensure that no memory access violations can occur during this critical section.
Zephyr’s memory domain API allows grouping memory partitions and assigning them to threads. When used with MPU, memory domains can provide an additional layer of flexibility.
However, note that memory domains are primarily for user mode threads and do not replace the need for proper MPU configuration for system memory.
Example of defining a memory domain and assigning a partition:
/* Define a memory partition for a custom buffer */K_APP_DMEM(custom_buffer) = {0};/* Initialize the memory domain */k_mem_domain_init(&my_domain, 1, &custom_buffer_partition);/* Assign the domain to a thread */k_thread_mem_domain_add(&my_thread_data, &my_domain);
When configuring MPU regions, it’s important to consider the cache attributes. Incorrect cache settings can lead to stale data or unexpected behavior.
For example, marking a region as cacheable when it should be device memory (non-cacheable) can cause issues with memory-mapped peripherals.
Always refer to the processor’s documentation for the correct cache and buffer settings for each memory region.
Configuring the Zephyr Memory Protection Unit (MPU) is a critical step in building robust and secure embedded systems. By defining memory regions with appropriate access permissions, you can:
The Zephyr MPU abstraction simplifies configuration across different architectures while providing fine-grained control over memory access permissions. Follow the step-by-step guide and code examples presented here to integrate MPU protection into your Zephyr-based applications, significantly enhancing system reliability and security.
Quick Links
Legal Stuff





