Gerenciamento de Memória 💾

Alocação Dinâmica no ESP-IDF

O gerenciamento de memória em sistemas embarcados requer atenção especial devido a recursos limitados e consequências graves de vazamentos de memória. O ESP-IDF oferece múltiplos esquemas de alocação otimizados para diferentes padrões de uso, permitindo que você escolha estratégia apropriada baseada nos requisitos específicos da aplicação.

A função malloc padrão no ESP-IDF aloca da heap principal compartilhada por todas as tasks. Esta heap é gerenciada por allocator sofisticado que minimiza fragmentação através de estratégias inteligentes de coalescing e splitting de blocos. No entanto, fragmentação ainda pode ocorrer ao longo do tempo com padrões de alocação e desalocação aleatórios.

O ESP32 possui múltiplas regiões de RAM com características diferentes, e o ESP-IDF permite que você aloque especificamente de regiões particulares conforme necessidades. Por exemplo, heap_caps_malloc permite especificar requisitos como DMA-capable memory ou internal RAM, garantindo que alocação satisfaça constraints de hardware específicos.

⚠️ Armadilhas Comuns em Alocação Dinâmica

Vazamentos de memória representam problema insidioso em sistemas embarcados que executam continuamente por longos períodos. Cada pequeno vazamento eventualmente esgota memória disponível causando falha do sistema. Diferentemente de sistemas desktop que reiniciam frequentemente, sistemas IoT podem executar por meses sem reinicialização, amplificando impacto de vazamentos pequenos.

Fragmentação de heap é outra preocupação séria onde memória livre total é suficiente mas não existe bloco contíguo grande suficiente para satisfazer alocação. Isto resulta em falha de alocação apesar de memória “disponível”, problema particularmente frustrante de debug.

Alocação em contextos de interrupção é estritamente proibida pois allocators requerem locks que não podem ser adquiridos em ISRs. Violações desta regra causam deadlocks ou corrupção de heap difíceis de diagnosticar. ISRs devem usar apenas memória pré-alocada ou enviar notificações para tasks que podem alocar em contexto seguro.

O uso de heap durante inicialização antes que FreeRTOS inicie scheduler requer cuidado especial. Algumas estruturas de dados do sistema ainda não foram inicializadas, limitando funcionalidades disponíveis. Code que aloca durante early boot deve ser testado cuidadosamente.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include <string.h>

static const char *TAG = "MEMORY_EXAMPLE";

/**
 * Estrutura para demonstrar alocação de objetos complexos
 */
typedef struct {
    int sensor_id;
    float *readings;        // Array alocado dinamicamente
    size_t readings_count;
    char *description;      // String alocada dinamicamente
} sensor_data_t;

/**
 * Criar objeto sensor com alocação dinâmica adequada
 * 
 * Demonstra padrão correto de alocação com verificação de erros
 * e cleanup apropriado em caso de falha
 */
sensor_data_t* create_sensor_data(int id, size_t max_readings, 
                                   const char *desc)
{
    // Alocar estrutura principal
    sensor_data_t *sensor = malloc(sizeof(sensor_data_t));
    if (sensor == NULL) {
        ESP_LOGE(TAG, "Falha ao alocar estrutura sensor");
        return NULL;
    }
    
    // Inicializar campos básicos
    sensor->sensor_id = id;
    sensor->readings_count = max_readings;
    
    // Alocar array de leituras
    sensor->readings = malloc(sizeof(float) * max_readings);
    if (sensor->readings == NULL) {
        ESP_LOGE(TAG, "Falha ao alocar array de leituras");
        free(sensor);  // Limpar estrutura já alocada
        return NULL;
    }
    
    // Inicializar array com zeros
    memset(sensor->readings, 0, sizeof(float) * max_readings);
    
    // Alocar e copiar descrição
    sensor->description = malloc(strlen(desc) + 1);
    if (sensor->description == NULL) {
        ESP_LOGE(TAG, "Falha ao alocar descrição");
        free(sensor->readings);  // Limpar arrays já alocados
        free(sensor);
        return NULL;
    }
    strcpy(sensor->description, desc);
    
    ESP_LOGI(TAG, "Sensor criado: ID=%d, capacidade=%zu leituras",
             id, max_readings);
    
    return sensor;
}

/**
 * Liberar objeto sensor adequadamente
 * 
 * IMPORTANTE: Sempre liberar na ordem reversa da alocação
 * e configurar ponteiros para NULL após liberar
 */
void destroy_sensor_data(sensor_data_t **sensor_ptr)
{
    if (sensor_ptr == NULL || *sensor_ptr == NULL) {
        return;  // Ponteiro já nulo, nada a fazer
    }
    
    sensor_data_t *sensor = *sensor_ptr;
    
    ESP_LOGI(TAG, "Destruindo sensor ID=%d", sensor->sensor_id);
    
    // Liberar subestruturas primeiro
    if (sensor->description != NULL) {
        free(sensor->description);
        sensor->description = NULL;
    }
    
    if (sensor->readings != NULL) {
        free(sensor->readings);
        sensor->readings = NULL;
    }
    
    // Finalmente liberar estrutura principal
    free(sensor);
    
    // Configurar ponteiro original para NULL
    *sensor_ptr = NULL;
}

/**
 * Demonstrar heap_caps para alocação especializada
 * 
 * O ESP32 tem múltiplas regiões de memória com características diferentes.
 * heap_caps permite alocar de regiões específicas conforme requisitos.
 */
void demonstrate_heap_caps(void)
{
    ESP_LOGI(TAG, "Demonstrando heap_caps para alocação especializada");
    
    // Alocar memória que pode ser usada para DMA
    // DMA requer memória interna e específica do ESP32
    size_t dma_buffer_size = 4096;
    void *dma_buffer = heap_caps_malloc(dma_buffer_size, 
                                        MALLOC_CAP_DMA);
    
    if (dma_buffer != NULL) {
        ESP_LOGI(TAG, "Buffer DMA alocado: %d bytes em %p",
                 dma_buffer_size, dma_buffer);
        
        // Verificar capacidades da memória alocada
        uint32_t caps = heap_caps_get_allocated_size(dma_buffer);
        ESP_LOGI(TAG, "Tamanho alocado real: %lu bytes", caps);
        
        heap_caps_free(dma_buffer);
    } else {
        ESP_LOGE(TAG, "Falha ao alocar buffer DMA");
    }
    
    // Alocar memória na RAM interna rápida
    void *fast_buffer = heap_caps_malloc(1024, MALLOC_CAP_INTERNAL);
    if (fast_buffer != NULL) {
        ESP_LOGI(TAG, "Buffer rápido alocado em RAM interna");
        heap_caps_free(fast_buffer);
    }
    
    // Alocar em memória externa (se disponível)
    void *ext_buffer = heap_caps_malloc(100000, MALLOC_CAP_SPIRAM);
    if (ext_buffer != NULL) {
        ESP_LOGI(TAG, "Buffer grande alocado em SPIRAM externa");
        heap_caps_free(ext_buffer);
    } else {
        ESP_LOGW(TAG, "SPIRAM não disponível ou insuficiente");
    }
}

/**
 * Monitorar uso de memória do sistema
 * 
 * Ferramenta essencial para detectar vazamentos e problemas de fragmentação
 */
void monitor_heap_status(void)
{
    // Obter informações sobre heap
    multi_heap_info_t heap_info;
    heap_caps_get_info(&heap_info, MALLOC_CAP_DEFAULT);
    
    ESP_LOGI(TAG, "=== Status da Heap ===");
    ESP_LOGI(TAG, "Total livre: %u bytes", heap_info.total_free_bytes);
    ESP_LOGI(TAG, "Maior bloco livre: %u bytes", 
             heap_info.largest_free_block);
    ESP_LOGI(TAG, "Mínimo livre (histórico): %u bytes",
             heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT));
    ESP_LOGI(TAG, "Total alocado: %u bytes", heap_info.total_allocated_bytes);
    ESP_LOGI(TAG, "Número de blocos livres: %u", heap_info.free_blocks);
    ESP_LOGI(TAG, "Número de blocos alocados: %u", heap_info.allocated_blocks);
    
    // Calcular fragmentação
    if (heap_info.total_free_bytes > 0) {
        float fragmentation = 1.0f - 
            ((float)heap_info.largest_free_block / heap_info.total_free_bytes);
        ESP_LOGI(TAG, "Fragmentação estimada: %.1f%%", fragmentation * 100.0f);
        
        if (fragmentation > 0.5f) {
            ESP_LOGW(TAG, "Heap significativamente fragmentada!");
        }
    }
}

/**
 * Task que demonstra uso correto de memória
 */
void memory_demo_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de demonstração de memória iniciada");
    
    // Monitorar heap antes das alocações
    ESP_LOGI(TAG, "Estado inicial da heap:");
    monitor_heap_status();
    
    // Criar vários sensores
    sensor_data_t *sensors[3];
    
    for (int i = 0; i < 3; i++) {
        char desc[32];
        snprintf(desc, sizeof(desc), "Sensor de Temperatura %d", i + 1);
        
        sensors[i] = create_sensor_data(i + 1, 100, desc);
        
        if (sensors[i] == NULL) {
            ESP_LOGE(TAG, "Falha ao criar sensor %d", i + 1);
            // Limpar sensores já criados
            for (int j = 0; j < i; j++) {
                destroy_sensor_data(&sensors[j]);
            }
            vTaskDelete(NULL);
            return;
        }
        
        // Simular algumas leituras
        for (size_t j = 0; j < 10; j++) {
            sensors[i]->readings[j] = 20.0f + (float)(esp_random() % 100) / 10.0f;
        }
    }
    
    ESP_LOGI(TAG, "Todos os sensores criados com sucesso");
    
    // Monitorar heap após alocações
    ESP_LOGI(TAG, "Estado da heap após alocações:");
    monitor_heap_status();
    
    // Usar sensores por um tempo
    vTaskDelay(pdMS_TO_TICKS(5000));
    
    // Demonstrar heap_caps
    demonstrate_heap_caps();
    
    // Limpar todos os sensores adequadamente
    ESP_LOGI(TAG, "Limpando sensores...");
    for (int i = 0; i < 3; i++) {
        destroy_sensor_data(&sensors[i]);
        
        // Verificar que ponteiro foi configurado para NULL
        if (sensors[i] == NULL) {
            ESP_LOGD(TAG, "Sensor %d limpo e ponteiro configurado para NULL", 
                     i + 1);
        }
    }
    
    // Monitorar heap após limpeza
    ESP_LOGI(TAG, "Estado da heap após limpeza:");
    monitor_heap_status();
    
    ESP_LOGI(TAG, "Demonstração concluída, deletando task");
    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando gerenciamento de memória no ESP-IDF");
    
    // Criar task de demonstração
    xTaskCreate(memory_demo_task, "MemoryDemo", 4096, NULL, 5, NULL);
}

Estratégias para Prevenir Vazamentos de Memória

Prevenir vazamentos de memória requer combinação de boas práticas de codificação, ferramentas de análise e testing rigoroso. O ESP-IDF oferece várias funcionalidades que facilitam detecção e prevenção de vazamentos quando usadas consistentemente durante desenvolvimento.

A estratégia mais fundamental é estabelecer política clara de ownership para cada alocação de memória. Cada ponteiro deve ter owner claramente definido responsável por liberar a memória quando não for mais necessária. Documentar ownership em comentários e nomear funções apropriadamente torna responsabilidades explícitas.

Padrões de alocação como RAII podem ser aproximados em C através de funções create e destroy pareadas que encapsulam lifecycle completo de objetos. Sempre que você chama função create que aloca memória, deve eventualmente chamar função destroy correspondente. Esta simetria torna vazamentos mais fáceis de detectar através de code review.

✅ Ferramentas de Detecção de Vazamentos

O ESP-IDF inclui heap tracing que permite rastrear todas as alocações e desalocações em tempo de execução. Quando habilitado, o sistema registra cada malloc e free com informações sobre onde no código a operação ocorreu. Ao final de período de teste, você pode analisar quais alocações não foram correspondidas por frees, identificando vazamentos potenciais.

Para usar heap tracing, você deve primeiro habilitá-lo na configuração através do menuconfig. Então, no código da aplicação, você inicia tracing antes da seção que deseja monitorar e para tracing depois, gerando relatório de todas as alocações não liberadas. Este relatório inclui stack traces mostrando exatamente onde cada vazamento se originou.

Memory leak detection pode ser combinada com testes automatizados onde você executa funcionalidade específica repetidamente enquanto monitora uso de memória. Se memória cresce continuamente ao longo de iterações, isso indica vazamento. Esta abordagem é particularmente efetiva para detectar vazamentos pequenos que ocorrem apenas sob condições específicas.

Ferramentas de análise estática como clang static analyzer podem detectar muitos padrões de vazamentos analisando código fonte sem execução. Integrar estas ferramentas em pipeline de CI garante que problemas sejam detectados antes de chegarem a produção.