Choosing a Memory Allocation Strategy for Bare-Metal Embedded C
A decision guide for choosing between static, dynamic, and pool-based memory allocation in bare-metal Embedded C, featuring a MISRA-compliant implementation.
17 Nov 2025, 15:17 UTC

The Memory Management Dilemma
In bare-metal firmware for microcontrollers (MCUs) lacking a Memory Management Unit (MMU), the developer is responsible for every byte of RAM. The primary challenge is balancing the need for runtime flexibility against the strict requirements of determinism and safety. In safety-critical environments, an unexpected NULL return from an allocator or a fragmented heap can lead to catastrophic system failure.
The goal is to select an allocation strategy that ensures the system never runs out of memory at runtime and maintains a predictable execution time (determinism).
Allocation Strategies Comparison
| Strategy | Determinism | Fragmentation | MISRA 21.3 | Flexibility | Implementation |
|---|---|---|---|---|---|
| Static (Global/File-scope) | Absolute | None | Compliant | Low | Trivial |
| Dynamic (malloc/free) | Low | External | Non-compliant | High | Library-based |
| Static Memory Pools | High (O(1)) | Internal | Compliant | Medium | Custom Code |
Trade-offs and Constraints
Static Compile-Time Allocation
By declaring variables as globals or using the static keyword, memory is assigned to the .bss (zero-initialized) or .data (initialized) sections. The linker calculates the total RAM usage at build time. If the code fits in the linker map, it will not fail due to "out of memory" errors during execution. However, this lacks flexibility; you must size every buffer for its worst-case scenario.
Dynamic Allocation (Heap)
Standard malloc and free allow for runtime sizing but introduce external fragmentation—where free memory is split into small, non-contiguous blocks. This makes allocation time non-deterministic and increases the risk of failure. MISRA C 2012 Rule 21.3 explicitly advises against this in safety-related code because the failure modes are difficult to test exhaustively.
Static Memory Pools
A pool allocator pre-allocates a large static array and carves it into fixed-size blocks. This provides the flexibility of requesting and releasing memory at runtime while maintaining O(1) time complexity and eliminating external fragmentation. The trade-off is internal fragmentation: if you request 32 bytes from a 64-byte block pool, 32 bytes are wasted.
Implementation: Fixed-Block Pool Allocator
The following implementation provides a deterministic allocator for 64-byte blocks, suitable for message buffers or OS event objects. This approach avoids the heap entirely.
/* pool.h */
#ifndef POOL_H
#define POOL_H
#include <stddef.h>
#include <stdbool.h>
#define POOL_BLOCK_SIZE 64U
#define POOL_BLOCK_COUNT 16U
typedef struct {
unsigned char data[POOL_BLOCK_SIZE];
} pool_block_t;
typedef struct {
pool_block_t blocks[POOL_BLOCK_COUNT];
bool used[POOL_BLOCK_COUNT];
} pool_t;
void pool_init(pool_t *const p);
void *pool_alloc(pool_t *const p);
void pool_free(pool_t *const p, void *const ptr);
#endif
/* pool.c */
#include "pool.h"
void pool_init(pool_t *const p) {
for (size_t i = 0; i < POOL_BLOCK_COUNT; ++i) {
p->used[i] = false;
}
}
void *pool_alloc(pool_t *const p) {
for (size_t i = 0; i < POOL_BLOCK_COUNT; ++i) {
if (!p->used[i]) {
p->used[i] = true;
return &p->blocks[i];
}
}
return NULL; // Deterministic failure: pool exhausted
}
void pool_free(pool_t *const p, void *const ptr) {
if (ptr == NULL) return;
// Pointer arithmetic to find index relative to start of array
const size_t index = ((pool_block_t *)ptr) - p->blocks;
if (index < POOL_BLOCK_COUNT) {
p->used[index] = false;
}
}
Validation and Verification
To ensure the chosen strategy is safe, perform the following checks:
- Linker Map Analysis: Examine the
.mapfile generated by the toolchain. Sum the sizes of.bssand.data. Ensure this total is significantly lower than the physical RAM to leave room for the stack. - Stack Depth Analysis: Use compiler flags like
-fstack-usage(GCC) to generate.sufiles. This identifies the worst-case stack frame for every function, preventing stack-heap collisions. - Dynamic Allocation Guard: To enforce a no-malloc policy, add
-Werror=implicit-function-declarationand use a static analysis tool to flag any calls tomallocorfree.
Limitations
- Internal Waste: Pool allocators waste space if the requested data is smaller than the block size.
- Static Sizing: All pools must be sized at compile time. If the application requirements change, the code must be recompiled.
- Stack Risks: None of these strategies prevent stack overflow. Always use a stack guard or a pattern-fill check in the idle task to monitor high-water marks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.