Skip to content
Home » Embedded Systems » Embedded C » Memory Management Strategies for RTOS Applications

Memory Management Strategies for RTOS Applications

RTOS for Embedded Systems
Part 3 of 10View Full Path →

KEY TAKEAWAYS

  • Standard malloc/free is dangerous in RTOS because it’s non-deterministic, causes fragmentation, and can fail unpredictably at runtime.
  • Fixed-size memory pools provide O(1) allocation with zero fragmentation — the safest choice for real-time systems.
  • The safest strategy is to allocate all memory at startup and never allocate dynamically during runtime operation.

Part of the Complete Guide to RTOS for Embedded Systems series.

Memory management in RTOS-based embedded systems requires different strategies than desktop or server applications. You’re working with kilobytes (not gigabytes), fragmentation can be fatal, and allocation must be deterministic. This article covers practical memory management approaches from simplest to most sophisticated.

Why malloc/free Is Dangerous in Real-Time Systems

The standard C library malloc() has three problems in RTOS applications:

Non-deterministic timing — malloc searches a free list, potentially traversing hundreds of entries. Worst-case execution time can be orders of magnitude longer than typical case, which violates real-time guarantees.

Fragmentation — over time, allocating and freeing different-sized blocks creates gaps in memory that are individually too small to use but collectively waste significant RAM. In a system with 32 KB of RAM, fragmentation can make 10 KB unusable.

No failure recovery — when malloc returns NULL, most embedded code has no meaningful recovery path. The system is effectively dead.

These problems may not appear during testing but surface hours, days, or months into deployment — making them extremely dangerous. A medical device, automotive ECU, or industrial controller that crashes due to heap fragmentation after 3 weeks of operation is a serious safety issue.

For these reasons, many safety standards (MISRA C, DO-178C, IEC 62304) restrict or prohibit dynamic memory allocation after initialization.

Static Allocation: The Simplest Solution

The simplest and safest approach: allocate everything at compile time using static variables and arrays.

// All buffers are static — no runtime allocation needed
static uint8_t uart_rx_buffer[256];
static uint8_t uart_tx_buffer[256];
static SensorReading_t sensor_buffer[32];
static LogEntry_t log_buffer[64];

// RTOS objects also statically allocated static StaticTask_t sensor_tcb; static StackType_t sensor_stack[512]; static StaticQueue_t sensor_queue_struct; static uint8_t sensor_queue_storage[16 * sizeof(SensorReading_t)];

void system_init(void) { xTaskCreateStatic(vSensorTask, “Sensor”, 512, NULL, 3, sensor_stack, &sensor_tcb); xQueueCreateStatic(16, sizeof(SensorReading_t), sensor_queue_storage, &sensor_queue_struct); } “`

Benefits:

  • Linker catches all memory issues at build time — if you exceed RAM, you know immediately
  • Zero runtime allocation failures possible
  • Zero fragmentation
  • Fully deterministic
  • Easy to analyze memory usage from the linker map file

The only downside is inflexibility — you must know maximum sizes at design time. But for most embedded systems, you do know these bounds.

Fixed-Size Memory Pools

When you do need runtime allocation (e.g., for network packet buffers, dynamic message creation), fixed-size pools are the best approach.

A memory pool pre-allocates N blocks of identical size. Allocation pops a block from the free list (O(1)). Deallocation pushes it back (O(1)). There is zero fragmentation because all blocks are the same size.

// Generic pool implementation
typedef struct MemPool {
    uint8_t *storage;          // Backing memory
    void **free_stack;         // Stack of free block pointers
    size_t block_size;
    size_t capacity;
    size_t free_count;
    SemaphoreHandle_t mutex;
    SemaphoreHandle_t available;  // Counting semaphore for blocking alloc
} MemPool_t;

void pool_init(MemPool_t *pool, void *memory, size_t block_size, size_t count) { pool->storage = (uint8_t *)memory; pool->block_size = block_size; pool->capacity = count; pool->free_count = count; pool->mutex = xSemaphoreCreateMutex(); pool->available = xSemaphoreCreateCounting(count, count); pool->free_stack = pvPortMalloc(count * sizeof(void *)); for (size_t i = 0; i free_stack[i] = &pool->storage[i * block_size]; } }

void *pool_alloc(MemPool_t *pool, TickType_t timeout) { // Block until a block is available (or timeout) if (xSemaphoreTake(pool->available, timeout) != pdTRUE) { return NULL; // Timeout — no blocks available } xSemaphoreTake(pool->mutex, portMAX_DELAY); void *block = pool->free_stack[–pool->free_count]; xSemaphoreGive(pool->mutex); return block; }

void pool_free(MemPool_t *pool, void *block) { xSemaphoreTake(pool->mutex, portMAX_DELAY); pool->free_stack[pool->free_count++] = block; xSemaphoreGive(pool->mutex); xSemaphoreGive(pool->available); // Signal that a block is available } “`

The counting semaphore (pool->available) lets tasks block-wait for a free buffer instead of busy-polling, which integrates cleanly with RTOS scheduling.

Stack Sizing and Monitoring

Each RTOS task has its own stack, and sizing it correctly is critical. Too small: stack overflow corrupts memory. Too large: wasted RAM.

Stack usage depends on:

  • Local variables in the task function and all functions it calls
  • Function call depth (each call pushes a frame: return address + saved registers + locals)
  • Interrupt preemption — on ARM Cortex-M, interrupts use the MSP (main stack pointer), not the task’s PSP, so ISR stack depth doesn’t affect task stacks. But on some architectures, ISRs use the current task’s stack.

Measurement approach:

// During development, periodically check stack usage
void vMonitorTask(void *pvParameters) {
    TaskHandle_t tasks[] = {xSensorHandle, xCommHandle, xDisplayHandle};
    const char *names[] = {"Sensor", "Comm", "Display"};

for (;;) { for (int i = 0; i < 3; i++) { UBaseType_t hwm = uxTaskGetStackHighWaterMark(tasks[i]); printf("%s stack free: %u wordsn", names[i], hwm); if (hwm < 50) { printf("WARNING: %s stack nearly full!n", names[i]); } } vTaskDelay(pdMS_TO_TICKS(5000)); } } “`

High-water mark returns the minimum free stack space (in words) since the task started. Run all code paths — normal operation, error handling, worst-case input — before finalizing stack sizes.

For production, enable FreeRTOS stack overflow hook:

// In FreeRTOSConfig.h
#define configCHECK_FOR_STACK_OVERFLOW 2

// Implement the hook void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { // Log the task name and halt — do NOT try to recover log_fatal(“Stack overflow in task: %s”, pcTaskName); while (1); // Halt for debugging } “`

📖 Related: Dynamic Memory Allocation in C

Leave a Reply

Your email address will not be published. Required fields are marked *