Static vs Dynamic Memory in Embedded C: A UART Driver Case Study
Choosing the right memory allocation strategy prevents data loss in UART drivers. Static allocation guarantees deterministic behavior, while dynamic allocation introduces unpredictability.
10 Jul 2025, 06:59 UTC

Problem Context: Data Loss in UART Drivers
On many microcontrollers, a UART peripheral is the primary conduit between the device and the outside world. The driver usually receives data in an interrupt service routine (ISR) and stores it in a buffer before the main loop processes it. If that buffer runs out of space, incoming bytes are lost, and the application may hang or behave unpredictably. The root cause often lies in how the buffer is allocated and managed.
Why Memory Allocation Matters
Embedded systems typically run a single thread (or a small set of cooperative threads) and have a very limited RAM pool. Memory allocation strategies determine:
- Determinism – Can the driver guarantee that it always has space when data arrives?
- Fragmentation – Does the allocation leave unusable gaps over time?
- Latency – How long does it take to obtain or free memory during an ISR?
Static Allocation Strategy
Deterministic Memory Usage
Static allocation reserves a fixed block of RAM at compile time. The compiler places it in a known section, such as .bss or .data, and the linker map shows exactly how much RAM is used. Once firmware is flashed, the size cannot change. This predictability is a cornerstone of real‑time safety‑critical systems where any variation in memory usage can violate timing constraints.
Ring Buffer Example
Below is a ring‑buffer implemented with static arrays that can be used in a UART ISR. The buffer size is a compile‑time constant; adjust UART_RX_BUF_SIZE to match your throughput needs.
#include <stdint.h>
#include <stdbool.h>
/* Size must be a power of two for efficient modulo */
#define UART_RX_BUF_SIZE 256
/* Static buffer */
static uint8_t uart_rx_buf[UART_RX_BUF_SIZE];
static volatile uint16_t uart_rx_head = 0;
static volatile uint16_t uart_tail = 0;
/* ISR called when a byte arrives */
void UART_IRQHandler(void)
{
uint8_t byte = UART_ReadByte(); /* Platform‑specific */
uint16_t next = (uart_rx_head + 1) & (UART_RX_BUF_SIZE - 1);
if (next != uart_tail) {
uart_rx_buf[uart_rx_head] = byte;
uart_rx_head = next;
} else {
/* Handle overflow */
uart_overflow_flag = true;
}
}
/* Main loop reads data */
bool UART_Read(uint8_t *byte)
{
if (uart_rx_head == uart_tail) return false; /* Empty */
*byte = uart_rx_buf[uart_tail];
uart_tail = (uart_tail + 1) & (UART_RX_BUF_SIZE - 1);
return true;
}
Key points:
- The buffer is static; its size appears in the linker map, making RAM usage transparent.
- Head and tail indices are
volatileto prevent compiler optimizations. - Modulo is achieved by masking with
UART_RX_BUF_SIZE - 1; this works only when the size is a power of two. - Overflow is detected and can be handled by a flag or by dropping the oldest data.
Dynamic Allocation Pitfalls
Unpredictable Latency
Using malloc and free in an ISR is discouraged because the heap allocator may perform memory copying, lock acquisition, or fragmentation compaction. These operations can take a variable amount of time, breaking the real‑time guarantee that an interrupt must finish in a bounded period.
Fragmentation and Leaks
Dynamic memory can fragment over time, especially on systems that allocate and free buffers of varying sizes. A memory leak in a driver that never frees a buffer will eventually exhaust the heap, causing a hard failure or a silent data loss.
Thread‑Safety Concerns
Many vendor toolchains provide a non‑thread‑safe allocator. If the application uses an RTOS with multiple tasks that may call malloc concurrently, you must supply a custom allocator or guard the calls with a mutex, adding overhead and complexity.
Trade‑off Analysis
| Aspect | Static Allocation | Dynamic Allocation |
|---|---|---|
| Determinism | Guaranteed, no runtime overhead | Variable latency, potential blocking |
| Memory Footprint | Fixed, may waste unused space | Only uses what you request, but risk of fragmentation |
| Complexity | Simple, no allocator code | Requires allocator integration |
| Safety | High for safety‑critical code | Higher risk of leaks and corruption |
Rule of thumb: Reserve static buffers for time‑critical or high‑frequency peripherals such as UART, SPI, or ADC. Use dynamic allocation only for optional features with bounded lifetime, like a temporary log buffer or a command parser that can be cleared after use.
Actionable Closing
When evaluating your own project:
- Profile the expected data rate. Compute the maximum burst size and choose a static buffer that can hold it.
- Check the linker map. Verify the static buffer size and ensure it fits within your RAM budget.
- Run a stress test. Continuously transmit data at the worst‑case rate and confirm no overruns or heap corruption.
- Measure ISR latency. Compare a static ISR to a dynamic one; the static version should finish in a few cycles regardless of data volume.
- Document the decision and keep a copy of the map for future audits.
By favoring static allocation for UART drivers, you gain deterministic behavior, avoid fragmentation, and reduce the risk of silent data loss, which is critical for safety‑critical or real‑time embedded applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.