
Memory is one of the most precious resources in embedded systems. Unlike desktop applications with gigabytes of RAM, microcontrollers often operate with as little as 32 bytes to a few kilobytes of RAM. Every byte counts, and how you manage it can mean the difference between a rock-solid firmware and a system that crashes unpredictably in the field. This fundamental tension makes the choice between static and dynamic memory allocation one of the most important design decisions an embedded engineer will make.
Static memory allocation reserves memory at compile time. The compiler places variables in well-defined memory regions — .data for initialized variables, .bss for zero-initialized variables, and the stack for local function variables and call frames. The total memory footprint is known before the program ever executes.
// Static allocation - size known at compile time#define BUFFER_SIZE 256static uint8_t rx_buffer[BUFFER_SIZE];static uint32_t packet_count = 0;void process_packet(void) {uint8_t local_buf[64]; // Stack allocation (automatic storage, size fixed at compile time)// ...}
The key advantage is determinism. There is no runtime overhead for allocation itself, no risk of fragmentation, and no possibility of memory leaks. Safety-critical standards like MISRA C strongly prefer static allocation for these reasons. In automotive, aerospace, and medical device firmware, dynamic heap allocation is often outright banned.
Dynamic memory allocation happens at runtime through malloc(), calloc(), and free(). In many systems, the heap grows upward from the end of .bss toward the top of RAM, while the stack grows downward from the top, but this is implementation-dependent. Between them lies the heap — a shared, contested space.
// Dynamic allocation - size determined at runtimeuint8_t *create_buffer(size_t size) {uint8_t *buf = (uint8_t *)malloc(size);if (buf == NULL) {// Handle allocation failurereturn NULL;}return buf;}void destroy_buffer(uint8_t *buf) {free(buf);}
Dynamic allocation offers flexibility. When the exact memory requirement depends on runtime conditions — such as a configurable number of sensor channels or variable-length communication frames — dynamic allocation allows the firmware to adapt without wasting memory on worst-case static arrays.
The most insidious problem with dynamic memory in long-running embedded systems is fragmentation. As blocks of varying sizes are allocated and freed over time, the heap becomes a patchwork of used and free regions. Even if the total free memory is sufficient, no single contiguous block may be large enough to satisfy a new allocation request.
Consider a system that runs for months allocating and freeing 64-byte and 128-byte blocks alternately. Over time, the heap becomes fragmented into alternating 64-byte holes. When a 256-byte allocation is needed, it fails despite having hundreds of bytes of total free memory.
A call to malloc() may need to search the heap depending on the allocation algorithm for a suitable free block. The time this takes depends on the heap’s current state — the number of free blocks, their sizes, and the allocation algorithm used. In hard real-time systems where interrupt service routines must complete within microseconds, this variability is unacceptable.
Unlike desktop systems where an out-of-memory condition might trigger swapping or graceful degradation, an embedded system that fails to allocate memory often has no recovery path. A NULL return from malloc() that goes unchecked leads to crashes. Conversely, forgetting to call free() causes memory leaks that slowly consume the heap until the system fails.
Many embedded systems adopt memory pools (also called fixed-size block allocators) as a compromise. A pool pre-allocates a fixed number of equally-sized blocks. Allocation and deallocation are O(1) operations with no fragmentation, since all blocks are the same size.
// Simple memory pool implementation#define POOL_BLOCK_SIZE 64#define POOL_NUM_BLOCKS 16static uint8_t pool_memory[POOL_NUM_BLOCKS][POOL_BLOCK_SIZE];static bool pool_used[POOL_NUM_BLOCKS] = {0};void *pool_alloc(void) {for (int i = 0; i < POOL_NUM_BLOCKS; i++) {if (!pool_used[i]) {pool_used[i] = true;return pool_memory[i];}}return NULL; // Pool exhausted}void pool_free(void *ptr) {for (int i = 0; i < POOL_NUM_BLOCKS; i++) {if (ptr == (void *)pool_memory[i]) {pool_used[i] = false;return;}}}
Memory pools eliminate fragmentation and provide deterministic allocation times while retaining some of the flexibility of dynamic allocation. They are widely used in RTOS message queues, network packet buffers, and task communication.
This simple pool is not thread-safe. In RTOS or interrupt-driven systems, it must be protected using mutexes or critical sections.
Prefer static allocation for safety-critical code. If the maximum memory requirement can be determined at compile time, use static arrays and global variables. This eliminates entire categories of runtime failures.
Use dynamic allocation only during initialization. Allocate all needed memory during the startup phase before entering the main operational loop. After initialization, never call malloc() or free(). This prevents fragmentation and ensures that if the system boots successfully, it has all the memory it needs.
Consider memory pools for runtime flexibility. When you genuinely need to allocate and free memory during operation, use fixed-size memory pools instead of the general heap. This gives you O(1) allocation with zero fragmentation.
Always check malloc() return values. Never assume that malloc() will succeed. Every call must be followed by a NULL check with appropriate error handling.
Monitor stack usage. The stack is often overlooked. Use stack canaries, MPU regions, or compiler flags like -fstack-usage to ensure your stack does not overflow into the heap or other memory regions.
In embedded systems, memory management is not just a programming concern — it is an architectural decision that affects reliability, safety, and longevity. Static allocation provides maximum determinism and is the gold standard for safety-critical systems. Dynamic allocation offers flexibility but introduces fragmentation, non-deterministic timing, and the risk of memory leaks. Memory pools offer a practical middle ground for systems that need runtime flexibility without the risks of a general-purpose heap. The best embedded engineers choose their allocation strategy deliberately, understanding the trade-offs and designing for the constraints of their target platform.
Quick Links
Legal Stuff





