Memory Management¶
Comprehensive guide to memory management in Nexus embedded applications.
Overview¶
Effective memory management is critical for embedded systems with limited RAM. This guide covers memory allocation strategies, optimization techniques, and debugging tools.
Memory Types:
Stack - Automatic variables, function calls
Heap - Dynamic allocation
Static - Global and static variables
Code - Program instructions (Flash/ROM)
Key Concepts:
Memory layout and sections
Stack and heap management
Memory pools
Memory fragmentation
Memory debugging
Memory Layout¶
Typical ARM Cortex-M Layout¶
┌─────────────────────┐ 0xFFFFFFFF
│ Device Registers │
├─────────────────────┤ 0x40000000
│ External RAM │
├─────────────────────┤ 0x20000000
│ SRAM │
│ ┌───────────────┐ │
│ │ Heap (grows→)│ │
│ ├───────────────┤ │
│ │ Free Space │ │
│ ├───────────────┤ │
│ │ (←grows Stack)│ │
│ ├───────────────┤ │
│ │ .bss (zero) │ │
│ ├───────────────┤ │
│ │ .data (init) │ │
│ └───────────────┘ │
├─────────────────────┤ 0x08000000
│ Flash/ROM │
│ ┌───────────────┐ │
│ │ .text (code) │ │
│ ├───────────────┤ │
│ │ .rodata │ │
│ └───────────────┘ │
└─────────────────────┘ 0x00000000
Memory Sections:
.text- Program code.rodata- Read-only data (const).data- Initialized global/static variables.bss- Zero-initialized global/static variables.heap- Dynamic allocation.stack- Function call stack
Linker Script¶
Example Linker Script (STM32F4):
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 192K
}
SECTIONS
{
.text :
{
*(.isr_vector)
*(.text*)
*(.rodata*)
} > FLASH
.data :
{
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT> FLASH
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE(end = .);
PROVIDE(_heap_start = .);
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
}
Stack Management¶
Stack Allocation¶
FreeRTOS Task Stacks:
/* Create task with specific stack size */
osal_task_config_t config = {
.name = "my_task",
.func = my_task_function,
.arg = NULL,
.stack_size = 1024, /* 1KB stack */
.priority = OSAL_PRIORITY_NORMAL,
};
osal_task_handle_t task;
osal_task_create(&config, &task);
Stack Size Guidelines:
Task Type |
Recommended Size |
|---|---|
Minimal |
256-512 bytes |
Normal |
512-1024 bytes |
Complex |
1024-2048 bytes |
With printf |
+512 bytes |
With FPU |
+128 bytes |
Stack Usage Monitoring¶
Check Stack High Water Mark:
void monitor_stack_usage(void)
{
osal_task_handle_t current = osal_task_get_current();
uint32_t high_water = osal_task_get_stack_high_water(current);
uint32_t stack_size = osal_task_get_stack_size(current);
uint32_t used = stack_size - high_water;
uint32_t percent = (used * 100) / stack_size;
LOG_INFO("Task: %s", osal_task_get_name(current));
LOG_INFO("Stack: %lu/%lu bytes (%lu%% used)",
used, stack_size, percent);
if (percent > 80) {
LOG_WARN("Stack usage high - consider increasing size");
}
}
Stack Overflow Detection:
/* FreeRTOS stack overflow hook */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char* pcTaskName)
{
LOG_FATAL("Stack overflow in task: %s", pcTaskName);
/* Dump task information */
LOG_FATAL("Task handle: %p", xTask);
/* Enter safe state */
__disable_irq();
while (1) {
/* Halt system */
}
}
Stack Canaries:
/* Enable stack protection */
#define STACK_CANARY 0xDEADBEEF
typedef struct {
uint32_t canary_start;
uint8_t stack[1024];
uint32_t canary_end;
} protected_stack_t;
void check_stack_canary(protected_stack_t* stack)
{
if (stack->canary_start != STACK_CANARY ||
stack->canary_end != STACK_CANARY) {
LOG_FATAL("Stack corruption detected!");
}
}
Heap Management¶
Dynamic Allocation¶
OSAL Memory Functions:
/* Allocate memory */
void* ptr = osal_malloc(1024);
if (!ptr) {
LOG_ERROR("Memory allocation failed");
return;
}
/* Use memory */
memset(ptr, 0, 1024);
/* Free memory */
osal_free(ptr);
Aligned Allocation:
/* Allocate aligned memory (for DMA) */
void* aligned_ptr = osal_malloc_aligned(1024, 32);
if (!aligned_ptr) {
LOG_ERROR("Aligned allocation failed");
return;
}
/* Use for DMA */
dma_transfer(aligned_ptr, 1024);
/* Free aligned memory */
osal_free_aligned(aligned_ptr);
Heap Monitoring¶
Check Heap Usage:
void monitor_heap_usage(void)
{
size_t free_heap = osal_get_free_heap_size();
size_t min_free = osal_get_minimum_ever_free_heap_size();
size_t total_heap = configTOTAL_HEAP_SIZE;
size_t used = total_heap - free_heap;
uint32_t percent = (used * 100) / total_heap;
LOG_INFO("Heap Usage:");
LOG_INFO(" Total: %zu bytes", total_heap);
LOG_INFO(" Used: %zu bytes (%lu%%)", used, percent);
LOG_INFO(" Free: %zu bytes", free_heap);
LOG_INFO(" Min Free Ever: %zu bytes", min_free);
if (percent > 90) {
LOG_WARN("Heap usage critical!");
}
if (min_free < 1024) {
LOG_WARN("Heap has been nearly exhausted!");
}
}
Periodic Monitoring:
void heap_monitor_task(void* arg)
{
while (1) {
monitor_heap_usage();
osal_task_delay(10000); /* Check every 10 seconds */
}
}
Memory Pools¶
Fixed-Size Pools¶
Create Memory Pool:
/* Define pool for message structures */
typedef struct {
uint32_t id;
uint8_t data[64];
} message_t;
#define POOL_SIZE 10
osal_pool_handle_t message_pool;
void init_message_pool(void)
{
osal_pool_create(&message_pool, POOL_SIZE, sizeof(message_t));
}
Use Memory Pool:
void send_message(uint32_t id, const uint8_t* data, size_t len)
{
/* Allocate from pool */
message_t* msg = osal_pool_alloc(message_pool);
if (!msg) {
LOG_ERROR("Pool exhausted");
return;
}
/* Fill message */
msg->id = id;
memcpy(msg->data, data, len);
/* Send message */
osal_queue_send(msg_queue, &msg, 0);
}
void receive_message(void)
{
message_t* msg;
/* Receive message */
if (osal_queue_receive(msg_queue, &msg, 1000) == OSAL_OK) {
/* Process message */
process_message(msg);
/* Return to pool */
osal_pool_free(message_pool, msg);
}
}
Pool Statistics:
void print_pool_stats(void)
{
uint32_t available = osal_pool_get_available(message_pool);
uint32_t total = POOL_SIZE;
uint32_t used = total - available;
LOG_INFO("Message Pool:");
LOG_INFO(" Total: %lu", total);
LOG_INFO(" Used: %lu", used);
LOG_INFO(" Available: %lu", available);
}
Static Allocation¶
Compile-Time Allocation¶
Static Buffers:
/* Static allocation - no runtime overhead */
#define MAX_DEVICES 10
static device_t devices[MAX_DEVICES];
static size_t device_count = 0;
device_t* allocate_device(void)
{
if (device_count >= MAX_DEVICES) {
return NULL;
}
return &devices[device_count++];
}
Static RTOS Objects:
/* FreeRTOS static allocation */
#define TASK_STACK_SIZE 1024
static StackType_t task_stack[TASK_STACK_SIZE];
static StaticTask_t task_tcb;
void create_static_task(void)
{
TaskHandle_t task = xTaskCreateStatic(
task_function,
"static_task",
TASK_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
task_stack,
&task_tcb
);
}
Memory Fragmentation¶
Causes and Prevention¶
Fragmentation Example:
Initial: [--------FREE--------]
Alloc A: [AAA-----FREE--------]
Alloc B: [AAABBB--FREE--------]
Alloc C: [AAABBBCC-FREE-------]
Free B: [AAA---CC-FREE-------] ← Fragmentation!
Alloc D: Can't allocate 4 bytes even though 6 bytes free
Prevention Strategies:
Use Memory Pools - Fixed-size allocations
Allocate Once - Allocate at startup, never free
Use Static Allocation - No fragmentation
Allocate in Order - Allocate large blocks first
Avoid Frequent Alloc/Free - Reuse buffers
Example: Buffer Reuse:
/* Bad: Allocate/free repeatedly */
void process_data_bad(void)
{
for (int i = 0; i < 100; i++) {
uint8_t* buffer = malloc(1024);
process_buffer(buffer);
free(buffer); /* Causes fragmentation */
}
}
/* Good: Reuse buffer */
void process_data_good(void)
{
uint8_t* buffer = malloc(1024);
if (!buffer) return;
for (int i = 0; i < 100; i++) {
process_buffer(buffer);
}
free(buffer);
}
Memory Debugging¶
Leak Detection¶
Track Allocations:
#ifdef DEBUG_MEMORY
typedef struct {
void* ptr;
size_t size;
const char* file;
int line;
} alloc_info_t;
#define MAX_ALLOCS 100
static alloc_info_t alloc_table[MAX_ALLOCS];
static size_t alloc_count = 0;
void* debug_malloc(size_t size, const char* file, int line)
{
void* ptr = malloc(size);
if (ptr && alloc_count < MAX_ALLOCS) {
alloc_table[alloc_count].ptr = ptr;
alloc_table[alloc_count].size = size;
alloc_table[alloc_count].file = file;
alloc_table[alloc_count].line = line;
alloc_count++;
}
return ptr;
}
void debug_free(void* ptr)
{
for (size_t i = 0; i < alloc_count; i++) {
if (alloc_table[i].ptr == ptr) {
/* Remove from table */
alloc_table[i] = alloc_table[--alloc_count];
break;
}
}
free(ptr);
}
void check_memory_leaks(void)
{
if (alloc_count > 0) {
LOG_WARN("Memory leaks detected: %zu allocations",
alloc_count);
for (size_t i = 0; i < alloc_count; i++) {
LOG_WARN(" %p (%zu bytes) at %s:%d",
alloc_table[i].ptr,
alloc_table[i].size,
alloc_table[i].file,
alloc_table[i].line);
}
}
}
#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
#define free(ptr) debug_free(ptr)
#endif
Memory Corruption Detection¶
Guard Bytes:
#define GUARD_PATTERN 0xDEADBEEF
typedef struct {
uint32_t guard_start;
uint8_t data[256];
uint32_t guard_end;
} guarded_buffer_t;
void init_guarded_buffer(guarded_buffer_t* buf)
{
buf->guard_start = GUARD_PATTERN;
buf->guard_end = GUARD_PATTERN;
}
bool check_guarded_buffer(guarded_buffer_t* buf)
{
if (buf->guard_start != GUARD_PATTERN) {
LOG_ERROR("Buffer underflow detected!");
return false;
}
if (buf->guard_end != GUARD_PATTERN) {
LOG_ERROR("Buffer overflow detected!");
return false;
}
return true;
}
Memory Fill Patterns:
/* Fill freed memory with pattern */
void debug_free_with_pattern(void* ptr, size_t size)
{
memset(ptr, 0xDD, size); /* 0xDD = "Dead" */
free(ptr);
}
/* Detect use-after-free */
void check_for_use_after_free(void* ptr, size_t size)
{
uint8_t* bytes = (uint8_t*)ptr;
for (size_t i = 0; i < size; i++) {
if (bytes[i] == 0xDD) {
LOG_ERROR("Use-after-free detected at offset %zu", i);
return;
}
}
}
Best Practices¶
Prefer Static Allocation * Deterministic behavior * No fragmentation * Faster allocation * Suitable for safety-critical systems
Use Memory Pools * Fixed-size allocations * Fast allocation/deallocation * No fragmentation * Predictable behavior
Minimize Dynamic Allocation * Allocate at startup * Reuse buffers * Avoid frequent alloc/free * Use stack when possible
Monitor Memory Usage * Track stack high water marks * Monitor heap usage * Check for leaks * Set up alerts
Handle Allocation Failures * Always check return values * Have fallback strategies * Log failures * Graceful degradation
Align Data Properly * Use aligned allocations for DMA * Pack structures carefully * Consider cache line alignment * Use compiler attributes
Test Memory Limits * Test with low memory * Test allocation failures * Test fragmentation scenarios * Use memory debugging tools
Memory Optimization Checklist¶
Design Phase:
[ ] Estimate memory requirements
[ ] Choose allocation strategy
[ ] Plan for worst-case usage
[ ] Design for determinism
Implementation Phase:
[ ] Use appropriate allocation method
[ ] Check all allocation failures
[ ] Free all allocated memory
[ ] Avoid memory leaks
Testing Phase:
[ ] Test with memory limits
[ ] Monitor memory usage
[ ] Check for leaks
[ ] Verify stack sizes
Optimization Phase:
[ ] Reduce dynamic allocation
[ ] Optimize data structures
[ ] Pack structures
[ ] Use memory pools
See Also¶
Performance Optimization - Performance Optimization
Debugging Guide - Debugging Guide
Best Practices - Best Practices
Debugging Guide - Development Debugging