
Dynamic memory allocation in bare-metal and RTOS-based firmware remains a polarizing topic. Safety-critical standards like MISRA C and ISO 26262 discourage or forbid heap usage after initialization. Yet many embedded Linux, RTOS, and application-layer firmware stacks rely on dynamic allocation for protocol buffers, command queues, and variable-length data structures. FreeRTOS acknowledges this reality by shipping five heap implementations (Heap_1 through Heap_5) in Source/portable/MemMang/, each with distinct fragmentation behavior, determinism guarantees, and memory overhead characteristics.
Choosing the wrong heap scheme manifests as silent memory corruption, allocation failures after hours of uptime, or priority inversion when a low-priority task holds a block the high-priority task needs. This article dissects all five implementations, their failure modes, and the decision criteria for selecting the right one.
File: heap_1.c
API: pvPortMalloc() only — no vPortFree()
Heap_1 implements a simple bump-pointer allocator. The heap is a single static array (ucHeap[configTOTAL_HEAP_SIZE]). A pointer tracks the next free byte. Each allocation rounds up to portBYTE_ALIGNMENT, returns the current pointer, and advances it by the requested size. It stores no block headers. Free does nothing.
/* heap_1.c -- simplified core logic with alignment support */static uint8_t ucHeap[configTOTAL_HEAP_SIZE];static size_t xNextFreeByte = 0;void *pvPortMalloc(size_t xWantedSize) {void *pvReturn = NULL;/* Ensure allocation size is aligned */if ((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0x00) {xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK));}vTaskSuspendAll();{if ((xNextFreeByte + xWantedSize) <= configTOTAL_HEAP_SIZE) {pvReturn = &ucHeap[xNextFreeByte];xNextFreeByte += xWantedSize;}}xTaskResumeAll();return pvReturn;}void vPortFree(void *pv) { (void)pv; } /* No-op */
Characteristics:
configTOTAL_HEAP_SIZE for the entire uptime.Use case: Safety-critical systems where all objects are created at startup and live forever. Bootloaders, simple state machines, certification-friendly profiles.
Failure mode: pvPortMalloc() returns NULL once the heap is exhausted. No recovery possible without reset.
+--------------------------------------------------------------+| HEAP_1 MEMORY MAP |+--------------------------------------------------------------+| Block A (64B) | Block B (128B) | Block C (32B) | FREE SPACE |+--------------------------------------------------------------+^ ^| |Heap Start Next Free
File: heap_2.c
API: pvPortMalloc(), vPortFree()
Heap_2 adds a free list. Each block (allocated or free) carries a BlockLink_t header containing the block size and a pointer to the next free block. Free inserts the block into the free list in size-sorted order (smallest first); allocate scans the free list for a fit, resulting in a best-fit allocation strategy.
/* heap_2.c -- simplified BlockLink_t and allocation */typedef struct A_BLOCK_LINK {struct A_BLOCK_LINK *pxNextFreeBlock;size_t xBlockSize;} BlockLink_t;static BlockLink_t xStart;static BlockLink_t *pxEnd = NULL;void *pvPortMalloc(size_t xWantedSize) {BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;void *pvReturn = NULL;/* Ensure allocation size is aligned and includes the block header */if ((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0x00) {xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK));}xWantedSize += xHeapStructSize;vTaskSuspendAll();{pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;/* Scan the free list (which is sorted by block size) */while ((pxBlock->xBlockSize < xWantedSize) && (pxBlock != pxEnd)) {pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}if (pxBlock != pxEnd) {/* Return pointer to payload (jumps over the header structure) */pvReturn = (void *)((uint8_t *)pxBlock + xHeapStructSize);/* Remove the block from the free list */pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;/* Split the block if the remainder is large enough */if ((pxBlock->xBlockSize - xWantedSize) > heapMINIMUM_BLOCK_SIZE) {pxNewBlockLink = (BlockLink_t *)((uint8_t *)pxBlock + xWantedSize);pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;/* Insert the split block back into the free list (sorted by size) */prvInsertBlockIntoFreeList(pxNewBlockLink);}}}xTaskResumeAll();return pvReturn;}
Characteristics:
free(), enabling object lifecyclesUse case: Systems with bounded allocation patterns where objects are created/destroyed but total concurrent allocations stay within a predictable envelope.
Failure mode: External fragmentation. Many small free blocks exist but no single block satisfies a large request. Allocation fails with NULL despite ample total free bytes.
+--------------------------------------------------------------+| HEAP_2 MEMORY MAP (FRAGMENTED) |+--------------------------------------------------------------+| A(64) | FREE(32) | B(128) | FREE(16) | C(32) | FREE(200) | ...+--------------------------------------------------------------+^ ^ ^| | |Cannot satisfy Cannot satisfy Only large block64B request 48B request but fragmented(two 32B free) (16+? free)
File: heap_3.c
API: pvPortMalloc(), vPortFree() → wraps malloc()/free()
Heap_3 is a thin thread-safety wrapper around the compiler’s standard library heap. Every pvPortMalloc() calls vTaskSuspendAll(), then malloc(), then xTaskResumeAll(). Free does the same with free().
/* heap_3.c -- entire implementation */void *pvPortMalloc(size_t xWantedSize) {void *pvReturn;vTaskSuspendAll();pvReturn = malloc(xWantedSize);xTaskResumeAll();return pvReturn;}void vPortFree(void *pv) {vTaskSuspendAll();free(pv);xTaskResumeAll();}
Characteristics:
.heap or __heap_start/__heap_end)Use case: Porting existing code that calls malloc/free directly; prototypes; non-real-time tasks.
Failure mode: Priority inversion during long malloc/free inside vTaskSuspendAll(). High-priority tasks blocked for indeterminate time. Also inherits libc fragmentation behavior.
+--------------------------------------------------------------+| HEAP_3 EXECUTION FLOW |+--------------------------------------------------------------+| Task A (high prio) || pvPortMalloc(64) || vTaskSuspendAll() ---- SCHEDULER SUSPENDED ---- || malloc(64) [unbounded time, may scan free lists] || xTaskResumeAll() ---- SCHEDULER RESUMED ---- || || Task B (low prio) -- BLOCKED entire duration -- |+--------------------------------------------------------------+
File: heap_4.c
API: pvPortMalloc(), vPortFree()
Heap_4 is the most widely used implementation for general-purpose FreeRTOS applications. It maintains a free list sorted by address. On vPortFree(), it checks whether the freed block is adjacent to the previous or next free block and coalesces them into a single larger block. This dramatically reduces external fragmentation compared to Heap_2.
/* heap_4.c -- coalescing logic in vPortFree */void vPortFree(void *pv) {BlockLink_t *pxBlockToFree = (BlockLink_t *)((uint8_t *)pv - xHeapStructSize);/* Clear the allocated status bit (MSB of xBlockSize) */pxBlockToFree->xBlockSize &= ~((size_t)1 << ((sizeof(size_t) * 8) - 1));vTaskSuspendAll();{/* Find the block in the free list */BlockLink_t *pxIterator = &xStart;while (pxIterator->pxNextFreeBlock < pxBlockToFree) {pxIterator = pxIterator->pxNextFreeBlock;}/* Coalesce with previous free block if adjacent */if ((uint8_t *)pxIterator + pxIterator->xBlockSize == (uint8_t *)pxBlockToFree) {pxIterator->xBlockSize += pxBlockToFree->xBlockSize;pxBlockToFree = pxIterator;} else {pxBlockToFree->pxNextFreeBlock = pxIterator->pxNextFreeBlock;pxIterator->pxNextFreeBlock = pxBlockToFree;}/* Coalesce with next free block if adjacent */if ((uint8_t *)pxBlockToFree + pxBlockToFree->xBlockSize ==(uint8_t *)pxBlockToFree->pxNextFreeBlock) {pxBlockToFree->xBlockSize += pxBlockToFree->pxNextFreeBlock->xBlockSize;pxBlockToFree->pxNextFreeBlock = pxBlockToFree->pxNextFreeBlock->pxNextFreeBlock;}}xTaskResumeAll();}
Characteristics:
ucHeap[configTOTAL_HEAP_SIZE])Use case: General-purpose FreeRTOS applications with dynamic object lifecycles. Default choice for most projects.
Failure mode: Internal fragmentation (allocation rounding) + residual external fragmentation when allocation patterns create many small holes that cannot be coalesced (non-adjacent frees). Large allocations may still fail despite free memory.
+--------------------------------------------------------------+| HEAP_4 COALESCING EXAMPLE |+--------------------------------------------------------------+| BEFORE FREE: || [A:64][FREE:32][B:128][FREE:16][C:32][FREE:200] || || vPortFree(B) -- coalesces with adjacent FREE blocks: || || AFTER FREE: || [A:64][FREE:376][C:32] || ^^^^^^^^^^^ merged into single 376-byte free block |+--------------------------------------------------------------+
File: heap_5.c
API: pvPortMalloc(), vPortFree(), vPortDefineHeapRegions()
Heap_5 extends Heap_4 by allowing the heap to span multiple disjoint memory regions. This is essential for MCUs with multiple RAM banks (e.g., STM32H7: SRAM1/SRAM2/SRAM3/AXI SRAM/ITC/DTC), where the largest contiguous region may be smaller than the total available RAM.
/* heap_5.c -- region definition */typedef struct HeapRegion {uint8_t *pucStartAddress;size_t xSizeInBytes;} HeapRegion_t;HeapRegion_t xHeapRegions[] = {{ (uint8_t *)0x20000000, 0x20000 }, /* SRAM1: 128 KB */{ (uint8_t *)0x20020000, 0x10000 }, /* SRAM2: 64 KB */{ (uint8_t *)0x30000000, 0x40000 }, /* AXI SRAM: 256 KB */{ NULL, 0 } /* Terminator */};void vPortDefineHeapRegions(const HeapRegion_t *const pxHeapRegions) {/* Validate alignment, sort by address, build free list */}
Characteristics:
portBYTE_ALIGNMENTvPortDefineHeapRegions() must be called before any allocation (typically in main() before scheduler start)Use case: Multi-bank RAM MCUs (STM32H7, NXP i.MX RT, TI TM4C, ESP32-S3 with external PSRAM), applications needing large total heap but no single contiguous region.
Failure mode: Same fragmentation risks as Heap_4, distributed across regions. A large allocation may fail even if total free space is sufficient, if no single region has a large enough block.
+--------------------------------------------------------------+| HEAP_5 MULTI-REGION MAP |+--------------------------------------------------------------+| REGION 1 (SRAM1, 128KB) | REGION 2 (SRAM2, 64KB) || +----------------------------+ | +------------------------+ || | TaskA(32K) | FREE(64K) | | | FREE(64K) | || +----------------------------+ | +------------------------+ || | || REGION 3 (AXI SRAM, 256KB) || +----------------------------------------------------------+ || | TaskB(100K) | FREE(156K) | || +----------------------------------------------------------+ || || pvPortMalloc(200K) --> FAILS (no single region has 200K) || pvPortMalloc(80K) --> SUCCEEDS (allocates from Region 3) |+--------------------------------------------------------------+
| Criterion | Heap_1 | Heap_2 | Heap_3 | Heap_4 | Heap_5 |
|---|---|---|---|---|---|
free() support | ❌ | ✅ | ✅ | ✅ | ✅ |
| Coalescing | N/A | ❌ | libc | ✅ | ✅ |
| Multi-region | ❌ | ❌ | ❌ | ❌ | ✅ |
| Deterministic alloc | ✅ O(1) | ⚠️ O(n) | ❌ | ⚠️ O(n) | ⚠️ O(n) |
| Fragmentation risk | None | High | libc | Low | Low |
| Overhead/block | 0B | ~8B | libc | ~8B | ~8B |
Heap_4/5 sizing: Set configTOTAL_HEAP_SIZE (Heap_4) or sum of region sizes (Heap_5) to leave 15-20% headroom above peak observed usage. Monitor with xPortGetFreeHeapSize() and xPortGetMinimumEverFreeHeapSize().
Heap_5 region ordering: Regions must be declared in start-address order (from lowest address to highest address). Heap_5 does not sort them and will fail an assertion (configASSERT( ( size_t ) xAddress > ( size_t ) pxEnd )) or corrupt memory if defined out-of-order. Additionally, each region must be individually aligned to portBYTE_ALIGNMENT (typically 8 bytes on Cortex-M).
Debugging fragmentation: Enable configUSE_MALLOC_FAILED_HOOK to catch allocation failures. For Heap_4 and Heap_5, call vPortGetHeapStats() (introduced in FreeRTOS v10.1.0) to get a HeapStats_t structure detailing the number of free blocks, the size of the largest/smallest free blocks, and the allocation/free call counters. This avoids having to write custom code to walk the private free list structures.
Thread safety: All five implementations use vTaskSuspendAll/xTaskResumeAll for mutual exclusion. Because they rely on scheduler suspension, none of the standard FreeRTOS heaps are ISR-safe. You must never call pvPortMalloc() or vPortFree() from an Interrupt Service Routine. For ISR-safe memory management, use a lock-free ring buffer, a statically allocated pool, or defer the processing to a task via xQueueSendFromISR or task notifications.
FreeRTOS’s five heap implementations form a spectrum from zero-overhead bump allocation (Heap_1) to multi-region coalescing allocators (Heap_5). The choice is not merely about whether you need free() — it’s about fragmentation tolerance, determinism requirements, and physical memory topology. Heap_4 is the correct default for most applications. Heap_5 is mandatory when your MCU’s RAM is split across non-contiguous banks and you need a single logical heap larger than any one bank. Heap_1 and Heap_2 serve safety-critical niches where certification demands zero fragmentation. Heap_3 is a migration aid, not a production allocator for real-time tasks.
Measure your actual allocation patterns under load. Enable the malloc failed hook. Profile xPortGetMinimumEverFreeHeapSize() over 24-hour soak tests. The heap that works on the bench at 20% load may fragment catastrophically at 80% load with a different allocation sequence.
Quick Links
Legal Stuff

