Static Allocation vs Dynamic Memory in Embedded C: A Practical Guide to Deterministic Systems
Choosing static allocation over dynamic memory in embedded C guarantees deterministic timing and prevents heap fragmentation. This guide shows why, with a UART ring‑buffer example, how to place data in Flash, align structures, and verify placement via the linker map.
27 Sept 2025, 08:38 UTC

Problem: Deterministic Memory in Embedded C
In safety‑critical or real‑time embedded systems the timing of every instruction matters. A single unpredictable pause can cause missed deadlines, corrupted data, or safety violations. One of the main culprits of non‑determinism is the heap – the pool of memory that malloc() and free() use. Fragmentation, variable allocation times, and the possibility of an out‑of‑memory (OOM) crash make dynamic memory a risky choice for deterministic applications.
Why Static Allocation Wins
- Known size at compile time – The linker can place all buffers in a fixed location, and the developer knows exactly how much SRAM will be used.
- No fragmentation – Static objects live for the program’s lifetime, so the memory layout never changes.
- Deterministic execution – No allocation or deallocation overhead, no hidden function calls, and no chance of OOM.
- Compiler‑level optimisations – Read‑only data can be put in Flash/ROM using
const, freeing precious SRAM. - Alignment guarantees – Using compiler attributes (e.g.,
__attribute__((aligned(4)))) ensures bus‑safe data layouts on architectures that require word alignment.
Practical Example: UART Ring Buffer
Suppose you need a 256‑byte receive buffer for a UART peripheral. A dynamic allocation would look like this (not recommended for deterministic code):
uint8_t *rx_buf = malloc(256);
if (!rx_buf) { /* handle error */ }
Instead, declare a static buffer and a const lookup table for parity masks:
/* Place the buffer in SRAM */
static uint8_t uart_rx_buf[256] __attribute__((aligned(4)));
/* Place a read‑only table in Flash */
static const uint8_t parity_lookup[256] = {
/* 0x00 … 0xFF */
0,1,1,0,1,0,0,1,/* … */
};
/* MMIO access to UART status register */
#define UART_STATUS_REG (*(volatile uint32_t *)0x4000C000U)
/* Example ISR using the static buffer */
void UART_IRQHandler(void) {
uint8_t data = *(volatile uint8_t *)0x4000C004U; /* UART data register */
static uint16_t head = 0;
uart_rx_buf[head++] = data;
head &= 0xFF; /* wrap around */
}
Key points:
- All buffers are statically allocated; the compiler knows their size.
- The
consttable lives in Flash, saving SRAM. - The
volatilequalifier protects MMIO accesses from being optimised away. - Alignment attribute prevents bus errors on some MCUs.
Verifying Placement
After linking, inspect the linker map file. Look for sections:
| Section | Expected Content |
|---|---|
| .rodata | const tables like parity_lookup |
| .data | initialised static objects (none here) |
| .bss | zero‑initialised statics like uart_rx_buf |
| .stack | stack space |
| .heap | unused, should be minimal or zero |
Use a debugger to read the memory map or dump uart_rx_buf to confirm it occupies SRAM and not Flash.
Trade‑Offs and When to Reconsider
- Wasted SRAM – If you over‑size static buffers for worst‑case scenarios that never occur, you may starve other parts of the system.
- Limited Flexibility – Static allocation cannot adapt to changing data rates or dynamic workloads.
- Stack Depth – Recursion or large local arrays can still overflow the stack; static allocation does not solve that.
- Volatile Overuse – Declaring a variable
volatilewhen it isn’t tied to hardware can force the compiler to reload it each time, hurting performance.
When the system has enough SRAM and the workload is highly variable, a hybrid approach works: use static allocation for critical buffers, but reserve a small heap for non‑critical, temporary allocations, and guard it with a size check.
Actionable Checklist
- Identify all buffers that must be deterministic. Declare them
staticorexternwith known sizes. - Mark read‑only data
constand place it in Flash by using the linker script or compiler attributes. - Use
volatileonly for memory‑mapped I/O registers. - Align data structures with
__attribute__((aligned(N)))or#pragma packwhere required. - After build, check the .map file to verify section placement.
- Run a static analysis tool to calculate maximum stack depth and ensure it fits within your stack limits.
- Profile the system under realistic workloads; if you notice unused SRAM, consider tightening buffer sizes.
By following these steps you can build an embedded C application that is both memory‑efficient and deterministic, meeting the stringent requirements of safety‑critical and real‑time systems.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.