Periféricos e GPIO 🔌

Configuração e Controle de GPIO

Os pinos GPIO do ESP32 representam interface fundamental entre software e mundo físico, permitindo que você leia sensores digitais e controle atuadores. O driver GPIO do ESP-IDF oferece API completa que abstrai detalhes de registradores hardware mantendo flexibilidade necessária para implementar protocolos de comunicação customizados.

Cada pino GPIO pode ser configurado individualmente com características específicas incluindo direção, resistores pull-up ou pull-down, e tipo de interrupção. A estrutura gpio_config_t encapsula todas as opções de configuração permitindo que você especifique múltiplas configurações atomicamente através de single call para gpio_config.

A bitmask usada para especificar pinos na configuração permite configurar múltiplos pinos simultaneamente com as mesmas características. Isto é particularmente útil quando você tem banco de LEDs ou múltiplos sensores que compartilham configuração idêntica, reduzindo quantidade de código necessário e melhorando performance de inicialização.

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "GPIO_EXAMPLE";

// Definir pinos de forma organizada
#define LED_RED_PIN     GPIO_NUM_2
#define LED_GREEN_PIN   GPIO_NUM_4
#define LED_BLUE_PIN    GPIO_NUM_5
#define BUTTON_PIN      GPIO_NUM_0
#define SENSOR_PIN      GPIO_NUM_15

/**
 * Configurar múltiplos LEDs de uma vez usando bitmask
 * 
 * Esta abordagem é mais eficiente que configurar cada pino
 * individualmente quando eles compartilham mesmas características
 */
esp_err_t configure_output_leds(void)
{
    ESP_LOGI(TAG, "Configurando pinos de LED como saída");
    
    // Criar bitmask combinando múltiplos pinos
    uint64_t pin_bit_mask = (1ULL << LED_RED_PIN) | 
                            (1ULL << LED_GREEN_PIN) | 
                            (1ULL << LED_BLUE_PIN);
    
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,      // Sem interrupções
        .mode = GPIO_MODE_OUTPUT,             // Modo saída
        .pin_bit_mask = pin_bit_mask,        // Pinos a configurar
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_DISABLE,
    };
    
    esp_err_t ret = gpio_config(&io_conf);
    
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "LEDs configurados com sucesso");
        
        // Inicializar todos os LEDs desligados
        gpio_set_level(LED_RED_PIN, 0);
        gpio_set_level(LED_GREEN_PIN, 0);
        gpio_set_level(LED_BLUE_PIN, 0);
    } else {
        ESP_LOGE(TAG, "Falha ao configurar LEDs: %s", esp_err_to_name(ret));
    }
    
    return ret;
}

/**
 * Configurar pino de entrada com pull-up
 * 
 * Botões tipicamente precisam de pull-up ou pull-down para garantir
 * nível lógico definido quando botão não está pressionado
 */
esp_err_t configure_input_button(void)
{
    ESP_LOGI(TAG, "Configurando botão como entrada");
    
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,      // Configurar depois
        .mode = GPIO_MODE_INPUT,              // Modo entrada
        .pin_bit_mask = (1ULL << BUTTON_PIN),
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_ENABLE,    // Habilitar pull-up interno
    };
    
    esp_err_t ret = gpio_config(&io_conf);
    
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Botão configurado com pull-up");
    }
    
    return ret;
}

/**
 * Demonstrar leitura de pino digital
 * 
 * gpio_get_level retorna estado atual do pino (0 ou 1)
 */
void read_button_state(void)
{
    int button_state = gpio_get_level(BUTTON_PIN);
    
    if (button_state == 0) {
        ESP_LOGI(TAG, "Botão pressionado (nível baixo)");
    } else {
        ESP_LOGI(TAG, "Botão solto (nível alto)");
    }
}

/**
 * Criar padrão de LED usando timing preciso
 * 
 * Demonstra controle fino de GPIO para criar efeitos visuais
 */
void led_pattern_knight_rider(int iterations)
{
    const gpio_num_t leds[] = {LED_RED_PIN, LED_GREEN_PIN, LED_BLUE_PIN};
    const int num_leds = sizeof(leds) / sizeof(leds[0]);
    
    ESP_LOGI(TAG, "Executando padrão Knight Rider");
    
    for (int iter = 0; iter < iterations; iter++) {
        // Acender da esquerda para direita
        for (int i = 0; i < num_leds; i++) {
            gpio_set_level(leds[i], 1);
            vTaskDelay(pdMS_TO_TICKS(100));
            gpio_set_level(leds[i], 0);
        }
        
        // Acender da direita para esquerda
        for (int i = num_leds - 1; i >= 0; i--) {
            gpio_set_level(leds[i], 1);
            vTaskDelay(pdMS_TO_TICKS(100));
            gpio_set_level(leds[i], 0);
        }
    }
}

/**
 * Demonstrar configuração de GPIO para comunicação customizada
 * 
 * Open-drain é útil para barramentos I2C ou comunicação multi-master
 */
esp_err_t configure_open_drain_pin(gpio_num_t pin)
{
    ESP_LOGI(TAG, "Configurando GPIO %d como open-drain", pin);
    
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,
        .mode = GPIO_MODE_OUTPUT_OD,          // Open-drain output
        .pin_bit_mask = (1ULL << pin),
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_ENABLE,    // Pull-up necessário para open-drain
    };
    
    return gpio_config(&io_conf);
}

/**
 * Task que monitora botão e controla LEDs
 * 
 * Demonstra polling simples de entrada digital
 */
void button_led_control_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de controle iniciada");
    
    int last_button_state = 1;  // Assume botão inicialmente solto
    int led_index = 0;
    const gpio_num_t leds[] = {LED_RED_PIN, LED_GREEN_PIN, LED_BLUE_PIN};
    
    while (1) {
        int button_state = gpio_get_level(BUTTON_PIN);
        
        // Detectar borda de descida (botão pressionado)
        if (button_state == 0 && last_button_state == 1) {
            ESP_LOGI(TAG, "Botão pressionado! Alternando LED");
            
            // Desligar LED atual
            gpio_set_level(leds[led_index], 0);
            
            // Avançar para próximo LED
            led_index = (led_index + 1) % 3;
            
            // Ligar próximo LED
            gpio_set_level(leds[led_index], 1);
            
            // Debounce simples
            vTaskDelay(pdMS_TO_TICKS(50));
        }
        
        last_button_state = button_state;
        
        vTaskDelay(pdMS_TO_TICKS(10));  // Polling a cada 10ms
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando controle de GPIO");
    
    // Configurar periféricos
    configure_output_leds();
    configure_input_button();
    
    // Executar padrão de LED de demonstração
    led_pattern_knight_rider(3);
    
    // Criar task de controle interativo
    xTaskCreate(button_led_control_task, "ButtonLED", 2048, NULL, 5, NULL);
    
    ESP_LOGI(TAG, "Sistema GPIO inicializado");
}

Sistema de Interrupções GPIO

Interrupções de GPIO permitem que seu código responda instantaneamente a mudanças em pinos de entrada sem necessidade de polling contínuo. Isto reduz drasticamente latência de resposta e economiza energia ao permitir que CPU entre em modo sleep até que evento importante ocorra.

O ESP-IDF suporta interrupções em qualquer pino GPIO configurável, com capacidade de detectar bordas de subida, descida, ou ambas, além de níveis altos ou baixos. Quando interrupção é disparada, handler de interrupção é executado imediatamente, preemptando qualquer código em execução. Isto requer que handlers sejam extremamente rápidos e evitem operações bloqueantes.

O padrão recomendado para handlers de interrupção é realizar trabalho mínimo necessário no handler e delegar processamento real para task através de fila ou semáforo. Isto mantém handler rápido enquanto permite que processamento complexo ocorra em contexto de task normal onde todas as funcionalidades do FreeRTOS estão disponíveis.

🚨 Regras Estritas para ISRs

Interrupt Service Routines têm restrições rígidas que devem ser respeitadas religiosamente. Violações destas regras causam comportamento imprevisível incluindo crashes, corrupção de dados e deadlocks difíceis de diagnosticar.

ISRs devem executar o mais rapidamente possível, idealmente em poucos microsegundos. Cada microsegundo gasto em ISR é tempo que outras interrupções podem ser bloqueadas, potencialmente causando perda de eventos. Se seu handler precisa de mais que alguns microsegundos, você está fazendo demais na ISR.

Chamadas bloqueantes são absolutamente proibidas em ISRs. Isto inclui vTaskDelay, xSemaphoreTake sem usar versão FromISR, printf padrão, e qualquer função que possa aguardar recursos. Violações causam travamento do sistema porque scheduler não pode executar enquanto ISR está ativa.

Apenas versões específicas FromISR das APIs FreeRTOS podem ser usadas em ISRs. Estas versões são otimizadas para contexto de interrupção e não tentam realizar operações bloqueantes. Usar versões normais em ISR causa comportamento indefinido.

Variáveis compartilhadas entre ISRs e tasks normais devem ser declaradas volatile para prevenir otimizações do compilador que assumem que valor não muda. Sem volatile, compilador pode cachear valor em registrador e nunca ver mudanças feitas pela ISR.

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_timer.h"

static const char *TAG = "ISR_EXAMPLE";

#define BUTTON_PIN GPIO_NUM_0
#define LED_PIN    GPIO_NUM_2

// Fila para comunicação ISR -> Task
static QueueHandle_t gpio_evt_queue = NULL;

// Variável para debounce (acessada pela ISR)
static volatile uint64_t last_interrupt_time = 0;

/**
 * Estrutura de evento GPIO para passar da ISR para task
 */
typedef struct {
    gpio_num_t pin;
    uint64_t timestamp;
    int level;
} gpio_event_t;

/**
 * ISR - Interrupt Service Routine
 * 
 * ATENÇÃO: Esta função executa em contexto de interrupção!
 * - Deve ser EXTREMAMENTE RÁPIDA (poucos microsegundos)
 * - NÃO pode chamar funções bloqueantes
 * - NÃO pode usar printf/ESP_LOGI normal
 * - Deve usar apenas funções marcadas como ISR-safe
 * - Atributo IRAM_ATTR garante que código fica em RAM rápida
 */
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
    // Obter tempo atual para timestamp e debounce
    uint64_t current_time = esp_timer_get_time();
    
    // Implementar debounce simples
    // Ignorar interrupções muito próximas (< 50ms)
    const uint64_t DEBOUNCE_TIME_US = 50000;
    if ((current_time - last_interrupt_time) < DEBOUNCE_TIME_US) {
        return;  // Ignora bounce
    }
    
    last_interrupt_time = current_time;
    
    // Preparar evento para enviar à task
    gpio_event_t evt;
    evt.pin = (gpio_num_t)(int)arg;
    evt.timestamp = current_time;
    evt.level = gpio_get_level(evt.pin);
    
    // Enviar evento para fila usando versão ISR-safe
    // NÃO usar xQueueSend() normal - apenas xQueueSendFromISR()
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(gpio_evt_queue, &evt, &xHigherPriorityTaskWoken);
    
    // Se envio para fila acordou task de prioridade mais alta,
    // solicitar context switch após retornar da ISR
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
    
    // ISR deve retornar rapidamente
    // Todo processamento real acontece na task
}

/**
 * Task que processa eventos GPIO vindos da ISR
 * 
 * Esta task executa em contexto normal onde todas as
 * funcionalidades do FreeRTOS estão disponíveis
 */
void gpio_event_processing_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de processamento de eventos iniciada");
    
    gpio_event_t evt;
    int event_count = 0;
    
    while (1) {
        // Aguardar evento da fila
        // Bloqueia indefinidamente até evento chegar
        if (xQueueReceive(gpio_evt_queue, &evt, portMAX_DELAY)) {
            event_count++;
            
            // Calcular latência desde interrupção
            uint64_t latency_us = esp_timer_get_time() - evt.timestamp;
            
            ESP_LOGI(TAG, "Evento #%d: Pin=%d Level=%d Latência=%lluμs",
                     event_count, evt.pin, evt.level, latency_us);
            
            // Processar evento (pode fazer operações complexas aqui)
            if (evt.level == 0) {  // Botão pressionado
                ESP_LOGI(TAG, "Botão pressionado! Alternando LED");
                
                // Alternar LED
                static int led_state = 0;
                led_state = !led_state;
                gpio_set_level(LED_PIN, led_state);
                
                // Processamento adicional pode ser feito aqui
                // sem restrições de ISR
                vTaskDelay(pdMS_TO_TICKS(10));  // Exemplo de delay
            }
        }
    }
}

/**
 * Configurar GPIO com interrupção
 */
esp_err_t configure_gpio_interrupt(void)
{
    ESP_LOGI(TAG, "Configurando interrupção GPIO");
    
    // Criar fila antes de habilitar interrupções
    gpio_evt_queue = xQueueCreate(10, sizeof(gpio_event_t));
    if (gpio_evt_queue == NULL) {
        ESP_LOGE(TAG, "Falha ao criar fila");
        return ESP_FAIL;
    }
    
    // Configurar pino de entrada
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_NEGEDGE,      // Interrupção na borda de descida
        .mode = GPIO_MODE_INPUT,
        .pin_bit_mask = (1ULL << BUTTON_PIN),
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_ENABLE,    // Pull-up interno
    };
    
    esp_err_t ret = gpio_config(&io_conf);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha ao configurar GPIO: %s", esp_err_to_name(ret));
        return ret;
    }
    
    // Configurar LED de saída
    gpio_config_t led_conf = {
        .intr_type = GPIO_INTR_DISABLE,
        .mode = GPIO_MODE_OUTPUT,
        .pin_bit_mask = (1ULL << LED_PIN),
    };
    gpio_config(&led_conf);
    
    // Instalar serviço de ISR do GPIO
    // Deve ser chamado antes de adicionar handlers individuais
    ret = gpio_install_isr_service(0);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha ao instalar serviço ISR: %s", 
                 esp_err_to_name(ret));
        return ret;
    }
    
    // Adicionar handler específico para este pino
    // Passar número do pino como argumento para ISR
    ret = gpio_isr_handler_add(BUTTON_PIN, gpio_isr_handler, 
                                (void *)BUTTON_PIN);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha ao adicionar handler ISR: %s", 
                 esp_err_to_name(ret));
        return ret;
    }
    
    ESP_LOGI(TAG, "Interrupção GPIO configurada com sucesso");
    return ESP_OK;
}

/**
 * Demonstrar diferentes tipos de interrupção
 */
void demonstrate_interrupt_types(void)
{
    ESP_LOGI(TAG, "Demonstrando tipos de interrupção");
    
    // Mudar para detectar borda de subida
    ESP_LOGI(TAG, "Configurando para borda de subida");
    gpio_set_intr_type(BUTTON_PIN, GPIO_INTR_POSEDGE);
    vTaskDelay(pdMS_TO_TICKS(5000));
    
    // Mudar para detectar ambas as bordas
    ESP_LOGI(TAG, "Configurando para ambas as bordas");
    gpio_set_intr_type(BUTTON_PIN, GPIO_INTR_ANYEDGE);
    vTaskDelay(pdMS_TO_TICKS(5000));
    
    // Voltar para borda de descida
    ESP_LOGI(TAG, "Voltando para borda de descida");
    gpio_set_intr_type(BUTTON_PIN, GPIO_INTR_NEGEDGE);
}

/**
 * Demonstrar desabilitação temporária de interrupções
 * Útil para seções críticas ou quando processando eventos
 */
void demonstrate_interrupt_disable(void)
{
    ESP_LOGI(TAG, "Desabilitando interrupções por 5 segundos");
    
    // Desabilitar interrupções do pino
    gpio_intr_disable(BUTTON_PIN);
    
    ESP_LOGI(TAG, "Interrupções desabilitadas - botão não responde");
    vTaskDelay(pdMS_TO_TICKS(5000));
    
    // Reabilitar interrupções
    gpio_intr_enable(BUTTON_PIN);
    ESP_LOGI(TAG, "Interrupções reabilitadas");
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando sistema de interrupções GPIO");
    
    // Configurar GPIO com interrupções
    esp_err_t ret = configure_gpio_interrupt();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha na configuração, abortando");
        return;
    }
    
    // Criar task de processamento de eventos
    xTaskCreate(gpio_event_processing_task, "GPIOEvents", 
                3072, NULL, 10, NULL);
    
    ESP_LOGI(TAG, "Sistema de interrupções inicializado");
    ESP_LOGI(TAG, "Pressione o botão para ver eventos sendo processados");
    
    // Task principal continua executando outras operações
    vTaskDelay(pdMS_TO_TICKS(10000));
    
    // Demonstrar mudança de tipo de interrupção
    demonstrate_interrupt_types();
    
    // Demonstrar desabilitação temporária
    vTaskDelay(pdMS_TO_TICKS(5000));
    demonstrate_interrupt_disable();
}

Conversores ADC e DAC

O ESP32 integra conversores analógico-digital que permitem ler sinais analógicos de sensores, transformando voltagens contínuas em valores digitais que seu código pode processar. Compreender características e limitações do ADC é essencial para obter leituras precisas e confiáveis em aplicações IoT.

O ADC do ESP32 possui resolução de 12 bits, teoricamente capaz de distinguir 4096 níveis diferentes de voltagem. No entanto, a resolução efetiva é menor devido a ruído inerente e não-linearidades do conversor. O ESP-IDF oferece sistema de calibração que melhora precisão compensando variações entre chips individuais e efeitos de temperatura.

Existem dois ADCs independentes no ESP32, cada um com múltiplos canais multiplexados. ADC1 possui 8 canais enquanto ADC2 tem 10 canais, mas ADC2 não pode ser usado quando WiFi está ativo devido a conflitos de hardware internos. Esta limitação é importante considerar ao planejar pinout de projetos que usam WiFi.

📊 Atenuação e Faixas de Voltagem

O ADC do ESP32 possui faixa de entrada nativa de 0 a aproximadamente 1.1V, mas pode medir voltagens mais altas usando atenuação configurável. Diferentes níveis de atenuação expandem a faixa mensurável sacrificando precisão nas leituras individuais.

Atenuação de 0dB oferece melhor precisão mas limita medições a 0-1.1V, ideal para sinais de baixa voltagem como alguns sensores analógicos. Atenuação de 2.5dB expande faixa para aproximadamente 1.5V, adequada para leituras de bateria em sistemas de baixa potência.

Atenuação de 6dB permite medir até cerca de 2.2V, cobrindo muitos sensores analógicos comuns que operam em 0-2V. Finalmente, atenuação de 11dB expande faixa para aproximadamente 3.3V, permitindo medição da faixa completa de voltagem lógica do ESP32.

A escolha da atenuação apropriada envolve trade-off entre faixa e precisão. Use menor atenuação que acomode seu sinal para maximizar resolução efetiva. Para sinais que variam dinamicamente, você pode até ajustar atenuação em tempo de execução conforme amplitude do sinal muda.

#include "driver/adc.h"
#include "esp_adc_cal.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <math.h>

static const char *TAG = "ADC_EXAMPLE";

// Canal ADC para leitura (GPIO34 = ADC1_CHANNEL_6)
#define ADC_CHANNEL     ADC1_CHANNEL_6
#define ADC_ATTEN       ADC_ATTEN_DB_11
#define ADC_WIDTH       ADC_WIDTH_BIT_12

// Características de calibração do ADC
static esp_adc_cal_characteristics_t *adc_chars = NULL;

/**
 * Inicializar e calibrar ADC
 * 
 * Calibração compensa variações entre chips individuais
 * melhorando significativamente a precisão das leituras
 */
esp_err_t initialize_adc(void)
{
    ESP_LOGI(TAG, "Inicializando ADC");
    
    // Configurar largura de bits do ADC1
    esp_err_t ret = adc1_config_width(ADC_WIDTH);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha ao configurar largura ADC: %s", 
                 esp_err_to_name(ret));
        return ret;
    }
    
    // Configurar atenuação do canal específico
    ret = adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha ao configurar atenuação: %s", 
                 esp_err_to_name(ret));
        return ret;
    }
    
    // Alocar estrutura de características de calibração
    adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
    if (adc_chars == NULL) {
        ESP_LOGE(TAG, "Falha ao alocar estrutura de calibração");
        return ESP_ERR_NO_MEM;
    }
    
    // Caracterizar ADC com base em eFuse ou valores padrão
    esp_adc_cal_value_t val_type = esp_adc_cal_characterize(
        ADC_UNIT_1,
        ADC_ATTEN,
        ADC_WIDTH,
        1100,           // Voltagem de referência padrão (mV)
        adc_chars
    );
    
    // Informar tipo de calibração disponível
    if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
        ESP_LOGI(TAG, "Calibração: eFuse Two Point");
    } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
        ESP_LOGI(TAG, "Calibração: eFuse Vref");
    } else {
        ESP_LOGI(TAG, "Calibração: Valores padrão");
    }
    
    return ESP_OK;
}

/**
 * Ler ADC com calibração aplicada
 * 
 * Retorna voltagem em milivolts usando características de calibração
 */
uint32_t read_adc_calibrated(void)
{
    // Ler valor bruto do ADC (0-4095 para 12 bits)
    int raw_value = adc1_get_raw(ADC_CHANNEL);
    
    // Converter para voltagem calibrada em mV
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(raw_value, adc_chars);
    
    return voltage_mv;
}

/**
 * Ler múltiplas amostras e calcular média
 * 
 * Multisampling reduz ruído e melhora estabilidade das leituras
 */
uint32_t read_adc_multisampled(int num_samples)
{
    uint64_t sum = 0;
    
    for (int i = 0; i < num_samples; i++) {
        sum += read_adc_calibrated();
        
        // Pequeno delay entre amostras
        vTaskDelay(pdMS_TO_TICKS(1));
    }
    
    return (uint32_t)(sum / num_samples);
}

/**
 * Calcular valor RMS para sinais AC
 * 
 * Útil para medir sinais alternados como corrente AC
 */
float calculate_rms_voltage(int num_samples, int sample_period_ms)
{
    float sum_squares = 0.0f;
    
    for (int i = 0; i < num_samples; i++) {
        uint32_t voltage_mv = read_adc_calibrated();
        float voltage_v = voltage_mv / 1000.0f;
        
        sum_squares += voltage_v * voltage_v;
        
        vTaskDelay(pdMS_TO_TICKS(sample_period_ms));
    }
    
    float mean_square = sum_squares / num_samples;
    float rms = sqrtf(mean_square);
    
    return rms;
}

/**
 * Converter leitura ADC para valor de sensor específico
 * 
 * Exemplo: Sensor de temperatura LM35 (10mV/°C)
 */
float adc_to_temperature_lm35(uint32_t voltage_mv)
{
    // LM35 produz 10mV por grau Celsius
    float temperature_c = voltage_mv / 10.0f;
    
    return temperature_c;
}

/**
 * Converter leitura ADC para porcentagem de bateria
 * 
 * Exemplo: Bateria Li-ion (3.0V vazia, 4.2V cheia)
 */
float adc_to_battery_percentage(uint32_t voltage_mv)
{
    const float BATTERY_MIN_MV = 3000.0f;
    const float BATTERY_MAX_MV = 4200.0f;
    
    float voltage = (float)voltage_mv;
    
    // Limitar aos valores min/max
    if (voltage < BATTERY_MIN_MV) voltage = BATTERY_MIN_MV;
    if (voltage > BATTERY_MAX_MV) voltage = BATTERY_MAX_MV;
    
    // Calcular porcentagem
    float percentage = ((voltage - BATTERY_MIN_MV) / 
                        (BATTERY_MAX_MV - BATTERY_MIN_MV)) * 100.0f;
    
    return percentage;
}

/**
 * Task de aquisição contínua de dados analógicos
 * 
 * Demonstra padrão típico para monitoramento de sensores
 */
void adc_monitoring_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de monitoramento ADC iniciada");
    
    // Buffer circular para histórico de leituras
    const int HISTORY_SIZE = 10;
    uint32_t voltage_history[HISTORY_SIZE] = {0};
    int history_index = 0;
    
    while (1) {
        // Ler ADC com multisampling para reduzir ruído
        uint32_t voltage_mv = read_adc_multisampled(10);
        
        // Armazenar no histórico
        voltage_history[history_index] = voltage_mv;
        history_index = (history_index + 1) % HISTORY_SIZE;
        
        // Calcular média móvel
        uint64_t sum = 0;
        for (int i = 0; i < HISTORY_SIZE; i++) {
            sum += voltage_history[i];
        }
        uint32_t average_mv = sum / HISTORY_SIZE;
        
        // Converter para valores úteis
        float temp_c = adc_to_temperature_lm35(voltage_mv);
        float battery_pct = adc_to_battery_percentage(voltage_mv);
        
        // Registrar leituras
        ESP_LOGI(TAG, "Voltagem: %lumV (média: %lumV)", 
                 voltage_mv, average_mv);
        ESP_LOGI(TAG, "  Como temperatura LM35: %.1f°C", temp_c);
        ESP_LOGI(TAG, "  Como bateria Li-ion: %.1f%%", battery_pct);
        
        // Detectar mudanças significativas
        static uint32_t last_voltage = 0;
        int32_t change = abs((int32_t)voltage_mv - (int32_t)last_voltage);
        
        if (change > 100) {  // Mudança > 100mV
            ESP_LOGW(TAG, "Mudança significativa detectada: %ldmV", change);
        }
        
        last_voltage = voltage_mv;
        
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

/**
 * Demonstrar diferentes configurações de atenuação
 */
void demonstrate_attenuation_ranges(void)
{
    ESP_LOGI(TAG, "Testando diferentes níveis de atenuação");
    
    adc_atten_t attenuations[] = {
        ADC_ATTEN_DB_0,
        ADC_ATTEN_DB_2_5,
        ADC_ATTEN_DB_6,
        ADC_ATTEN_DB_11
    };
    
    const char *atten_names[] = {
        "0dB (0-1.1V)",
        "2.5dB (0-1.5V)",
        "6dB (0-2.2V)",
        "11dB (0-3.3V)"
    };
    
    for (int i = 0; i < 4; i++) {
        ESP_LOGI(TAG, "Configurando atenuação: %s", atten_names[i]);
        
        // Reconfigurar atenuação
        adc1_config_channel_atten(ADC_CHANNEL, attenuations[i]);
        
        // Recaracterizar ADC para nova atenuação
        esp_adc_cal_characterize(ADC_UNIT_1, attenuations[i],
                                 ADC_WIDTH, 1100, adc_chars);
        
        // Fazer algumas leituras
        vTaskDelay(pdMS_TO_TICKS(100));  // Permitir estabilização
        
        for (int j = 0; j < 3; j++) {
            uint32_t voltage = read_adc_calibrated();
            ESP_LOGI(TAG, "  Leitura %d: %lumV", j + 1, voltage);
            vTaskDelay(pdMS_TO_TICKS(100));
        }
        
        ESP_LOGI(TAG, "");
    }
    
    // Restaurar configuração padrão
    adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN,
                            ADC_WIDTH, 1100, adc_chars);
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando conversores ADC");
    
    // Inicializar ADC com calibração
    esp_err_t ret = initialize_adc();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Falha na inicialização ADC, abortando");
        return;
    }
    
    // Demonstrar diferentes atenuações
    demonstrate_attenuation_ranges();
    
    // Criar task de monitoramento contínuo
    xTaskCreate(adc_monitoring_task, "ADC_Monitor", 
                4096, NULL, 5, NULL);
    
    ESP_LOGI(TAG, "Sistema ADC inicializado");
}