
In embedded systems, especially those with real-time constraints, memory allocation is a critical design decision that can make or break system reliability. Standard dynamic memory allocation via malloc() and free() introduces two dangerous sources of indeterminism: unbounded execution time and unpredictable memory fragmentation. Over hours or days of continuous operation, repeated allocation and deallocation of variable-size blocks can fragment the heap to the point where allocation fails despite sufficient total free memory—a catastrophic outcome for safety-critical systems.
Memory pool allocation offers an elegant solution. By pre-partitioning a fixed region of memory into equal-sized blocks—or a set of pools with different fixed sizes—pool allocators eliminate external fragmentation entirely and guarantee constant-time O(1) allocation and deallocation. This makes them indispensable for hard real-time embedded applications where worst-case execution time (WCET) must be provably bounded.
The standard C library allocator is designed for general-purpose computing, where throughput matters more than predictability. It employs complex algorithms—free list coalescing, block splitting, delayed free—that ensure good average-case performance but produce highly variable worst-case timing. A single malloc() call might complete in microseconds or stall for milliseconds while searching a fragmented heap or invoking sbrk() to request more memory from the operating system.
In a bare-metal or RTOS-based embedded system, neither behavior is acceptable. There is usually no OS to extend the heap, and timing jitter in allocation can cascade into missed deadlines, priority inversion, or system failure. Moreover, malloc() provides no thread safety guarantees without additional locking, which introduces further latency and deadlock risk.
Real-time system designers traditionally avoided dynamic allocation altogether, resorting to fully static designs where every buffer, message queue, and state structure is defined at compile time. While safe, this approach sacrifices flexibility—requiring oversized buffers to handle worst-case payloads and making runtime reconfiguration impossible.
A memory pool is a pre-allocated array of fixed-size memory blocks, managed as a singly linked list called a free list. At initialization, every block in the pool is linked into this free list. When an application requests memory, the allocator simply removes the first block from the free list and returns it. When the application frees the block, it is pushed back onto the front of the free list.
Both operations—allocate and free—are constant time O(1): they involve nothing more than pointer manipulation. There is no searching, no splitting, no merging, and no traversal. The execution path is identical regardless of how many blocks are allocated, the order in which they were freed, or the overall state of the pool.
#include <stdint.h>#include <stddef.h>/* Simple fixed-block memory pool */#define POOL_BLOCK_SIZE 64U#define POOL_BLOCK_COUNT 32U#define POOL_ALIGN 8U/* A block must be large enough to hold a pointer when free */typedef struct pool_block {struct pool_block *next;} pool_block_t;_Static_assert(POOL_BLOCK_SIZE >= sizeof(pool_block_t), "POOL_BLOCK_SIZE too small");typedef struct {pool_block_t *free_list;uint32_t total_blocks;uint32_t free_blocks;} memory_pool_t;/* Statically allocated pool storage */static uint8_t pool_storage[POOL_BLOCK_COUNT][POOL_BLOCK_SIZE] __attribute__((aligned(POOL_ALIGN)));/* Initialize the pool - links all blocks into the free list */void pool_init(memory_pool_t *pool){pool->total_blocks = POOL_BLOCK_COUNT;pool->free_blocks = POOL_BLOCK_COUNT;pool->free_list = NULL;/* Add each block to the front of the free list */for (uint32_t i = 0; i < POOL_BLOCK_COUNT; i++) {pool_block_t *block = (pool_block_t *)pool_storage[i];block->next = pool->free_list;pool->free_list = block;}}/* Allocate a block - O(1), deterministic */void *pool_alloc(memory_pool_t *pool){if (pool->free_list == NULL) {return NULL; /* Pool exhausted */}pool_block_t *block = pool->free_list;pool->free_list = block->next;pool->free_blocks--;return (void *)block;}/* Free a block - O(1), deterministic */void pool_free(memory_pool_t *pool, void *ptr){if (ptr == NULL) return;/* Validate pointer belong to pool */if (ptr < (void *)pool_storage || ptr >= (void *)(pool_storage + POOL_BLOCK_COUNT)){return;}pool_block_t *block = (pool_block_t *)ptr;block->next = pool->free_list;pool->free_list = block;pool->free_blocks++;}
This minimal implementation demonstrates the core concept: no loops, no system calls, no fragmentation. The block size is fixed at compile time, so there is zero external fragmentation. Internal fragmentation is limited to at most (POOL_BLOCK_SIZE - requested_bytes) per allocation—a known, bounded quantity that the designer controls by choosing the block size.
A single fixed-size pool is efficient but inflexible. Real-world embedded systems often need to allocate structures of different sizes—small message headers, medium-sized packet buffers, and large frame storage. Allocating all of them from a single large pool wastes memory when many small objects are needed.
The solution is size-segregated pooling: maintain multiple pools, each with a different fixed block size. When an application requests N bytes, it selects the smallest pool whose block size is ≥ N. This approach, used by RTOS kernels like ChibiOS, FreeRTOS, and ARM CMSIS-RTOS, dramatically reduces internal fragmentation while preserving O(1) deterministic allocation for each pool.
The concept is similar to the TLSF (Two-Level Segregated Fit) allocator, which extends this idea with a two-level bitmap-indexed lookup to find the appropriate pool in O(1) time, supporting a wide range of block sizes efficiently.
Most modern RTOS kernels provide built-in memory pool services. ARM CMSIS-RTOS2 defines osMemoryPoolNew(), osMemoryPoolAlloc(), and osMemoryPoolFree() as standard APIs, ensuring portable code across Cortex-M microcontrollers. These RTOS pools are thread-safe by design—allocation and free operations can be called from both task context and ISRs (with timeout = 0 from ISRs), using internal locking or lock-free algorithms.
ChibiOS/RT offers both a simple pool allocator (chPoolAlloc, chPoolFree) and a guarded pool variant that uses a semaphore to block threads when the pool is empty, eliminating busy-waiting. The Zephyr RTOS implements memory pools with a buddy system that supports variable-size block requests while limiting fragmentation through power-of-two splitting and merging.
FreeRTOS takes a different approach, eschewing built-in pool APIs in favor of its heap implementations (heap_1 through heap_5), where heap_4 uses a first-fit coalescing algorithm and the application is expected to implement pool-like behavior using statically allocated queues fixed at specific object sizes.
Memory fragmentation is the gradual fragmentation of free memory into small, non-contiguous pieces that cannot satisfy allocation requests despite their aggregate size. In embedded systems that run for months or years—automotive ECUs, aerospace controllers, industrial PLCs—fragmentation accumulates steadily until the system fails unpredictably.
Memory pools completely eliminate external fragmentation because blocks are never split or merged. A returned block always goes back to exactly the same position in the free list. The only fragmentation that exists is internal: when a request for 20 bytes comes from a 64-byte pool, 44 bytes are wasted. This waste is deterministic—the designer chooses block sizes during system design based on known allocation patterns.
By contrast, general-purpose allocators like the standard malloc() can suffer from unbounded fragmentation over time. Studies have shown that fragmentation in long-running embedded systems can waste 30-50% of total memory, with no guarantee of improvement even if the total free memory exceeds the requested block size.
One of the most compelling reasons to use memory pools is their compatibility with Worst-Case Execution Time analysis. WCET analysis is a fundamental requirement in safety-critical systems governed by standards like DO-178C (avionics), ISO 26262 (automotive), and IEC 61508 (industrial). These standards require that every timing-critical operation has a provably bounded execution time.
A fixed-block pool allocator’s alloc() and free() operations execute the same instructions regardless of system state. There are no loops whose iteration count depends on heap condition, no system calls, and no memory searches. The worst-case execution time equals the best-case execution time—enabling straightforward static timing analysis or simple cycle-counting on the target platform.
On a Cortex-M4 running at 72 MHz, a lock-free fixed-block pool allocation takes fewer than 20 clock cycles (under 280 nanoseconds). This predictability makes pools suitable even for allocation inside interrupt service routines, where deterministic latency is paramount.
When incorporating memory pools into an embedded design, several factors must be considered:
Block sizing should match the actual structures being allocated. Profile the application to determine the distribution of allocation sizes, then configure pools with block sizes that minimize internal fragmentation across the expected workload.
Pool capacity must be sufficient for the maximum concurrent allocations. This is known at design time because embedded systems typically have a fixed number of tasks, each with a known maximum allocation count. Over-provisioning pools with extra blocks costs a small amount of RAM but eliminates runtime allocation failures.
Alignment requirements must be respected. ARM Cortex-M processors require 4-byte or 8-byte aligned accesses for optimal performance. Pool block sizes should be rounded up to the platform’s alignment requirement, and the pool storage array should be aligned accordingly using compiler attributes or alignment macros.
ISR safety requires lock-free pool implementations. When an ISR needs to allocate or free a pool block (for example, to pass a data structure to a waiting task via a message queue), the pool operations must not use mutexes that could deadlock. Atomic compare-and-swap (CAS) instructions or interrupt-disable/enable sequences provide safe alternatives.
Memory pool allocation provides deterministic, constant-time memory management that is essential for real-time embedded systems. By eliminating external fragmentation and providing predictable O(1) allocation and deallocation, pools resolve the inherent indeterminism of general-purpose allocators like malloc(). Whether implemented as a simple custom fixed-block allocator or integrated through an RTOS’s built-in pool APIs, this technique enables safe dynamic memory usage in safety-critical, long-running, and resource-constrained environments.
For embedded engineers preparing for interviews or designing real-time systems, understanding memory pool internals—free list management, size segregation, ISR-safe access, and WCET analysis—is fundamental knowledge that demonstrates mastery of both C programming and embedded systems architecture.
Quick Links
Legal Stuff




