Memory Management
Zephyr provides several memory management mechanisms suited to different embedded use cases. This chapter covers the memory allocation strategies available and when to use each one.
Overview
Unlike desktop operating systems, embedded systems have limited RAM and no virtual memory. Zephyr offers multiple allocation strategies, each with different tradeoffs:
| Mechanism | Allocation Size | Fragmentation | Deterministic | Thread-Safe |
|---|---|---|---|---|
| Stack variables | Fixed at compile time | None | Yes | Yes (per-thread) |
| k_malloc/k_free | Variable | Yes | No | Yes |
| k_heap | Variable (per-heap) | Yes | No | Yes |
| Memory Slabs | Fixed-size blocks | None | Yes | Yes |
| Memory Blocks | Variable (pool) | Reduced | Configurable | Yes |
Dynamic memory allocation in embedded systems should be used judiciously. Heap fragmentation can cause out-of-memory failures that are hard to debug. Prefer static allocation or fixed-size allocators when possible.
Code Relocation
By default, Zephyr places code in flash (XIP — execute in place) and data in RAM. You can relocate code or data to different memory regions for performance or hardware requirements.
Why Relocate Code?
- Performance — execute code from SRAM instead of flash for faster execution
- TCM (Tightly Coupled Memory) — place critical ISR handlers in fast memory
- External memory — place large data buffers in external SDRAM
Configuration
# prj.conf
CONFIG_CODE_DATA_RELOCATION=y
In your CMakeLists.txt:
# Relocate a source file to SRAM
zephyr_code_relocate(FILES src/critical_path.c LOCATION SRAM)
# Relocate data only
zephyr_code_relocate(FILES src/big_buffer.c LOCATION SRAM_DATA)
Linker Regions
Common memory regions available (board-dependent):
| Region | Typical Use |
|---|---|
| FLASH | Default code location (XIP) |
| SRAM | Fast execution, variables |
| CCM | Core-coupled memory (STM32) |
| DTCM/ITCM | Tightly coupled memory (Cortex-M7) |
| SDRAM | External large memory |
System Heap (k_malloc / k_free)
Zephyr provides a system heap for general-purpose dynamic allocation.
Configuration
# prj.conf
CONFIG_HEAP_MEM_POOL_SIZE=4096 # System heap size in bytes
Usage
#include <zephyr/kernel.h>
void example(void)
{
/* Allocate memory */
void *ptr = k_malloc(256);
if (ptr == NULL) {
/* Allocation failed — heap exhausted or fragmented */
return;
}
/* Use the memory */
memset(ptr, 0, 256);
/* Free when done */
k_free(ptr);
}
Aligned Allocation
/* Allocate with specific alignment */
void *aligned_ptr = k_aligned_alloc(16, 256); /* 16-byte aligned, 256 bytes */
Calloc
/* Allocate and zero-initialize */
void *ptr = k_calloc(10, sizeof(struct my_data)); /* 10 elements */
k_malloc and k_free use a system-wide heap. In long-running applications, repeated alloc/free cycles can fragment the heap, eventually causing allocation failures even when total free memory is sufficient.
Synchronized Heaps (k_heap)
For better control, use k_heap to create dedicated heaps for specific subsystems or tasks.
Why Use k_heap Over k_malloc?
- Isolation — each subsystem gets its own heap; one subsystem’s exhaustion doesn’t affect others
- Sizing — you can size each heap according to its needs
- Debugging — easier to track memory usage per subsystem
- Timeout support — can block waiting for memory to become available
Usage
#include <zephyr/kernel.h>
/* Define a 2KB heap */
K_HEAP_DEFINE(my_heap, 2048);
void example(void)
{
/* Allocate from the specific heap */
void *ptr = k_heap_alloc(&my_heap, 128, K_NO_WAIT);
if (ptr == NULL) {
return;
}
/* Use the memory */
/* Free back to the same heap */
k_heap_free(&my_heap, ptr);
}
Blocking Allocation
/* Wait up to 100ms for memory to become available */
void *ptr = k_heap_alloc(&my_heap, 128, K_MSEC(100));
if (ptr == NULL) {
/* Timed out — no memory available within 100ms */
}
/* Wait forever (careful — can deadlock!) */
void *ptr2 = k_heap_alloc(&my_heap, 64, K_FOREVER);
Aligned Heap Allocation
/* Allocate with alignment from a specific heap */
void *ptr = k_heap_aligned_alloc(&my_heap, 32, 256, K_NO_WAIT);
Memory Slabs
Memory slabs provide fixed-size block allocation — the fastest and most deterministic allocation method.
Why Memory Slabs?
- Zero fragmentation — all blocks are the same size
- O(1) allocation — constant time alloc and free
- Deterministic — guaranteed allocation time (important for real-time)
- Simple — no coalescing or splitting needed
Usage
#include <zephyr/kernel.h>
/* Define a slab: 16 blocks of 256 bytes each */
K_MEM_SLAB_DEFINE(my_slab, 256, 16, 4); /* block_size, num_blocks, align */
void example(void)
{
void *block;
/* Allocate a block */
int ret = k_mem_slab_alloc(&my_slab, &block, K_NO_WAIT);
if (ret != 0) {
/* No blocks available */
return;
}
/* Use the fixed-size block */
struct my_message *msg = (struct my_message *)block;
msg->type = MSG_SENSOR_DATA;
msg->value = 42;
/* Free the block */
k_mem_slab_free(&my_slab, block);
}
Checking Availability
/* How many blocks are currently used? */
uint32_t used = k_mem_slab_num_used_get(&my_slab);
/* How many free blocks remain? */
uint32_t free_blocks = k_mem_slab_num_free_get(&my_slab);
When to Use Memory Slabs
Memory slabs are ideal for:
- Message buffers — fixed-size messages between threads
- Network packet buffers — all packets same size
- DMA buffers — hardware needs fixed-size aligned buffers
- Real-time contexts — ISRs that need guaranteed allocation
Memory Blocks Allocator
The memory blocks allocator (sys_mem_blocks) provides a pool-based allocator with support for multi-block allocations.
Configuration
# prj.conf
CONFIG_SYS_MEM_BLOCKS=y
Usage
#include <zephyr/sys/mem_blocks.h>
/* Define: 64-byte blocks, 32 blocks total */
SYS_MEM_BLOCKS_DEFINE(my_blocks, 64, 32, 4);
void example(void)
{
void *blocks[4];
/* Allocate 4 contiguous blocks (256 bytes total) */
int ret = sys_mem_blocks_alloc_contiguous(&my_blocks, 4, blocks);
if (ret != 0) {
return;
}
/* Use the memory */
/* Free all 4 blocks */
sys_mem_blocks_free_contiguous(&my_blocks, blocks[0], 4);
}
Userspace and Memory Protection
When using Zephyr’s userspace feature, memory access is restricted by the Memory Protection Unit (MPU).
Configuration
# prj.conf
CONFIG_USERSPACE=y
Memory Domains
Threads in userspace can only access memory explicitly granted to them:
#include <zephyr/kernel.h>
/* Memory partitions accessible to user threads */
K_MEM_PARTITION_DEFINE(my_partition, my_buffer, sizeof(my_buffer),
K_MEM_PARTITION_P_RW_U_RW);
/* Add partition to a memory domain */
struct k_mem_domain my_domain;
struct k_mem_partition *parts[] = { &my_partition };
void setup(void)
{
k_mem_domain_init(&my_domain, ARRAY_SIZE(parts), parts);
k_mem_domain_add_thread(&my_domain, my_thread_id);
}
Shared Memory Between Threads
/* Grant a kernel object to a user thread */
k_object_access_grant(&my_mutex, my_thread_id);
/* Grant memory to a user thread */
k_mem_domain_add_partition(&my_domain, &shared_partition);
Choosing the Right Allocator
flowchart TD
START[Need dynamic memory?] --> FIXED{Fixed size<br/>blocks?}
FIXED -->|Yes| SLAB[Use Memory Slab<br/>k_mem_slab]
FIXED -->|No| ISOLATED{Need heap<br/>isolation?}
ISOLATED -->|Yes| KHEAP[Use k_heap<br/>per-subsystem]
ISOLATED -->|No| REALTIME{Real-time<br/>constraints?}
REALTIME -->|Yes| BLOCKS[Use Memory Blocks<br/>sys_mem_blocks]
REALTIME -->|No| KMALLOC[Use k_malloc<br/>system heap]
Decision Guide
| Scenario | Recommended | Reason |
|---|---|---|
| Message passing between threads | Memory Slab | Fixed size, deterministic |
| Network buffers | Memory Slab or net_buf | Designed for this use case |
| Occasional large allocation | k_heap | Variable size with isolation |
| Quick prototyping | k_malloc | Simplest API |
| Safety-critical code | Memory Slab | Deterministic, no fragmentation |
| Multiple independent subsystems | k_heap (per-subsystem) | Fault isolation |
Product Development Tips
- Prefer static allocation — stack variables and static buffers are always safest
- Use memory slabs for IPC — when passing data between threads, fixed-size blocks prevent fragmentation
- Size heaps carefully — use
CONFIG_HEAP_MEM_POOL_SIZEwisely; too small causes failures, too large wastes RAM - Monitor usage — enable
CONFIG_SYS_HEAP_RUNTIME_STATS=yto track heap usage at runtime - Test with constrained memory — simulate low-memory conditions to find allocation failures early
- Avoid malloc in ISRs — never call
k_mallocfrom interrupt context; useK_NO_WAITwith slabs if needed
Example Code
View the complete memory management example — demonstrates k_malloc, k_heap, and memory slabs.
west build -b qemu_cortex_m3 examples/part3/memory
west build -t run
Next Steps
Continue to Part 4: Synchronization & IPC to learn about inter-thread communication and synchronization.