
Dynamic memory allocation is a double-edged sword in embedded systems. While malloc() and free() offer flexibility, their uncontrolled use in RTOS-based firmware can lead to heap fragmentation—a silent killer that causes sporadic allocation failures in systems that ran perfectly for hours or days. Understanding heap fragmentation and implementing robust memory management strategies is essential for building reliable embedded software.
Heap fragmentation occurs when free memory is scattered throughout the heap in small, non-contiguous blocks. Over time, allocations and frees of varying sizes create a “Swiss cheese” pattern in memory. Even though the total free memory may be sufficient, no single contiguous block is large enough to satisfy a new allocation request.
There are two types of fragmentation:
The diagram below illustrates how a heap evolves from a clean state to a fragmented one over repeated allocation and deallocation cycles:
HEAP STATE OVER TIME====================INITIAL STATE (after boot):+--------------------------------------------------------------+| FREE || (64 KB total) |+--------------------------------------------------------------+AFTER ALLOCATIONS (Tasks A-F allocate the entire heap):+------+--------+------+------+------+-------------------------+|Task A| Task B |Task C|Task D|Task E| Task F || 4 KB | 8 KB | 2 KB | 4 KB | 6 KB | 40 KB |+------+--------+------+------+------+-------------------------+AFTER SOME FREES (Tasks A, C, and D freed, others alive):+------+--------+------+------+------+-------------------------+| FREE | Task B | FREE | FREE |Task E| Task F || 4 KB | 8 KB | 2 KB | 4 KB | 6 KB | 40 KB |+------+--------+------+------+------+-------------------------+^ ^ ^| | |+--- Fragmented small blocksTHE PROBLEM: A new 6 KB allocation FAILS despite 10 KB free!The 4 KB, 2 KB, and 4 KB free blocks are all too small individually.Task B separates the first 4 KB block from the others.AFTER COALESCING (heap_4.c style -- adjacent free blocks merged):The 2 KB and 4 KB free blocks are adjacent, so they merge intoa single 6 KB block. The first 4 KB block remains isolated.+------+--------+-------------+------+-------------------------+| FREE | Task B | FREE (6 KB) |Task E| Task F || 4 KB | 8 KB | (2KB + 4KB) | 6 KB | 40 KB |+------+--------+-------------+------+-------------------------+OK A 6 KB allocation now succeeds from the merged 6 KB block!OVER TIME, WITHOUT COALESCING (heap_2.c style -- never merges):If Task E and F are later freed, their blocks remain separate.+------+--------+------+------+------+-------------------------+| FREE | Task B | FREE | FREE | FREE | FREE || 4 KB | 8 KB | 2 KB | 4 KB | 6 KB | 40 KB |+------+--------+------+------+------+-------------------------+^ ^ ^ ^| | | |These adjacent free blocks are never merged!(A 52 KB allocation would fail here)
In RTOS environments, fragmentation is especially problematic because tasks allocate and free memory at different rates and with different size patterns.
In a bare-metal super-loop architecture, allocation patterns are typically predictable—most allocations happen at startup, and few or none occur during the main loop. RTOS systems are different:
Consider a scenario where Task A allocates 64-byte blocks and Task B allocates 128-byte blocks. If Task A frees every other block and Task B frees blocks at a different rate, the heap quickly becomes a mosaic of used and free regions that cannot satisfy a new 256-byte allocation—even when 70% of the heap is free.
The most robust approach replaces generic malloc()/free() with fixed-size block allocators. A memory pool pre-allocates an array of fixed-size blocks and maintains a free list. All allocations return blocks of identical size, eliminating external fragmentation entirely.
class MemoryPool {uint8_t* pool;uint32_t block_size;uint32_t block_count;void* free_list;public:// block_size must be >= sizeof(void*) and aligned to alignof(void*)// to avoid misaligned access faults (e.g., HardFault on Cortex-M0/M0+).MemoryPool(void* memory, uint32_t block_size, uint32_t count): block_size(block_size), block_count(count){pool = static_cast<uint8_t*>(memory);// Build free list in forward order (Block 0 at head)free_list = nullptr;for (uint32_t i = count; i > 0; --i) {void* block = pool + ((i - 1) * block_size);*reinterpret_cast<void**>(block) = free_list;free_list = block;}}void* allocate(){if (!free_list) return nullptr; // Pool exhaustedvoid* block = free_list;free_list = *reinterpret_cast<void**>(free_list);return block;}void deallocate(void* block){if (block) {*reinterpret_cast<void**>(block) = free_list;free_list = block;}}};
Memory pools are deterministic—both allocation and deallocation are O(1) operations with no searching or coalescing overhead. In an RTOS environment, pool operations must be wrapped in a critical section or mutex to ensure thread safety across concurrent tasks.
The internal structure of a memory pool with 5 blocks of 64 bytes each:
MEMORY POOL (5 x 64-byte blocks)=================================FREE LIST (initially all blocks free, head at Block 0):head --> +----------+ +----------+ +----------+| Block 0 |---->| Block 1 |---->| Block 2 |--> ...| next* | | next* | | next* |+----------+ +----------+ +----------++----------+ +----------+| Block 3 |---->| Block 4 |--> NULL| next* | | next* |+----------+ +----------+* "next" pointer is overlaid in the block's first sizeof(void*) bytes.When allocated, the caller gets the full 64 bytes (zero overhead).AFTER allocate() x 3 (returns Block 0, Block 1, Block 2):head --> +----------+ +----------+| Block 3 |---->| Block 4 |--> NULL| next* | | next* |+----------+ +----------+Allocated: Block 0, Block 1, Block 2 <-- returned to callerAFTER deallocate(Block 1) -- pushed to head of free list:head --> +----------+ +----------+ +----------+| Block 1 |---->| Block 3 |---->| Block 4 |--> NULL| next* | | next* | | next* |+----------+ +----------+ +----------+OK Zero fragmentation -- every alloc returns a full 64-byte blockOK O(1) pointer unlink/link -- no searching, no mergingOK Deterministic -- critical for real-time tasks
For systems requiring multiple allocation sizes, use a set of memory pools with different block sizes (e.g., 32, 64, 128, 256 bytes). The allocator routes each request to the smallest pool that can satisfy it. This “size-class” approach is used by commercial RTOS implementations such as µC/OS-III memory partitions and can be implemented as a custom allocator layer on top of any RTOS. Note that FreeRTOS’s built-in heap schemes (heap_1 through heap_5) are general-purpose allocators and do not use this pattern natively.
The safest approach for safety-critical systems is to eliminate dynamic allocation entirely. All objects—task stacks, message buffers, state machines—are allocated statically at compile time. MISRA C:2012 Directive 4.12 states that “dynamic memory allocation shall not be used,” and Rule 21.3 specifically prohibits malloc, calloc, realloc, and free. FreeRTOS supports this pattern through configSUPPORT_STATIC_ALLOCATION, which enables APIs like xTaskCreateStatic() and xQueueCreateStatic() for creating kernel objects with statically allocated memory.
An arena allocator pre-allocates a large contiguous region and hands out memory sequentially. Deallocation resets the entire region at once. This pattern is useful for processing pipelines where all temporary data has a well-defined lifecycle.
Most RTOS kernels provide multiple heap implementation options:
| Heap Variant | malloc | free | Coalesces Adjacent? | Deterministic? |
|---|---|---|---|---|
| heap_1 | YES | NO | - | YES |
| heap_2 | YES | YES | NO | NO - best-fit search |
| heap_3 | YES | YES | (libc) | NO (libc) |
| heap_4 | YES | YES | YES | NO - first-fit search + merge cost |
| heap_5 | YES | YES | YES | NO - first-fit search + merge cost, multi-region |
Recommended choice by use case:
| Use Case | Recommended Heap |
|---|---|
| Safety-critical, no freeing | heap_1 or static alloc |
| Fixed-size blocks only | heap_2 (or memory pools) |
| Variable-size, general purpose | heap_4 (with monitoring) |
| Disjoint memory regions (RAM1 + RAM2 + external SDRAM) | heap_5 |
Note: heap_3 is rarely the right choice — wraps libc malloc/free with mutex protection but inherits all fragmentation behavior.
malloc()/free() with scheduler suspension for thread safety. Subject to all libc fragmentation behavior.For production systems, static allocation or custom memory pools are preferred. heap_4 is the best general-purpose option when variable-size allocations are unavoidable. heap_2 is legacy and heap_3 should be avoided in new designs.
Even with good design practices, it is wise to monitor heap health at runtime:
// FreeRTOS provides vPortGetHeapStats() which populates the HeapStats_t// structure (available in heap_4 and heap_5). Key fields include:// xAvailableHeapSpaceInBytes - total free heap bytes// xSizeOfLargestFreeBlockInBytes - largest contiguous free block// xSizeOfSmallestFreeBlockInBytes - smallest free block// xNumberOfFreeBlocks - count of free blocks (fragmentation indicator)// xMinimumEverFreeBytesRemaining - low-water mark of free heap// xNumberOfSuccessfulAllocations - lifetime allocation count// xNumberOfSuccessfulFrees - lifetime free countvoid log_heap_stats(){HeapStats_t stats;vPortGetHeapStats(&stats);// Fragmentation index: 0.0 = no fragmentation, 1.0 = fully fragmented// Formula: 1.0 - (largest_free_block / total_free)float frag_index = 0.0f;if (stats.xAvailableHeapSpaceInBytes > 0) {frag_index = 1.0f - ((float)stats.xSizeOfLargestFreeBlockInBytes/ (float)stats.xAvailableHeapSpaceInBytes);}printf("Heap: %u free / %u total (min ever: %u)\n",(unsigned)stats.xAvailableHeapSpaceInBytes,(unsigned)configTOTAL_HEAP_SIZE,(unsigned)stats.xMinimumEverFreeBytesRemaining);printf("Fragmentation index: %.2f\n", frag_index);printf("Largest block: %u / Free blocks: %u\n",(unsigned)stats.xSizeOfLargestFreeBlockInBytes,(unsigned)stats.xNumberOfFreeBlocks);}
The fragmentation index is calculated as 1.0 - (largest_free_block / total_free_memory). A value of 0.0 means all free memory is in one contiguous block (no fragmentation). A value approaching 1.0 signals that free memory is scattered into many small blocks—the largest free block is small relative to total free memory. Monitoring this value during development and in field-deployed units provides early warning of impending allocation failures.
malloc()/free() in time-critical paths: The non-deterministic execution time makes them unsuitable for hard real-time tasks. Consider a TLSF (Two-Level Segregated Fit) allocator if you need O(1) general-purpose allocation with bounded worst-case time.Heap fragmentation is one of the most insidious bugs in embedded systems because it manifests as intermittent failures that are nearly impossible to reproduce in short test runs. By adopting pool-based allocation, monitoring heap health, and designing for deterministic memory usage, firmware engineers can eliminate this entire class of runtime failures from their RTOS-based systems.
Buttazzo, G. C. (2011). Hard Real-Time Computing Systems: Predictable Scheduling Algorithms and Applications (3rd ed.). Springer. ISBN: 978-1-4614-3675-1. (Background on deterministic timing requirements in real-time systems.)
FreeRTOS Kernel Documentation — Memory Management. Amazon Web Services. https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/09-Memory-management/01-Memory-management
Barr, M. & Massa, A. (2006). Programming Embedded Systems: With C and GNU Development Tools (2nd ed.). O’Reilly Media. Chapter 6: Memory.
MISRA C:2012 Guidelines for the Use of the C Language in Critical Systems. MISRA Consortium. https://misra.org.uk/
Wilson, P. R., Johnstone, M. S., Neely, M., & Boles, D. (1995). “Dynamic Storage Allocation: A Survey and Critical Review.” International Workshop on Memory Management (IWMM), LNCS 986, Springer-Verlag, 1–116.
Labrosse, J. J. (2012). µC/OS-III: The Real-Time Kernel for the STM32 ARM Cortex-M3. Micrium Press. Chapter 10: Memory Management.
Masmano, M., Ripoll, I., Crespo, A., & Real, J. (2004). “TLSF: A New Dynamic Memory Allocator for Real-Time Systems.” Proceedings of the 16th Euromicro Conference on Real-Time Systems (ECRTS), IEEE, 79–86.
Quick Links
Legal Stuff





