Chapter 16: Memory Management in FreeRTOS

Chapter Objectives

  • Understand the difference between stack and heap memory.
  • Learn the purpose and risks of dynamic memory allocation.
  • Become familiar with the different FreeRTOS heap allocation schemes (heap_1 through heap_5).
  • Understand the ESP-IDF capability-based heap allocator (heap_caps).
  • Learn how to allocate and free memory using heap_caps_malloc() and heap_caps_free().
  • Understand how to query available heap memory and diagnose fragmentation.
  • Identify common memory management pitfalls like leaks, fragmentation, and dangling pointers.
  • Implement safe dynamic memory allocation practices in ESP32 applications.

Introduction

In previous chapters, we primarily dealt with variables whose size was known at compile time. These are typically allocated on the stack (for local variables within functions) or in the .data/.bss sections (for global/static variables). However, many real-world applications require dynamic memory allocation – the ability to request and release blocks of memory at runtime, when the exact size or lifetime of the data isn’t known beforehand. This dynamic allocation occurs in a memory region called the heap.

Consider needing to buffer incoming network data, create data structures whose size depends on user input, or manage objects whose lifetime extends beyond a single function call. These scenarios necessitate dynamic memory allocation.

While powerful, dynamic memory management is a double-edged sword, especially in resource-constrained embedded systems like the ESP32. Improper handling can lead to critical issues like memory leaks (gradually consuming all available memory), fragmentation (breaking the heap into small, unusable pieces), and system instability. FreeRTOS provides mechanisms for managing heap memory, and ESP-IDF builds upon this with a sophisticated capability-based system tailored for the ESP32’s diverse memory types. This chapter delves into the theory and practice of heap memory management in FreeRTOS and ESP-IDF.

Theory

Stack vs. Heap Memory

It’s crucial to distinguish between the stack and the heap:

Feature Stack Memory Heap Memory
Primary Use Local variables within functions, function parameters, return addresses, task context. Dynamic memory allocation at runtime (e.g., buffers, complex data structures whose size isn’t known at compile time).
Allocation/Deallocation Automatic (managed by compiler/CPU). Memory is allocated on function entry and deallocated on exit (LIFO). Manual (managed by programmer via functions like malloc(), free(), heap_caps_malloc(), heap_caps_free()).
Speed Very fast allocation and deallocation (typically involves adjusting a stack pointer). Slower allocation/deallocation due to searching for free blocks, metadata management, etc.
Size Management Size is typically fixed per task, determined at compile/link time (e.g., CONFIG_FREERTOS_DEFAULT_TASK_STACK_SIZE). Size is determined by the total available RAM not used by code, static data, or stacks. Can be a large, shared pool.
Lifetime Limited to the scope of the function or block where variables are defined. For tasks, stack exists for task lifetime. Memory block persists until explicitly freed by the programmer, regardless of function scope.
Flexibility Less flexible; size must be known at compile time. Highly flexible; memory can be allocated and freed as needed at runtime.
Key Risks Stack Overflow (allocating too much memory on the stack, deep recursion). Memory Leaks (forgetting to free), Fragmentation (heap broken into unusable small pieces), Allocation Failures (no suitable block found), Dangling Pointers (use after free), Double Free.
Data Structure LIFO (Last-In, First-Out). A pool of memory, often managed with linked lists of free/allocated blocks.
  1. Stack:
    • Used for static memory allocation (local variables, function parameters, return addresses).
    • Managed automatically by the compiler/CPU. Memory is allocated when entering a function scope and deallocated upon exit (LIFO – Last-In, First-Out).
    • Fast allocation/deallocation.
    • Size is typically fixed per task and determined at compile/link time (CONFIG_FREERTOS_DEFAULT_TASK_STACK_SIZE).
    • Risk: Stack Overflow if function calls nest too deeply or local variables are too large.
  2. Heap:
    • Used for dynamic memory allocation (requested at runtime).
    • Managed explicitly by the programmer (or libraries) using functions like malloc() and free().
    • Allocation/deallocation can be slower than stack operations.
    • Size is determined by the available RAM not used by code, static data, or stacks.
    • Risks: Memory Leaks, Fragmentation, Allocation Failures.

Dynamic Memory Allocation

The core idea is simple:

  1. Allocation: When you need a block of memory at runtime, you call an allocation function (like malloc or heap_caps_malloc), specifying the desired size. The memory manager searches the heap for a suitable free block and returns a pointer to its start.
  2. Usage: You use the returned pointer to access and modify the allocated memory block.
  3. Deallocation (Freeing): When you are finished with the memory block, you must call a deallocation function (like free or heap_caps_free), passing the original pointer. This marks the block as free, allowing the memory manager to reuse it for future allocations.

Failure to free memory results in a memory leak. The memory remains marked as “in use” even though the program no longer needs it, eventually exhausting the heap.

graph TD
    A[Start: Need Memory at Runtime] --> B{"Call Allocation Function<br>(e.g., heap_caps_malloc(size, caps))"};
    
    subgraph "Memory Manager Interaction"
        B --> C{Search for suitable<br>free block in Heap};
        C -- Found --> D[Return Pointer to<br>Allocated Block];
        C -- Not Found / Error --> E[Return NULL];
    end

    D --> F[Application Uses Memory via Pointer];
    F --> G{Finished with Memory?};
    
    G -- Yes --> H{"Call Deallocation Function<br>(e.g., heap_caps_free(pointer))"};
    H --> I[Memory Manager Marks<br>Block as Free in Heap];
    I --> J[Memory Can Be Reused];
    
    G -- No / Still in Use --> F;

    E --> K[Application Handles<br>Allocation Failure Gracefully];

    A --> K_Direct_Error{Initial Check<br>e.g. size = 0}
    K_Direct_Error -- Invalid Request --> E

    %% Styling
    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef successNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class A,J startNode;
    class B,C,D,F,H,I processNode;
    class G,K_Direct_Error decisionNode;
    class E,K errorNode;

FreeRTOS Heap Management Schemes

FreeRTOS itself doesn’t mandate a specific heap implementation; instead, it provides several sample implementations (heap schemes) that developers can choose based on their application’s needs. These are typically named heap_x.c. ESP-IDF integrates and configures one of these by default.

  1. heap_1.c (Simplest):
    • Allocates only; does not implement free().
    • Only suitable for applications that allocate all memory at startup and never free it.
    • Very deterministic, no fragmentation. Rarely used in practice for complex applications.
  2. heap_2.c (Basic Allocation/Freeing):
    • Implements malloc() and free().
    • Uses a best-fit algorithm.
    • Does not coalesce (merge) adjacent free blocks.
    • Prone to fragmentation over time. Now largely considered legacy.
  3. heap_3.c (Wrapper for Standard Library):
    • Simply wraps the standard C library malloc() and free().
    • Makes these functions thread-safe by temporarily suspending the FreeRTOS scheduler.
    • Behavior (fragmentation, performance) depends entirely on the underlying C library’s implementation. Often not the most memory-efficient for embedded systems.
  4. heap_4.c (Coalescing Allocator):
    • Implements malloc() and free().
    • Uses a first-fit algorithm.
    • Merges (coalesces) adjacent free blocks when free() is called. This significantly reduces external fragmentation compared to heap_2.
    • Not fully deterministic (allocation time can vary slightly).
    • This is the default heap implementation used by ESP-IDF.
  5. heap_5.c (Multiple Memory Regions):
    • Builds upon heap_4.
    • Allows the heap to span multiple, non-adjacent memory regions.
    • Requires initialization (vPortDefineHeapRegions()) before use.
    • Useful for systems with complex memory maps. ESP-IDF uses its own capability system (heap_caps) instead of directly using heap_5‘s multi-region API, but the underlying principles are related.

ESP-IDF configures heap_4 by default, providing a good balance between functionality (allocation/freeing) and fragmentation resistance due to its coalescing feature.

Scheme File Key Features malloc() / free() Fragmentation Handling Determinism Common Use / Notes
heap_1.c Simplest, allocation only. Allocates only, no free(). No fragmentation (as memory is never freed). Very deterministic. Rarely used. Suitable for systems that allocate all memory once at startup and never release it.
heap_2.c Basic allocation/freeing, best-fit algorithm. Implements malloc() and free(). Does not coalesce adjacent free blocks. Prone to fragmentation. Less deterministic than heap_1. Largely considered legacy due to fragmentation issues. Not recommended for new designs.
heap_3.c Wrapper for standard C library malloc()/free(). Uses C library’s malloc()/free(), made thread-safe by suspending scheduler. Depends on the C library’s implementation. Depends on C library. Behavior is tied to the toolchain’s C library. May not be most efficient for embedded systems.
heap_4.c Coalescing allocator, first-fit algorithm. Implements malloc() and free(). Merges (coalesces) adjacent free blocks when free() is called, reducing external fragmentation. Not fully deterministic (allocation time can vary slightly). Default for ESP-IDF. Good balance of features and fragmentation resistance for many embedded applications.
heap_5.c Builds on heap_4, allows heap to span multiple non-adjacent memory regions. Implements malloc() and free() across defined regions. Coalesces free blocks within each region. Similar to heap_4 within regions. Requires initialization (vPortDefineHeapRegions()). Useful for complex memory maps. ESP-IDF uses its own heap_caps system for multi-region/type support, which is built upon heap_4 principles.

ESP-IDF Heap Capabilities (heap_caps)

The ESP32 family features various types of memory with different characteristics and access capabilities (e.g., some memory might be executable, some might be suitable for DMA – Direct Memory Access). Standard malloc() doesn’t understand these nuances.

ESP-IDF introduces a capability-based heap allocator built on top of the underlying FreeRTOS heap (heap_4). This allows developers to request memory with specific attributes.

Key Memory Types on ESP32 (Varies by specific chip):

Memory Type Abbreviation / Common Name Key Characteristics & Use Cases Typical Location
Data RAM DRAM General-purpose RAM for data storage (variables, buffers, task stacks, heap). Usually the largest block of internal RAM. Accessed via data bus. Internal
Instruction RAM IRAM RAM primarily for executing performance-critical code. Often faster access for instructions. Can sometimes be used for data if needed (e.g., via MALLOC_CAP_EXEC or specific linker scripts). Internal
RTC FAST Memory RTC_FAST_MEM Small amount of RAM powered by the Real-Time Clock (RTC) domain. Retained during deep sleep. Accessible by the main CPU(s). Used for ULP coprocessor code and data, or data that needs to persist across deep sleep cycles for the main CPU. Internal (RTC Domain)
RTC SLOW Memory RTC_SLOW_MEM Small amount of RAM powered by the RTC domain. Retained during deep sleep. Primarily accessible by the ULP co-processor; main CPU access might be limited or require special handling. Internal (RTC Domain)
Pseudo-Static RAM PSRAM / SPIRAM External RAM connected via an SPI interface. Significantly increases available memory but has higher latency and lower bandwidth compared to internal RAM. Requires specific ESP32 modules (e.g., ESP32-WROVER) or external chip. External

Note: Availability and sizes of these memory types vary by specific ESP32 chip model (e.g., ESP32, ESP32-S2, ESP32-S3, ESP32-C3, etc.). Always consult the datasheet for your specific chip.

Heap Capability Flags:

The heap_caps API uses flags (defined in esp_heap_caps.h) to specify desired memory properties. Common flags include:

Capability Flag (esp_heap_caps.h) Description & Common Use
MALLOC_CAP_DEFAULT Requests general-purpose memory, typically internal DRAM. This is the most common flag and is often the default behavior of standard malloc() if it’s redirected to heap_caps_malloc(). Suitable for most data allocations.
MALLOC_CAP_INTERNAL Requests memory physically located on the ESP32 chip (e.g., DRAM, IRAM). Excludes external memory like SPIRAM. Useful when low latency is critical or when external RAM is not available/desired.
MALLOC_CAP_SPIRAM Requests memory from external SPI RAM (PSRAM). Only works if PSRAM is available and enabled in menuconfig. Used for allocating large buffers that don’t fit in internal RAM.
MALLOC_CAP_DMA Requests memory suitable for Direct Memory Access (DMA) operations. DMA controllers often have specific requirements for memory regions (e.g., must be in internal RAM, specific address ranges). Use this for buffers passed to DMA peripherals (SPI, I2S, ADC DMA, etc.).
MALLOC_CAP_EXEC Requests memory suitable for holding executable code. Typically allocates from IRAM. Useful for dynamically loading code or placing performance-critical functions in IRAM.
MALLOC_CAP_IRAM_8BIT (Specific to some chips like ESP32, ESP32-S3) Requests memory in IRAM that can be byte-accessed. Useful if IRAM is used for data that needs byte-wise manipulation. Often combined with MALLOC_CAP_INTERNAL.
MALLOC_CAP_32BIT Requests memory that can be accessed as 32-bit aligned words. Most memory regions satisfy this. Can be combined with other flags to ensure alignment if necessary.
MALLOC_CAP_8BIT Requests memory that allows 8-bit (byte-wise) access. Most general-purpose RAM (DRAM, SPIRAM) supports this.
MALLOC_CAP_RTCRAM Requests memory from the RTC domain (RTC FAST or RTC SLOW memory). This memory is retained during deep sleep. Useful for data that needs to persist across deep sleep cycles or for ULP coprocessor programs.

Note: Flags can be combined using the bitwise OR operator (|) to request memory meeting multiple criteria, e.g., MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA. The allocator will try to find memory satisfying all specified capabilities.

You can combine flags using the bitwise OR operator (|). For example, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA requests internal memory suitable for DMA.

Why Use heap_caps_malloc()?

While standard malloc() often works (as it’s typically mapped to heap_caps_malloc(size, MALLOC_CAP_DEFAULT)), it’s highly recommended to use the heap_caps_... functions directly in ESP-IDF:

  1. Explicitness: Makes memory requirements clear (e.g., needing DMA-capable memory).
  2. Flexibility: Allows allocation from specific memory types like SPIRAM or IRAM when needed.
  3. Correctness: Ensures memory allocated for specific hardware (like DMA) meets its requirements.

The Core heap_caps Functions:

  • void* heap_caps_malloc(size_t size, uint32_t caps);
    • Allocates size bytes of memory matching the specified caps.
    • Returns a pointer to the allocated block, or NULL if allocation fails.
  • void heap_caps_free(void* ptr);
    • Frees the memory block pointed to by ptr (which must have been allocated by a heap_caps function).
  • void* heap_caps_realloc(void* ptr, size_t size, uint32_t caps);
    • Resizes a previously allocated block (ptr) to size bytes, potentially moving it to satisfy caps. Returns the new pointer or NULL.
  • void* heap_caps_calloc(size_t n, size_t size, uint32_t caps);
    • Allocates memory for an array of n elements of size bytes each, initializes the memory to zero, and ensures it meets caps. Returns a pointer or NULL.

Tip: Always check the return value of allocation functions (heap_caps_malloc, heap_caps_calloc, heap_caps_realloc). A NULL return indicates allocation failure, which your application must handle gracefully.

Memory Fragmentation

Fragmentation occurs when the free memory in the heap is broken into many small, non-contiguous blocks. Even if the total free memory is large, you might be unable to allocate a single large block if no contiguous free block of that size exists.

  • Internal Fragmentation: Occurs within an allocated block when the block allocated is larger than the requested size (due to alignment or allocator overhead). This space is wasted until the block is freed.
  • External Fragmentation: Occurs when free memory exists but is divided into small chunks separated by allocated blocks. heap_4‘s coalescing helps reduce this by merging adjacent free blocks, but it can still occur with certain allocation/deallocation patterns.
External Memory Fragmentation 1. Heap State After Some Allocations & Frees Block A (100KB) Free (80KB) Block B (120KB) Free (70KB) Block C (130KB) Total Free Memory: 80KB + 70KB = 150KB 2. Attempt to Allocate a Large Block Request: Allocate 100KB 3. Allocation Fails! Block A Free (80KB) Block B Free (70KB) Block C Too Small Too Small Reason: No single contiguous free block is >= 100KB. Largest free block is 80KB. Total free is 150KB.

Fragmentation is a major concern in long-running embedded systems. Excessive fragmentation can lead to allocation failures and system instability.

Interactive Heap Fragmentation Demo

Total Heap Size: 1000 KB

Total Free: 1000 KB

Largest Contiguous Free: 1000 KB

Initial heap state. Try allocating some blocks.

Checking Heap Status

ESP-IDF provides functions (in esp_heap_caps.h) to inspect the heap state:

Function Name (from esp_heap_caps.h) Description Key Parameter(s) Return Value
size_t heap_caps_get_free_size(uint32_t caps); Returns the total amount of free memory (in bytes) that matches the specified capabilities. caps: Heap capability flags (e.g., MALLOC_CAP_DEFAULT, MALLOC_CAP_SPIRAM). size_t: Total free bytes for the given capabilities.
size_t heap_caps_get_largest_free_block(uint32_t caps); Returns the size (in bytes) of the largest contiguous free block matching the specified capabilities. Crucial for understanding if a large allocation can succeed, especially with fragmentation. caps: Heap capability flags. size_t: Size of the largest free contiguous block.
size_t heap_caps_get_minimum_free_size(uint32_t caps); Returns the minimum free memory (watermark in bytes) ever recorded for the given capabilities since the system booted. Very useful for detecting gradual memory leaks. caps: Heap capability flags. size_t: Minimum free bytes (watermark).
void heap_caps_print_heap_info(uint32_t caps); Prints detailed information about all heap regions matching the specified capabilities to the console. This includes allocated blocks, free blocks, watermarks, etc. Very useful for in-depth debugging of heap issues. caps: Heap capability flags. void (prints to console).
void heap_caps_dump_all(void); Prints detailed information about ALL heap regions, regardless of capabilities, to the console. More comprehensive than heap_caps_print_heap_info if you need a full system view. None. void (prints to console).
size_t heap_caps_get_total_size(uint32_t caps); Returns the total size (allocated and free) of memory matching the specified capabilities. caps: Heap capability flags. size_t: Total size of memory for the given capabilities.
esp_get_free_heap_size();
(from esp_system.h)
Legacy/Simple API. Equivalent to heap_caps_get_free_size(MALLOC_CAP_DEFAULT). Returns total free heap size for default memory (usually internal DRAM). None. size_t: Total free bytes in the default heap.

Regularly monitoring the total free heap and the largest free block can help detect leaks and fragmentation issues early.

Practical Examples

Project Setup:

Follow the same project setup steps as in Chapter 15 (create a copy of hello_world, open in VS Code).

Common Includes and Setup:

Add/ensure these includes are at the top of your main/your_main_file.c:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_heap_caps.h" // For heap capabilities functions
#include "esp_log.h"
#include "esp_system.h" // For esp_get_free_heap_size (optional)

static const char *TAG = "HEAP_EXAMPLE";

Example 1: Basic Allocation and Freeing

This example allocates a small block of default memory, uses it, and frees it, checking heap size along the way.

C
void app_main(void) {
    ESP_LOGI(TAG, "Starting Basic Heap Example...");

    size_t initial_free_heap = esp_get_free_heap_size(); // Or heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
    ESP_LOGI(TAG, "Initial free heap size: %u bytes", initial_free_heap);

    size_t alloc_size = 100;
    char *my_buffer = NULL;

    ESP_LOGI(TAG, "Attempting to allocate %u bytes...", alloc_size);

    // Allocate memory from the default heap (usually internal DRAM)
    my_buffer = (char *)heap_caps_malloc(alloc_size, MALLOC_CAP_DEFAULT);

    // --- CRITICAL: ALWAYS check the return value! ---
    if (my_buffer == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory!");
        // Handle error appropriately (e.g., retry, abort, log)
    } else {
        ESP_LOGI(TAG, "Memory allocated successfully at address: %p", my_buffer);

        size_t free_heap_after_alloc = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
        ESP_LOGI(TAG, "Free heap after allocation: %u bytes", free_heap_after_alloc);
        ESP_LOGI(TAG, "Largest free block after allocation: %u bytes",
                 heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));

        // Use the allocated memory (example: fill with data)
        snprintf(my_buffer, alloc_size, "Hello from allocated memory!");
        ESP_LOGI(TAG, "Buffer content: '%s'", my_buffer);

        // --- CRITICAL: Free the memory when done ---
        ESP_LOGI(TAG, "Freeing allocated memory...");
        heap_caps_free(my_buffer);
        my_buffer = NULL; // Good practice: nullify pointer after freeing

        size_t free_heap_after_free = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
        ESP_LOGI(TAG, "Free heap after freeing: %u bytes", free_heap_after_free);

        // Check if the heap size returned roughly to its initial state
        // Note: Small differences due to heap metadata overhead are normal.
        if (free_heap_after_free >= initial_free_heap - 64 && free_heap_after_free <= initial_free_heap + 64) {
             ESP_LOGI(TAG, "Heap size returned to near initial value.");
        } else {
             ESP_LOGW(TAG, "Heap size significantly different after free (Initial: %u, Final: %u)", initial_free_heap, free_heap_after_free);
        }
    }

    ESP_LOGI(TAG, "Basic Heap Example Finished.");

    // Keep main task running or delete it if appropriate
    vTaskDelay(pdMS_TO_TICKS(5000)); // Delay before potentially exiting/looping
}

Build, Flash, and Monitor:

Follow standard VS Code ESP-IDF procedures.

Expected Output:

Logs showing the initial free heap, allocation attempt, heap size after allocation, usage of the buffer, freeing action, and final heap size (which should be close to the initial size). Addresses and exact sizes will vary.

Plaintext
I (XXX) HEAP_EXAMPLE: Starting Basic Heap Example...
I (XXX) HEAP_EXAMPLE: Initial free heap size: 250000 bytes  <-- Example value
I (XXX) HEAP_EXAMPLE: Attempting to allocate 100 bytes...
I (XXX) HEAP_EXAMPLE: Memory allocated successfully at address: 0x3ffeXXXX
I (XXX) HEAP_EXAMPLE: Free heap after allocation: 249880 bytes <-- Slightly less than initial
I (XXX) HEAP_EXAMPLE: Largest free block after allocation: YYYYY bytes
I (XXX) HEAP_EXAMPLE: Buffer content: 'Hello from allocated memory!'
I (XXX) HEAP_EXAMPLE: Freeing allocated memory...
I (XXX) HEAP_EXAMPLE: Free heap after freeing: 250000 bytes <-- Back near initial
I (XXX) HEAP_EXAMPLE: Heap size returned to near initial value.
I (XXX) HEAP_EXAMPLE: Basic Heap Example Finished.

Example 2: Allocating DMA-Capable Memory

This example specifically requests memory suitable for DMA operations.

C
#define DMA_BUFFER_SIZE 1024

void app_main(void) {
    ESP_LOGI(TAG, "Starting DMA Heap Example...");

    uint8_t *dma_buffer = NULL;

    ESP_LOGI(TAG, "Attempting to allocate %d bytes of DMA-capable memory...", DMA_BUFFER_SIZE);

    // Allocate memory suitable for DMA operations from internal RAM
    dma_buffer = (uint8_t *)heap_caps_malloc(DMA_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
    // Note: MALLOC_CAP_INTERNAL might be implicitly included by MALLOC_CAP_DMA on some targets,
    // but explicitly requesting it can sometimes be necessary or clearer.

    if (dma_buffer == NULL) {
        ESP_LOGE(TAG, "Failed to allocate DMA-capable memory!");
        // Check if DMA-capable memory is available on this chip/configuration.
        // Maybe try MALLOC_CAP_DEFAULT as a fallback if DMA isn't strictly required?
    } else {
        ESP_LOGI(TAG, "DMA-capable memory allocated successfully at address: %p", dma_buffer);

        // You would now typically pass this buffer to a DMA peripheral driver
        // (e.g., for SPI transfers, I2S audio, etc.)
        ESP_LOGI(TAG, "Simulating usage of DMA buffer...");
        for(int i = 0; i < DMA_BUFFER_SIZE; i++) {
            dma_buffer[i] = i % 256;
        }
        ESP_LOGI(TAG, "DMA buffer filled (simulation complete).");


        ESP_LOGI(TAG, "Freeing DMA buffer...");
        heap_caps_free(dma_buffer);
        dma_buffer = NULL;

        ESP_LOGI(TAG, "DMA buffer freed.");
    }

    ESP_LOGI(TAG, "DMA Heap Example Finished.");

    vTaskDelay(pdMS_TO_TICKS(5000));
}

Build, Flash, and Monitor:

Standard procedure.

Expected Output:

Logs indicating the attempt to allocate DMA memory, success or failure, simulated usage, and freeing.

Plaintext
I (XXX) HEAP_EXAMPLE: Starting DMA Heap Example...
I (XXX) HEAP_EXAMPLE: Attempting to allocate 1024 bytes of DMA-capable memory...
I (XXX) HEAP_EXAMPLE: DMA-capable memory allocated successfully at address: 0x3ffeYYYY
I (XXX) HEAP_EXAMPLE: Simulating usage of DMA buffer...
I (XXX) HEAP_EXAMPLE: DMA buffer filled (simulation complete).
I (XXX) HEAP_EXAMPLE: Freeing DMA buffer...
I (XXX) HEAP_EXAMPLE: DMA buffer freed.
I (XXX) HEAP_EXAMPLE: DMA Heap Example Finished.

Example 3: Using Heap Information Functions

This example demonstrates how to query heap details.

C
void print_heap_info(const char* occasion) {
    ESP_LOGI(TAG, "Heap Status (%s):", occasion);
    ESP_LOGI(TAG, "  Total Free Default Heap: %u bytes", heap_caps_get_free_size(MALLOC_CAP_DEFAULT));
    ESP_LOGI(TAG, "  Largest Free Default Block: %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
    ESP_LOGI(TAG, "  Min Free Default Heap Ever: %u bytes", heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT));

    // Example: Check SPIRAM if enabled (add #ifdef CONFIG_SPIRAM_SUPPORT)
#ifdef CONFIG_SPIRAM_SUPPORT // Check if SPIRAM/PSRAM is enabled in menuconfig
    if (heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0) {
         ESP_LOGI(TAG, "  Total Free SPIRAM Heap: %u bytes", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
         ESP_LOGI(TAG, "  Largest Free SPIRAM Block: %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM));
         ESP_LOGI(TAG, "  Min Free SPIRAM Heap Ever: %u bytes", heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM));
    } else {
         ESP_LOGI(TAG, "  SPIRAM not available or configured.");
    }
#else
     ESP_LOGI(TAG, "  SPIRAM support not compiled.");
#endif

    // For very detailed info (use sparingly, can be verbose):
    // ESP_LOGI(TAG, "--- Detailed Default Heap Dump ---");
    // heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);
    // ESP_LOGI(TAG, "---------------------------------");
}

void app_main(void) {
    ESP_LOGI(TAG, "Starting Heap Info Example...");

    print_heap_info("Initial");

    // Allocate some blocks to potentially cause fragmentation
    void *block1 = heap_caps_malloc(5000, MALLOC_CAP_DEFAULT);
    void *block2 = heap_caps_malloc(10000, MALLOC_CAP_DEFAULT);
    void *block3 = heap_caps_malloc(7000, MALLOC_CAP_DEFAULT);

    if (!block1 || !block2 || !block3) {
         ESP_LOGE(TAG, "Initial allocation failed, cannot demonstrate fragmentation well.");
         // Free any successfully allocated blocks before returning
         if(block1) heap_caps_free(block1);
         if(block2) heap_caps_free(block2);
         if(block3) heap_caps_free(block3);
         return;
    }

    ESP_LOGI(TAG, "Allocated blocks: block1=%p, block2=%p, block3=%p", block1, block2, block3);
    print_heap_info("After Allocations");

    // Free the middle block - this can create fragmentation
    ESP_LOGI(TAG, "Freeing middle block (block2)...");
    heap_caps_free(block2);
    block2 = NULL;
    print_heap_info("After Freeing Middle Block");

    // Now, try to allocate a block larger than either remaining free chunk,
    // but potentially smaller than the total free heap.
    size_t large_alloc_size = 12000;
    ESP_LOGI(TAG, "Attempting large allocation (%u bytes)...", large_alloc_size);
    void *large_block = heap_caps_malloc(large_alloc_size, MALLOC_CAP_DEFAULT);

    if (large_block == NULL) {
        ESP_LOGW(TAG, "Failed to allocate large block (%u bytes) - likely due to fragmentation.", large_alloc_size);
        ESP_LOGW(TAG, "Note: Total free heap might still be > %u, but largest contiguous block is smaller.", large_alloc_size);
    } else {
        ESP_LOGI(TAG, "Successfully allocated large block at %p.", large_block);
        heap_caps_free(large_block); // Free it immediately for this example
    }

    // Cleanup remaining blocks
    ESP_LOGI(TAG, "Cleaning up remaining blocks...");
    if(block1) heap_caps_free(block1);
    if(block3) heap_caps_free(block3); // block2 already freed or NULL

    print_heap_info("After Cleanup");
    ESP_LOGI(TAG, "Heap Info Example Finished.");

    vTaskDelay(pdMS_TO_TICKS(5000));
}

Build, Flash, and Monitor:

Standard procedure.

Expected Output:

You’ll see the heap status printed at various stages. Pay attention to how the “Total Free” and “Largest Free Block” values change, especially after freeing the middle block (block2). You might observe that the large allocation fails even if the total free heap is greater than the requested size, demonstrating fragmentation.

Variant Notes

  • Memory Sizes: The total amount of DRAM, IRAM, and RTC memory varies significantly between ESP32 variants (ESP32, S2, S3, C3, C6, H2). Consult the specific datasheet for your chip.
  • SPIRAM/PSRAM: External SPIRAM is only available on certain modules (like ESP32-WROVER) or custom boards with an external PSRAM chip. You must explicitly enable SPIRAM support in menuconfig (Component config -> ESP PSRAM) for MALLOC_CAP_SPIRAM to work.
  • DMA Capabilities: While MALLOC_CAP_DMA is generally available for internal DRAM, the exact address ranges suitable for DMA might differ subtly between variants. Using the heap_caps API handles these differences transparently.

The heap_caps API is designed to abstract these hardware differences, allowing you to write portable code by requesting capabilities rather than specific memory addresses or types.

Common Mistakes & Troubleshooting Tips

Common Mistake / Pitfall Potential Symptom(s) Fix / Best Practice
Memory Leaks
(Forgetting to call heap_caps_free())
heap_caps_get_free_size() steadily decreases; heap_caps_get_minimum_free_size() drops continuously. Eventually, allocations fail (malloc/heap_caps_malloc returns NULL). System may become slow or crash. Ensure every successful allocation (heap_caps_malloc, calloc, realloc) has a corresponding heap_caps_free() when memory is no longer needed. Use heap monitoring tools during development. Consider RAII in C++.
Dangling Pointers (Use-After-Free)
(Accessing memory after it has been freed)
Unpredictable crashes (often LoadProhibited/StoreProhibited), corrupted data, security vulnerabilities. Behavior can be erratic as freed memory might be reused. Set pointers to NULL immediately after freeing the memory they point to. Be cautious with pointer copies and lifetimes, especially across tasks or global storage.
Double Free
(Calling heap_caps_free() twice on the same pointer)
Heap corruption, likely leading to crashes either immediately or during later memory operations. Can be hard to debug. Ensure heap_caps_free() is called exactly once for each allocated block. Setting pointers to NULL after freeing helps prevent accidental double frees.
Ignoring Allocation Failures
(Not checking if heap_caps_malloc() returned NULL)
Attempting to dereference a NULL pointer, usually causing an immediate crash (e.g., “Guru Meditation Error: Core 0 panic’d (LoadProhibited). Exception was unhandled.”). Always check the return value of allocation functions. If NULL, handle the error gracefully (log, release resources, retry, safe state, notify user).
Heap Fragmentation
(Frequent allocation/deallocation of varied sizes)
heap_caps_malloc() fails (returns NULL) even if heap_caps_get_free_size() shows ample total free memory. heap_caps_get_largest_free_block() will be smaller than the requested size. Minimize dynamic allocations. Prefer stack allocation. Use memory pools for fixed-size objects. Allocate large, long-lived buffers early. Try to allocate similar-sized blocks. Monitor heap_caps_get_largest_free_block().
Buffer Overflows/Underflows
(Writing past the allocated block’s boundaries)
Heap metadata corruption, crashes, unpredictable behavior, security issues. Can overwrite adjacent blocks or heap control structures. Ensure writes stay within allocated bounds. Use safe string functions (snprintf instead of sprintf). Be careful with pointer arithmetic and array indexing. Tools like AddressSanitizer (if available for the platform) can help detect these.
Incorrect Capability Flags
(e.g., requesting MALLOC_CAP_SPIRAM when none is present, or not using MALLOC_CAP_DMA for DMA buffers)
Allocation may fail (return NULL). If DMA memory is not allocated with MALLOC_CAP_DMA, DMA operations might fail or corrupt data. Use appropriate capability flags for the intended memory use and type. Check ESP-IDF documentation and chip datasheets. Conditionally compile code for features like SPIRAM.
Uninitialized Memory
(Reading from allocated memory before writing to it)
Using garbage data, leading to incorrect program behavior, calculations, or crashes. Initialize allocated memory before use, either manually or by using heap_caps_calloc() which zeroes out the memory.

Exercises

  1. Heap Monitoring Task: Create a low-priority FreeRTOS task that periodically (e.g., every 5 seconds) calls print_heap_info("Periodic Check"); (from Example 3) to continuously monitor the heap status while your main application runs other examples or performs allocations/deallocations. Observe how the values change.
  2. Fragmentation Simulation: Write a program that:
    • Allocates 10 small blocks (e.g., 1KB each).
    • Frees every second block (blocks 2, 4, 6, 8, 10).
    • Prints the total free heap and the largest free block size.
    • Attempts to allocate a single block slightly larger than one small block but smaller than the total free space (e.g., 1.5KB). Observe if it succeeds or fails due to fragmentation.
    • Cleans up all remaining allocated blocks.
  3. Safe Allocation Function: Create a wrapper function void* safe_malloc(size_t size, uint32_t caps, const char* purpose) that calls heap_caps_malloc(size, caps), checks for NULL, logs an error message including the purpose string if allocation fails, and returns the pointer. Modify Example 1 to use this safe_malloc function.

Summary

  • The heap provides memory for dynamic allocation at runtime, managed via functions like malloc/free.
  • FreeRTOS offers heap schemes; ESP-IDF defaults to heap_4, which supports malloc/free and coalesces free blocks to reduce fragmentation.
  • ESP-IDF provides the heap_caps API (heap_caps_malloc, heap_caps_free, etc.) for allocating memory with specific capabilities (e.g., MALLOC_CAP_DMA, MALLOC_CAP_SPIRAM, MALLOC_CAP_INTERNAL). Using heap_caps is preferred over standard malloc in ESP-IDF.
  • Memory leaks (forgetting free) exhaust the heap.
  • Fragmentation breaks the heap into small pieces, potentially preventing large allocations even with sufficient total free memory.
  • Dangling pointers (use-after-free) and double frees cause heap corruption and crashes.
  • Always check the return value of allocation functions for NULL.
  • Use heap_caps_get_free_size(), heap_caps_get_largest_free_block(), and heap_caps_print_heap_info() to monitor heap status and diagnose issues.
  • Careful memory management is critical for stable embedded systems.

Further Reading

Leave a Comment

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

Scroll to Top