Chapter 23: Using Heap Memory Efficiently

Chapter Objectives

  • Understand the difference between stack and heap memory allocation.
  • Learn about dynamic memory allocation using the heap.
  • Understand the concept and consequences of heap fragmentation.
  • Learn how to use the ESP-IDF heap memory allocation functions (heap_caps_...).
  • Understand memory capabilities and how to request memory with specific attributes (e.g., DMA-capable).
  • Learn how to monitor heap usage using ESP-IDF functions.
  • Identify common heap-related problems like memory leaks and double frees.
  • Apply best practices for efficient and safe heap memory management.

Introduction

In previous chapters, we’ve seen variables declared within functions or globally. Many of these variables reside on the stack, a region of memory managed automatically by the compiler. Stack allocation is fast and simple, but it’s limited in size and scope – stack variables only exist while the function they are declared in is executing.

However, many applications need more flexibility. We might need to:

  • Allocate memory whose size is only known at runtime.
  • Create data structures that need to persist longer than the function that created them.
  • Handle large buffers for network data, files, or sensor readings.

For these scenarios, we use dynamic memory allocation, which takes memory from a large pool called the heap. While powerful, using the heap requires careful management. Improper heap usage can lead to subtle bugs, crashes, and performance degradation, especially in resource-constrained embedded systems like the ESP32.

This chapter delves into the fundamentals of heap memory, how to use it effectively with the ESP-IDF, how to monitor its status, and how to avoid common pitfalls like memory leaks and fragmentation. Mastering heap management is crucial for building stable and robust ESP32 applications.

Theory

Stack vs. Heap Memory

Understanding the difference between stack and heap is fundamental:

  1. Stack Memory:
    • Allocation/Deallocation: Managed automatically by the compiler using LIFO (Last-In, First-Out) principle. Memory is allocated when entering a function (for local variables) and deallocated when exiting.
    • Speed: Very fast allocation and deallocation (just involves adjusting the stack pointer).
    • Size: Relatively small and fixed for each task (configured via menuconfig). Exceeding the stack limit leads to a stack overflow crash.
    • Scope: Local variables exist only within the function where they are declared.
    • Fragmentation: Not an issue due to the LIFO nature.
  2. Heap Memory:
    • Allocation/Deallocation: Managed explicitly by the programmer using functions like malloc (or heap_caps_malloc in ESP-IDF) and free (or heap_caps_free).
    • Speed: Slower than stack allocation, as the memory manager needs to find a suitable block of free memory.
    • Size: Much larger pool of memory available compared to the stack. The total available heap depends on the ESP32 variant and static memory usage.
    • Scope: Allocated memory persists until explicitly deallocated using free, regardless of which function allocated it.
    • Fragmentation: A significant potential problem (explained below).
Feature Stack Heap
Management Automatic (by compiler/runtime) Manual (by programmer: malloc/free or heap_caps_...)
Allocation/Deallocation Speed Very Fast (adjust stack pointer) Slower (search for free block, potential list management)
Structure LIFO (Last-In, First-Out) Pool of memory, blocks allocated/freed in any order
Size Limit Relatively small, fixed per task (can cause stack overflow) Much larger pool, limited by total available RAM
Variable Lifetime / Scope Tied to function scope (local variables) Persists until explicitly freed, independent of function scope
Fragmentation Risk None (due to LIFO) High (External fragmentation is a major concern)
Typical Use Local variables, function parameters, return addresses Dynamically sized data, large buffers, data structures with long lifetimes

Dynamic Memory Allocation

Dynamic memory allocation is the process of requesting blocks of memory from the heap at runtime. The standard C library functions are malloc() (memory allocate), calloc() (clear allocate), realloc() (resize allocate), and free() (deallocate).

ESP-IDF provides its own set of heap functions, primarily heap_caps_..., which offer more control, especially regarding memory capabilities.

Heap Fragmentation

Because heap blocks can be allocated and deallocated in any order and with varying sizes, the heap can become fragmented over time. This means the total free memory might be large, but it’s broken up into many small, non-contiguous chunks.

  • External Fragmentation: Occurs when there is enough total free heap space to satisfy a request, but no single contiguous block is large enough. Allocation requests fail even though sufficient total memory is free.
  • Internal Fragmentation: Occurs when the allocated block is larger than the requested size (due to allocator overhead or alignment requirements). The unused space within the allocated block is wasted.
%%{ init: { 'theme': 'base', 'themeVariables': {
      'primaryColor': '#F3F4F6',      'primaryTextColor': '#4B5563', 'primaryBorderColor': '#9CA3AF', /* Gray for Free */
      'secondaryColor': '#FEF3C7',    'secondaryTextColor': '#92400E', 'secondaryBorderColor': '#D97706', /* Amber for Allocated */
      'tertiaryColor': '#FEE2E2',     'tertiaryTextColor': '#991B1B', 'tertiaryBorderColor': '#DC2626', /* Red for Failed Alloc */
      'lineColor': '#A78BFA',         'textColor': '#1F2937',
      'mainBkg': '#FFFFFF',           'nodeBorder': '#A78BFA',
      'fontFamily': '"Open Sans", sans-serif'
} } }%%
graph TD
    subgraph "Time 1: Initial State"
        direction LR
        T1_Free["Heap: Free (100 units)"]:::freeBlock;
    end

    subgraph "Time 2: Allocations"
        direction LR
        T2_A["A (20u)"]:::allocBlock;
        T2_B["B (30u)"]:::allocBlock;
        T2_C["C (15u)"]:::allocBlock;
        T2_Free["Free (35u)"]:::freeBlock;
         T2_A -- B --- T2_B -- C --- T2_C -- Free --- T2_Free;
    end

    subgraph "Time 3: Deallocations (B and C freed)"
        direction LR
        T3_A["A (20u)"]:::allocBlock;
        T3_Free1["Free (30u)"]:::freeBlock;
        T3_Free2["Free (15u)"]:::freeBlock;
        T3_Free3["Free (35u)"]:::freeBlock;
         T3_A -- Gap 1 --- T3_Free1 -- Gap 2 --- T3_Free2 -- Gap 3 --- T3_Free3;
         subgraph " "
            direction TB
            TotalFree["Total Free = 30 + 15 + 35 = 80 units"];
         end
    end

     subgraph "Time 4: Large Allocation Request (50 units)"
        direction LR
        T4_A["A (20u)"]:::allocBlock;
        T4_Free1["Free (30u)"]:::freeBlock;
        T4_Free2["Free (15u)"]:::freeBlock;
        T4_Free3["Free (35u)"]:::freeBlock;
        T4_Fail["Request D (50u) -> FAILS!<br>(No single block large enough)"]:::failBlock;
         T4_A --- T4_Free1 --- T4_Free2 --- T4_Free3;
         subgraph " "
             direction TB
             FailDesc["External Fragmentation:<br>Enough total free space (80u),<br>but not contiguous."];
         end
         T4_Free1 --> T4_Fail;
         T4_Free2 --> T4_Fail;
         T4_Free3 --> T4_Fail;

    end


    T1_Free --> T2_A;
    T2_C --> T3_A;
    T3_Free3 --> T4_A;


    %% Styling
    classDef freeBlock fill:#F3F4F6,stroke:#9CA3AF,stroke-width:1px,color:#4B5563,stroke-dasharray:4 4;
    classDef allocBlock fill:#FEF3C7,stroke:#D97706,stroke-width:1.5px,color:#92400E;
    classDef failBlock fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B,font-weight:bold;

Fragmentation is a major concern in long-running embedded systems. Severe fragmentation can lead to allocation failures and unpredictable crashes, even if the system theoretically has enough RAM.

Strategies to Minimize Fragmentation:

  • Allocate buffers early and reuse them.
  • Prefer allocating fixed-size blocks where possible.
  • Avoid frequent allocation/deallocation of small, variably-sized blocks.
  • Consider using memory pools for objects of the same size.
  • Allocate large, long-lived buffers first.

ESP-IDF Heap Implementation (heap_caps_...)

ESP-IDF uses the multi_heap implementation, which allows different regions of RAM to be treated as separate heaps with specific capabilities. This is crucial because different memory regions on the ESP32 have different properties.

Memory Capabilities:

ESP-IDF defines memory capabilities using flags (e.g., MALLOC_CAP_... defined in esp_heap_caps.h). Common capabilities include:

Capability Flag Description & Use Case
MALLOC_CAP_DEFAULT General-purpose memory allocation, suitable for most standard data structures and buffers. Usually maps to internal, byte-addressable RAM. Use this unless specific capabilities are required.
MALLOC_CAP_8BIT Memory that is byte-addressable (can read/write individual bytes). Most internal RAM has this capability.
MALLOC_CAP_32BIT Memory that allows 32-bit aligned reads/writes. Often included with MALLOC_CAP_8BIT for internal RAM. Some specialized memory might only have this.
MALLOC_CAP_DMA Memory suitable for use with Direct Memory Access (DMA) controllers (e.g., for SPI, I2S peripherals). This memory must often reside in specific address ranges accessible by the DMA hardware. Essential for zero-copy peripheral drivers.
MALLOC_CAP_SPIRAM Memory allocated from external SPI RAM (PSRAM), if the ESP32 variant supports it and it’s enabled. Offers much larger heap space but is slower than internal RAM.
MALLOC_CAP_EXEC Memory region from which code can be executed. Less commonly used for dynamic allocation, but potentially useful for self-modifying code or dynamic code loading (advanced use cases).
MALLOC_CAP_INTERNAL Memory that is internal to the ESP32 chip (i.e., not SPIRAM). Often combined with other flags like MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL.

Note: Capabilities can be combined using the bitwise OR operator (|), e.g., MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL.

Capabilities-Based Allocation:

Instead of just malloc, ESP-IDF encourages using heap_caps_malloc():

C
void *heap_caps_malloc(size_t size, uint32_t caps);
  • size: The number of bytes to allocate.
  • caps: A bitmask specifying the required capabilities (e.g., MALLOC_CAP_DMA | MALLOC_CAP_8BIT). The allocator will find a heap region that satisfies all requested capabilities.

Using heap_caps_malloc ensures that memory allocated for specific purposes (like DMA buffers) resides in a suitable physical memory region.

Freeing Memory:

Memory allocated with heap_caps_malloc (or related functions like heap_caps_calloc, heap_caps_realloc) must be freed using heap_caps_free():

C
void heap_caps_free(void *ptr);

Mixing standard malloc/free with heap_caps_malloc/heap_caps_free can lead to corruption or crashes. Stick to the heap_caps_... functions in ESP-IDF.

Tip: For general-purpose allocations where no special capabilities are needed, use heap_caps_malloc(size, MALLOC_CAP_DEFAULT).

Monitoring Heap Usage

ESP-IDF provides functions to inspect the state of the heap at runtime, which is invaluable for debugging memory issues:

Function Returned Information
esp_get_free_heap_size() Total free bytes across all heap regions. Quick overview, but doesn’t show fragmentation.
esp_get_minimum_free_heap_size() Smallest total free heap size recorded since boot. Crucial for detecting past memory pressure.
heap_caps_get_free_size(caps) Total free bytes matching the specified capabilities mask.
heap_caps_get_minimum_free_size(caps) Smallest free heap size matching capabilities recorded since boot.
heap_caps_get_largest_free_block(caps) Size of the largest single contiguous free block matching capabilities. Good indicator of fragmentation (compare with total free size).
heap_caps_get_info(info_ptr, caps) Fills a multi_heap_info_t struct with detailed stats (total, free, min free, largest block, block counts) for matching capabilities.
heap_caps_print_heap_info(caps) Prints a formatted summary of heap information (like heap_caps_get_info) to the console.
heap_caps_dump(caps) / heap_caps_dump_all() Prints a highly detailed list of all allocated and free blocks in the specified heap(s). Useful for deep fragmentation analysis. (Very verbose output).

Regularly monitoring heap usage, especially the minimum free size and the largest free block, is crucial during development.

Practical Examples

Project Setup:

  • Use a standard ESP-IDF project template.
  • Ensure VS Code with the ESP-IDF extension is set up.
  • No additional hardware is needed.

Common Includes:

C
#include <stdio.h>
#include <string.h> // For memcpy, memset
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_heap_caps.h" // Required for heap capabilities functions
#include "esp_log.h"

static const char *TAG = "HEAP_EXAMPLE";

Example 1: Basic Allocation and Free

Demonstrates allocating memory for a structure, using it, and freeing it correctly.

C
typedef struct {
    int id;
    char data[20];
} my_data_t;

void app_main(void)
{
    ESP_LOGI(TAG, "Starting basic heap allocation example...");

    my_data_t *data_ptr = NULL;
    size_t initial_free_heap = esp_get_free_heap_size();
    ESP_LOGI(TAG, "Initial free heap size: %u bytes", initial_free_heap);

    // 1. Allocate memory from the heap
    ESP_LOGI(TAG, "Allocating memory for my_data_t structure...");
    // Use MALLOC_CAP_DEFAULT for general purpose internal RAM
    data_ptr = (my_data_t *)heap_caps_malloc(sizeof(my_data_t), MALLOC_CAP_DEFAULT);

    // 2. Check if allocation was successful
    if (data_ptr == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory!");
        // Handle error appropriately - maybe restart or enter error state
        return;
    } else {
        ESP_LOGI(TAG, "Memory allocated successfully at address: %p", data_ptr);
        size_t free_heap_after_alloc = esp_get_free_heap_size();
        ESP_LOGI(TAG, "Free heap after allocation: %u bytes (Allocated approx: %u)",
                 free_heap_after_alloc, initial_free_heap - free_heap_after_alloc);

        // 3. Use the allocated memory
        data_ptr->id = 123;
        strcpy(data_ptr->data, "Hello Heap!");
        ESP_LOGI(TAG, "Data stored - ID: %d, Message: '%s'", data_ptr->id, data_ptr->data);

        // 4. Free the allocated memory
        ESP_LOGI(TAG, "Freeing allocated memory...");
        heap_caps_free(data_ptr);
        data_ptr = NULL; // Good practice: Set pointer to NULL after freeing

        size_t free_heap_after_free = esp_get_free_heap_size();
        ESP_LOGI(TAG, "Free heap after free: %u bytes", free_heap_after_free);
    }

    // Example of allocating DMA capable memory (if needed for peripherals)
    size_t dma_buffer_size = 1024;
    uint8_t *dma_buffer = NULL;
    ESP_LOGI(TAG, "Attempting to allocate %d bytes of DMA-capable memory...", dma_buffer_size);
    dma_buffer = (uint8_t *)heap_caps_malloc(dma_buffer_size, MALLOC_CAP_DMA);

    if (dma_buffer == NULL) {
         ESP_LOGW(TAG, "Failed to allocate DMA-capable memory (might be expected if DMA pool is small/unavailable).");
    } else {
        ESP_LOGI(TAG, "DMA-capable memory allocated at %p", dma_buffer);
        // Use the DMA buffer...
        memset(dma_buffer, 0xA5, dma_buffer_size); // Example usage
        ESP_LOGI(TAG, "Freeing DMA-capable memory...");
        heap_caps_free(dma_buffer);
        dma_buffer = NULL;
    }


    ESP_LOGI(TAG, "Basic heap example finished.");
}

Build, Flash, and Monitor:

  1. idf.py build
  2. idf.py -p <YOUR_PORT> flash
  3. idf.py -p <YOUR_PORT> monitor

Expected Output:

You’ll see logs showing the initial free heap size, the size after allocation (it should decrease by slightly more than sizeof(my_data_t) due to heap overhead), the stored data, and the heap size after freeing (it should return close to the initial size). The DMA allocation might succeed or fail depending on the specific ESP32 variant and configuration.

Plaintext
I (xxx) HEAP_EXAMPLE: Starting basic heap allocation example...
I (xxx) HEAP_EXAMPLE: Initial free heap size: 2XXXXX bytes
I (xxx) HEAP_EXAMPLE: Allocating memory for my_data_t structure...
I (xxx) HEAP_EXAMPLE: Memory allocated successfully at address: 0x3XXXXXXX
I (xxx) HEAP_EXAMPLE: Free heap after allocation: 2XXXXX bytes (Allocated approx: 40)
I (xxx) HEAP_EXAMPLE: Data stored - ID: 123, Message: 'Hello Heap!'
I (xxx) HEAP_EXAMPLE: Freeing allocated memory...
I (xxx) HEAP_EXAMPLE: Free heap after free: 2XXXXX bytes
I (xxx) HEAP_EXAMPLE: Attempting to allocate 1024 bytes of DMA-capable memory...
I (xxx) HEAP_EXAMPLE: DMA-capable memory allocated at 0x3XXXXXXX
I (xxx) HEAP_EXAMPLE: Freeing DMA-capable memory...
I (xxx) HEAP_EXAMPLE: Basic heap example finished.

Example 2: Demonstrating a Memory Leak

This example intentionally leaks memory in a loop to show its effect on available heap.

C
#define LEAK_SIZE 512 // Allocate 512 bytes each time
#define LEAK_DELAY_MS 1000 // Delay between leaks

void app_main(void)
{
    ESP_LOGI(TAG, "Starting memory leak demonstration...");
    ESP_LOGI(TAG, "Will allocate %d bytes every %d ms WITHOUT freeing.", LEAK_SIZE, LEAK_DELAY_MS);

    uint32_t iteration = 0;
    while(1) {
        iteration++;
        ESP_LOGI(TAG, "Iteration %" PRIu32 "...", iteration);

        // Allocate memory
        void *leaked_memory = heap_caps_malloc(LEAK_SIZE, MALLOC_CAP_DEFAULT);

        if (leaked_memory == NULL) {
            ESP_LOGE(TAG, "Memory allocation failed! Heap likely exhausted.");
            // In a real app, handle this error (e.g., stop leaking, log critical error, restart)
            break; // Stop the loop for this example
        } else {
            ESP_LOGI(TAG, "Allocated %d bytes at %p.", LEAK_SIZE, leaked_memory);
            // *** INTENTIONALLY NOT CALLING heap_caps_free(leaked_memory); ***
        }

        // Log current heap status
        size_t current_free = esp_get_free_heap_size();
        size_t min_free = esp_get_minimum_free_heap_size();
        ESP_LOGI(TAG, "Current free heap: %u bytes, Minimum free ever: %u bytes", current_free, min_free);

        vTaskDelay(pdMS_TO_TICKS(LEAK_DELAY_MS));
    }

     ESP_LOGE(TAG, "Memory leak demonstration finished (due to allocation failure).");
     // The device will likely crash or become unstable soon after heap exhaustion.
     while(1) { vTaskDelay(portMAX_DELAY); } // Keep task alive
}

Build, Flash, and Monitor:

  1. Build, flash, monitor.
  2. Observe the monitor output over time.

Expected Output:

You will see the “Current free heap” size decrease with each iteration. The “Minimum free ever” will also track downwards. Eventually, heap_caps_malloc will return NULL, the “Memory allocation failed!” error will be logged, and the loop will stop. The device might become unstable or crash shortly after running out of heap. This clearly demonstrates the danger of memory leaks.

Plaintext
I (xxx) HEAP_EXAMPLE: Starting memory leak demonstration...
I (xxx) HEAP_EXAMPLE: Will allocate 512 bytes every 1000 ms WITHOUT freeing.
I (xxx) HEAP_EXAMPLE: Iteration 1...
I (xxx) HEAP_EXAMPLE: Allocated 512 bytes at 0x3XXXXXXX.
I (xxx) HEAP_EXAMPLE: Current free heap: 2XXXXX bytes, Minimum free ever: 2XXXXX bytes
I (xxx) HEAP_EXAMPLE: Iteration 2...
I (xxx) HEAP_EXAMPLE: Allocated 512 bytes at 0x3XXXXXXX.
I (xxx) HEAP_EXAMPLE: Current free heap: 2XXXXX bytes, Minimum free ever: 2XXXXX bytes
... (Free heap continues to decrease) ...
I (xxx) HEAP_EXAMPLE: Iteration N...
E (xxx) HEAP_EXAMPLE: Memory allocation failed! Heap likely exhausted.
E (xxx) HEAP_EXAMPLE: Memory leak demonstration finished (due to allocation failure).

Example 3: Using Heap Monitoring Functions

This example demonstrates various ways to check the heap status.

C
void app_main(void)
{
    ESP_LOGI(TAG, "Starting heap monitoring example...");

    // --- Initial State ---
    ESP_LOGI(TAG, "--- Initial Heap State ---");
    ESP_LOGI(TAG, "Total Free Heap: %u bytes", esp_get_free_heap_size());
    ESP_LOGI(TAG, "Minimum Free Heap Ever: %u bytes", esp_get_minimum_free_heap_size());
    ESP_LOGI(TAG, "Largest Free Block (Default Caps): %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
    ESP_LOGI(TAG, "Free DMA Heap: %u bytes", heap_caps_get_free_size(MALLOC_CAP_DMA));
    ESP_LOGI(TAG, "Largest Free DMA Block: %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_DMA));

    // Print detailed info for default heap
    ESP_LOGI(TAG, "Detailed Info (Default Caps):");
    heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);

    // --- Allocate some memory ---
    ESP_LOGI(TAG, "\n--- Allocating 10KB ---");
    void *block1 = heap_caps_malloc(10 * 1024, MALLOC_CAP_DEFAULT);
    if (!block1) ESP_LOGE(TAG, "Failed to allocate block1");

    ESP_LOGI(TAG, "--- Heap State After Allocation ---");
    ESP_LOGI(TAG, "Total Free Heap: %u bytes", esp_get_free_heap_size());
    ESP_LOGI(TAG, "Minimum Free Heap Ever: %u bytes", esp_get_minimum_free_heap_size()); // Might update
    ESP_LOGI(TAG, "Largest Free Block (Default Caps): %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));

    multi_heap_info_t info;
    heap_caps_get_info(&info, MALLOC_CAP_DEFAULT);
    ESP_LOGI(TAG, "Detailed Info (Default Caps):");
    ESP_LOGI(TAG, "  Total free bytes: %u", info.total_free_bytes);
    ESP_LOGI(TAG, "  Total allocated bytes: %u", info.total_allocated_bytes);
    ESP_LOGI(TAG, "  Largest free block: %u", info.largest_free_block);
    ESP_LOGI(TAG, "  Minimum free bytes: %u", info.minimum_free_bytes); // Same as esp_get_minimum...()
    ESP_LOGI(TAG, "  Allocated blocks: %u", info.allocated_blocks);
    ESP_LOGI(TAG, "  Free blocks: %u", info.free_blocks);
    ESP_LOGI(TAG, "  Total blocks: %u", info.total_blocks);


    // --- Allocate and free smaller blocks to potentially cause fragmentation ---
    ESP_LOGI(TAG, "\n--- Allocating/Freeing Small Blocks ---");
    void* small_blocks[10];
    for (int i = 0; i < 10; i++) {
        small_blocks[i] = heap_caps_malloc(256, MALLOC_CAP_DEFAULT);
        if (!small_blocks[i]) ESP_LOGE(TAG, "Failed small alloc %d", i);
    }
     for (int i = 0; i < 10; i+=2) { // Free every other block
         if(small_blocks[i]) heap_caps_free(small_blocks[i]);
         small_blocks[i] = NULL;
     }

    ESP_LOGI(TAG, "--- Heap State After Small Alloc/Free ---");
    ESP_LOGI(TAG, "Total Free Heap: %u bytes", esp_get_free_heap_size());
    ESP_LOGI(TAG, "Largest Free Block (Default Caps): %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
    ESP_LOGI(TAG, "Detailed Info (Default Caps):");
    heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);

    // Dump detailed block info (can be very long!)
    // ESP_LOGI(TAG, "\n--- Heap Dump (Default Caps) ---");
    // heap_caps_dump(MALLOC_CAP_DEFAULT);

    // --- Clean up ---
    ESP_LOGI(TAG, "\n--- Cleaning Up ---");
    if(block1) heap_caps_free(block1);
    for (int i = 1; i < 10; i+=2) { // Free remaining small blocks
         if(small_blocks[i]) heap_caps_free(small_blocks[i]);
    }

    ESP_LOGI(TAG, "--- Final Heap State ---");
    ESP_LOGI(TAG, "Total Free Heap: %u bytes", esp_get_free_heap_size());
    ESP_LOGI(TAG, "Minimum Free Heap Ever: %u bytes", esp_get_minimum_free_heap_size());
    ESP_LOGI(TAG, "Largest Free Block (Default Caps): %u bytes", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));

    ESP_LOGI(TAG, "Heap monitoring example finished.");
}

Build, Flash, and Monitor:

  1. Build, flash, monitor.

Expected Output:

This example will print various heap statistics at different stages: initially, after a large allocation, after allocating and freeing some small blocks (potentially showing a decrease in the largest free block relative to the total free space, indicating fragmentation), and finally after cleaning up. Pay attention to how esp_get_minimum_free_heap_size() captures the lowest point reached and how heap_caps_get_largest_free_block() changes.

Variant Notes

While the heap management concepts and the ESP-IDF API (heap_caps_...) are consistent across the ESP32 family (ESP32, S2, S3, C3, C6, H2), the primary difference is the amount of available internal RAM.

  • ESP32: Typically has 520KB of internal SRAM.
  • ESP32-S2/S3: Generally have around 320KB (S2) or 512KB (S3) of internal SRAM, but often come with integrated or support for external PSRAM (SPI RAM), significantly increasing available heap (up to several MB).
  • ESP32-C3/C6/H2: RISC-V based, generally have around 400KB (C3) or more (C6/H2) of internal SRAM. Some variants might support PSRAM.

Implications:

  • The initial esp_get_free_heap_size() will vary significantly between variants.
  • Memory constraints are tighter on variants with less RAM if PSRAM is not used.
  • Applications needing large amounts of heap might require variants with PSRAM support (ESP32, S3, potentially others). When PSRAM is enabled, heap_caps_malloc(size, MALLOC_CAP_SPIRAM) can be used to allocate from it, or MALLOC_CAP_DEFAULT might be configured to prefer external RAM for larger allocations.

Always check the datasheet for your specific variant and monitor heap usage carefully during development, especially the minimum free heap size.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Memory Leaks
Forgetting heap_caps_free().
Free heap steadily decreases; esp_get_minimum_free_heap_size() drops; eventual allocation failures (NULL return) and crashes. Ensure every heap_caps_malloc has a matching heap_caps_free. Monitor heap stats. Use heap tracing tools if available. Establish clear memory ownership.
Use-After-Free
Accessing memory via a pointer after it was freed.
Corrupted data, unpredictable behavior, intermittent crashes (often when freed memory gets reused). Set pointers to NULL immediately after freeing. Check pointers for NULL before use. Avoid dangling pointers.
Double Free
Calling heap_caps_free() twice on the same pointer.
Heap corruption, likely immediate crash or assert/panic within heap functions. Free memory only once. Setting pointers to NULL after free helps prevent this (freeing NULL is safe).
Ignoring Allocation Failures
Not checking if heap_caps_malloc returned NULL.
Immediate crash (Load/StoreProhibited error) when dereferencing the NULL pointer. Always check the return value of heap_caps_malloc (and related functions) for NULL. Handle allocation failures gracefully.
Excessive Fragmentation
Frequent alloc/free of small, varied sizes.
Allocation failures (NULL) even when total free heap seems sufficient. heap_caps_get_largest_free_block() is much smaller than total free size. Minimize dynamic allocations. Reuse buffers. Use fixed-size blocks or memory pools. Allocate large/long-lived buffers early. Monitor largest free block size.
Mixing malloc/free and heap_caps_...
Using standard C functions with ESP-IDF heap.
Potential heap corruption, crashes, unpredictable behavior. Standard malloc is not capabilities-aware. Consistently use the heap_caps_malloc, heap_caps_calloc, heap_caps_realloc, and heap_caps_free functions provided by ESP-IDF.
Allocating in ISRs
Calling heap_caps_malloc or heap_caps_free from an Interrupt Service Routine.
Heap functions are not ISR-safe (can block, not reentrant). Will likely cause crashes or system instability. Never allocate or free memory directly within an ISR. Use ISR-safe mechanisms like queues or ring buffers to pass data/requests to a regular task for processing and memory management.

Exercises

  1. Heap Allocation Failure Handling: Modify Example 1. After successfully allocating data_ptr, try to allocate an extremely large block of memory that is guaranteed to fail (e.g., heap_caps_malloc(500 * 1024, MALLOC_CAP_DEFAULT)). Check the return value. If it’s NULL, log an error message “Intentional large allocation failed as expected.” Then, proceed to free the original data_ptr correctly.
  2. Fragmentation Simulation: Write a task that repeatedly:
    • Allocates a large block (e.g., 20KB).
    • Allocates many small blocks (e.g., 50 blocks of 100 bytes).
    • Frees the large block.
    • Frees every other small block.
    • Logs esp_get_free_heap_size() and heap_caps_get_largest_free_block().Observe how the largest free block size might decrease relative to the total free size over several iterations, indicating fragmentation. Remember to add appropriate delays (vTaskDelay) to avoid watchdog timeouts.
  3. Minimum Free Heap Monitoring: Create a simple application with two tasks. Task A periodically allocates a medium-sized buffer (e.g., 5KB), holds it for a short time (e.g., 500ms), and then frees it. Task B runs less frequently (e.g., every 5 seconds) and logs the values returned by esp_get_free_heap_size() and esp_get_minimum_free_heap_size(). Observe how the minimum value captures the “low watermark” of heap usage caused by Task A’s allocations, even though the current free size is usually higher when Task B runs.

Summary

  • Heap provides flexible dynamic memory allocation but requires careful manual management.
  • Stack is used for automatic variables within function scope; Heap is for data with longer or runtime-defined lifetimes.
  • ESP-IDF uses heap_caps_... functions for capabilities-aware heap allocation (e.g., MALLOC_CAP_DMA, MALLOC_CAP_SPIRAM). Always use heap_caps_free for memory allocated with heap_caps_malloc.
  • Heap fragmentation (external and internal) occurs when free memory is broken into small pieces, potentially leading to allocation failures even with sufficient total free RAM.
  • Memory leaks (forgetting to free) exhaust heap memory over time.
  • Use-after-free and double-free errors cause heap corruption and crashes.
  • Always check the return value of allocation functions (heap_caps_malloc etc.) for NULL.
  • Use ESP-IDF monitoring functions (esp_get_free_heap_size, esp_get_minimum_free_heap_size, heap_caps_get_largest_free_block, heap_caps_print_heap_info) to track heap usage and diagnose issues.
  • Minimize fragmentation by reusing buffers, allocating fixed sizes, and avoiding frequent small allocations/deallocations.

Further Reading

Leave a Comment

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

Scroll to Top