Sistema Operacional FreeRTOS ⚙️

Fundamentos de Tasks e Escalonamento

O FreeRTOS implementa sistema de escalonamento preemptivo baseado em prioridades que permite execução aparentemente simultânea de múltiplas tasks no mesmo processador. Compreender como o escalonador funciona é fundamental para projetar aplicações responsivas e eficientes que aproveitem melhor os recursos disponíveis.

Tasks no FreeRTOS são unidades independentes de execução análogas a threads em sistemas operacionais tradicionais. Cada task possui sua própria stack, contexto de registradores e estado de execução. O escalonador alterna rapidamente entre tasks ativas criando ilusão de paralelismo em processadores single-core, ou implementando paralelismo real nos dois núcleos do ESP32.

O conceito de prioridade é central para o escalonamento no FreeRTOS. Tasks com prioridade mais alta sempre executam antes de tasks com prioridade mais baixa. Quando múltiplas tasks com mesma prioridade estão prontas para executar, o escalonador alterna entre elas em regime round-robin, garantindo que cada task receba fatia justa de tempo de CPU.

⚠️ Estados de uma Task

Cada task no FreeRTOS existe em um de quatro estados possíveis que determinam seu comportamento e elegibilidade para execução. Compreender estes estados e as transições entre eles é essencial para debug de problemas de concorrência e otimização de performance.

O estado Running indica que a task está atualmente executando no processador. Em sistemas single-core apenas uma task pode estar neste estado simultaneamente, enquanto sistemas dual-core como ESP32 podem ter até duas tasks running ao mesmo tempo, uma em cada núcleo.

Tasks no estado Ready estão prontas para executar mas aguardando sua vez com o processador. Estas tasks não estão bloqueadas aguardando recursos externos e executariam imediatamente se tivessem prioridade suficiente e o processador estivesse disponível.

O estado Blocked indica que a task está aguardando evento externo como timeout, semáforo ou dado em fila. Tasks bloqueadas não consomem tempo de CPU e permanecem neste estado até que a condição de bloqueio seja satisfeita, quando automaticamente transitam para estado Ready.

Finalmente, estado Suspended indica que a task foi explicitamente suspensa e não será escalonada até ser explicitamente retomada. Este estado é útil para desabilitar temporariamente funcionalidades sem destruir a task completamente.

Criação e Gerenciamento de Tasks

Criar tasks no FreeRTOS envolve mais que simplesmente chamar xTaskCreate. Você deve considerar cuidadosamente alocação de stack, prioridades relativas, afinidade de CPU e gerenciamento de lifecycle da task. Decisões tomadas durante criação impactam significativamente comportamento e performance da aplicação.

A função xTaskCreate aloca automaticamente stack para a task e inicializa estruturas de controle necessárias. O tamanho da stack especificado deve ser suficiente para todas as variáveis locais, parâmetros de função e espaço para contexto de interrupção. Subdimensionar stack causa crashes difíceis de debug, enquanto superdimensionar desperdiça memória preciosa.

Prioridades devem ser atribuídas considerando criticidade temporal de cada task. Tasks que respondem a eventos externos críticos requerem prioridade alta para garantir latência mínima. Tasks de processamento em background devem ter prioridade baixa para não interferir com operações críticas do sistema.

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

static const char *TAG = "TASK_MANAGEMENT";

// Handles para controle das tasks
static TaskHandle_t xHighPriorityTask = NULL;
static TaskHandle_t xLowPriorityTask = NULL;

/**
 * Task de alta prioridade para processamento crítico
 * 
 * Esta task responde a eventos críticos que requerem latência mínima.
 * Sua prioridade alta garante que ela preempte outras tasks quando
 * necessário processar eventos importantes.
 */
void high_priority_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de alta prioridade iniciada no núcleo %d", 
             xPortGetCoreID());
    
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xFrequency = pdMS_TO_TICKS(10); // 10ms
    
    while (1) {
        // Processamento crítico aqui
        // Esta task executa a cada 10ms com precisão alta
        
        ESP_LOGD(TAG, "Processamento crítico executando");
        
        // vTaskDelayUntil mantém frequência precisa
        // Compensa drift acumulado ao longo do tempo
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

/**
 * Task de baixa prioridade para processamento em background
 * 
 * Esta task executa processamento não-crítico que pode ser interrompido
 * por tasks de prioridade mais alta sem problemas. Ela usa taskYIELD()
 * para ceder CPU voluntariamente quando possível.
 */
void low_priority_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de baixa prioridade iniciada no núcleo %d", 
             xPortGetCoreID());
    
    while (1) {
        // Processamento não-crítico
        ESP_LOGD(TAG, "Processamento em background");
        
        // Simular trabalho computacionalmente intensivo
        for (int i = 0; i < 1000000; i++) {
            // Ceder CPU periodicamente para evitar starving de outras tasks
            if (i % 10000 == 0) {
                taskYIELD(); // Cede CPU voluntariamente
            }
        }
        
        vTaskDelay(pdMS_TO_TICKS(100)); // 100ms entre iterações
    }
}

/**
 * Demonstração de criação e gerenciamento de tasks
 */
void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando gerenciamento de tasks");
    
    // Criar task de alta prioridade no núcleo 1
    // Stack maior para acomodar logging e processamento
    BaseType_t xReturned = xTaskCreatePinnedToCore(
        high_priority_task,      // Função da task
        "HighPriority",          // Nome para debug
        4096,                    // Stack size em bytes
        NULL,                    // Parâmetro
        10,                      // Prioridade alta
        &xHighPriorityTask,      // Handle para controle
        1                        // Núcleo 1 (PRO_CPU)
    );
    
    if (xReturned != pdPASS) {
        ESP_LOGE(TAG, "Falha ao criar task de alta prioridade");
        return;
    }
    
    // Criar task de baixa prioridade no núcleo 0
    xReturned = xTaskCreatePinnedToCore(
        low_priority_task,
        "LowPriority",
        2048,                    // Stack menor - processamento simples
        NULL,
        1,                       // Prioridade baixa
        &xLowPriorityTask,
        0                        // Núcleo 0 (APP_CPU)
    );
    
    if (xReturned != pdPASS) {
        ESP_LOGE(TAG, "Falha ao criar task de baixa prioridade");
        // Limpar task de alta prioridade já criada
        vTaskDelete(xHighPriorityTask);
        return;
    }
    
    // Demonstrar consulta de estado das tasks
    vTaskDelay(pdMS_TO_TICKS(1000));
    
    ESP_LOGI(TAG, "Estado das tasks após 1 segundo:");
    
    // Obter informações sobre as tasks
    TaskStatus_t xTaskDetails;
    
    vTaskGetInfo(xHighPriorityTask, &xTaskDetails, pdTRUE, eInvalid);
    ESP_LOGI(TAG, "Task de alta prioridade:");
    ESP_LOGI(TAG, "  Estado: %d", xTaskDetails.eCurrentState);
    ESP_LOGI(TAG, "  Stack livre: %u bytes", xTaskDetails.usStackHighWaterMark);
    
    vTaskGetInfo(xLowPriorityTask, &xTaskDetails, pdTRUE, eInvalid);
    ESP_LOGI(TAG, "Task de baixa prioridade:");
    ESP_LOGI(TAG, "  Estado: %d", xTaskDetails.eCurrentState);
    ESP_LOGI(TAG, "  Stack livre: %u bytes", xTaskDetails.usStackHighWaterMark);
    
    // Demonstrar suspensão e retomada de tasks
    ESP_LOGI(TAG, "Suspendendo task de baixa prioridade por 2 segundos");
    vTaskSuspend(xLowPriorityTask);
    
    vTaskDelay(pdMS_TO_TICKS(2000));
    
    ESP_LOGI(TAG, "Retomando task de baixa prioridade");
    vTaskResume(xLowPriorityTask);
    
    // app_main retorna mas tasks continuam executando
    ESP_LOGI(TAG, "Configuração completa, tasks executando indefinidamente");
}

Mecanismos de Sincronização

Quando múltiplas tasks acessam recursos compartilhados, mecanismos de sincronização tornam-se essenciais para prevenir race conditions e garantir consistência de dados. O FreeRTOS oferece vários primitivos de sincronização, cada um adequado para diferentes padrões de acesso e requisitos de performance.

Mutexes são o mecanismo mais fundamental para exclusão mútua, garantindo que apenas uma task acesse recurso compartilhado por vez. Quando uma task adquire mutex, outras tasks que tentam adquiri-lo bloqueiam até que seja liberado. Mutexes no FreeRTOS implementam herança de prioridade para prevenir inversão de prioridade, problema onde task de alta prioridade é bloqueada indefinidamente por task de baixa prioridade.

Semáforos são ferramentas mais genéricas que podem ser usadas para sincronização e sinalização entre tasks. Semáforos binários funcionam como mutexes simplificados, enquanto semáforos contadores permitem controlar acesso a pools de recursos idênticos. Semáforos são particularmente úteis para sincronizar tasks com interrupções de hardware.

🚨 Deadlocks e Como Evitá-los

Deadlock ocorre quando duas ou mais tasks ficam bloqueadas indefinidamente aguardando recursos que estão sendo mantidos umas pelas outras. Esta situação representa falha crítica onde o sistema trava parcialmente, com tasks afetadas incapazes de progredir.

Considere cenário onde Task A mantém Mutex 1 e tenta adquirir Mutex 2, enquanto Task B mantém Mutex 2 e tenta adquirir Mutex 1. Ambas as tasks ficam bloqueadas indefinidamente criando deadlock clássico.

Prevenir deadlocks requer disciplina rigorosa no design do sistema. A estratégia mais efetiva é estabelecer ordem global de aquisição de locks que todas as tasks devem respeitar. Se todos sempre adquirem Mutex 1 antes de Mutex 2, deadlock circular torna-se impossível.

Timeouts em operações de aquisição de locks fornecem segunda linha de defesa. Se uma task não consegue adquirir lock necessário dentro de tempo razoável, ela pode liberar todos os locks que mantém e tentar novamente mais tarde. Esta abordagem previne bloqueios indefinidos embora possa reduzir eficiência.

Ferramentas de análise estática podem detectar potenciais deadlocks examinando padrões de aquisição de locks no código. Investir tempo em análise durante desenvolvimento economiza horas de debugging frustrante em produção quando deadlocks manifestam sob condições específicas de timing.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"

static const char *TAG = "SYNC_EXAMPLE";

// Recursos compartilhados protegidos por mutexes
static SemaphoreHandle_t xMutexSensor1 = NULL;
static SemaphoreHandle_t xMutexSensor2 = NULL;
static int sensor1_data = 0;
static int sensor2_data = 0;

/**
 * Exemplo INCORRETO que pode causar deadlock
 * NÃO use este padrão em código de produção!
 */
void task_deadlock_exemplo_ruim(void *pvParameters)
{
    while (1) {
        // Task A adquire mutex 1 primeiro
        if (xSemaphoreTake(xMutexSensor1, portMAX_DELAY) == pdTRUE) {
            ESP_LOGW(TAG, "Task A: Adquiriu mutex sensor1");
            vTaskDelay(pdMS_TO_TICKS(100)); // Simular processamento
            
            // Agora tenta adquirir mutex 2
            // Se Task B já tem mutex 2 e quer mutex 1 = DEADLOCK!
            if (xSemaphoreTake(xMutexSensor2, portMAX_DELAY) == pdTRUE) {
                ESP_LOGI(TAG, "Task A: Adquiriu ambos os mutexes");
                // Processar dados
                xSemaphoreGive(xMutexSensor2);
            }
            xSemaphoreGive(xMutexSensor1);
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

/**
 * Exemplo CORRETO usando ordem consistente de aquisição
 * Este padrão previne deadlocks através de disciplina rigorosa
 */
void task_safe_locking(void *pvParameters)
{
    const char *task_name = (const char *)pvParameters;
    
    while (1) {
        // SEMPRE adquirir mutexes na mesma ordem global
        // Ordem definida: sensor1 antes de sensor2
        
        ESP_LOGD(TAG, "%s: Tentando adquirir mutex sensor1", task_name);
        if (xSemaphoreTake(xMutexSensor1, pdMS_TO_TICKS(1000)) == pdTRUE) {
            ESP_LOGD(TAG, "%s: Adquiriu mutex sensor1", task_name);
            
            ESP_LOGD(TAG, "%s: Tentando adquirir mutex sensor2", task_name);
            if (xSemaphoreTake(xMutexSensor2, pdMS_TO_TICKS(1000)) == pdTRUE) {
                ESP_LOGI(TAG, "%s: Processando ambos os sensores", task_name);
                
                // Região crítica - acesso exclusivo a ambos os recursos
                sensor1_data++;
                sensor2_data += sensor1_data;
                
                ESP_LOGI(TAG, "%s: Sensor1=%d, Sensor2=%d", 
                         task_name, sensor1_data, sensor2_data);
                
                // Liberar na ordem reversa (boa prática)
                xSemaphoreGive(xMutexSensor2);
                ESP_LOGD(TAG, "%s: Liberou mutex sensor2", task_name);
            } else {
                ESP_LOGW(TAG, "%s: Timeout aguardando sensor2", task_name);
            }
            
            xSemaphoreGive(xMutexSensor1);
            ESP_LOGD(TAG, "%s: Liberou mutex sensor1", task_name);
        } else {
            ESP_LOGW(TAG, "%s: Timeout aguardando sensor1", task_name);
        }
        
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

/**
 * Exemplo usando try-lock com timeout e rollback
 * Abordagem defensiva que evita bloqueios indefinidos
 */
esp_err_t safe_acquire_both_locks(TickType_t timeout)
{
    TickType_t start_time = xTaskGetTickCount();
    TickType_t remaining_time = timeout;
    
    // Tentar adquirir primeiro mutex
    if (xSemaphoreTake(xMutexSensor1, remaining_time) != pdTRUE) {
        ESP_LOGW(TAG, "Falha ao adquirir mutex sensor1");
        return ESP_ERR_TIMEOUT;
    }
    
    // Calcular tempo restante
    TickType_t elapsed = xTaskGetTickCount() - start_time;
    if (elapsed >= timeout) {
        xSemaphoreGive(xMutexSensor1);
        return ESP_ERR_TIMEOUT;
    }
    remaining_time = timeout - elapsed;
    
    // Tentar adquirir segundo mutex com timeout ajustado
    if (xSemaphoreTake(xMutexSensor2, remaining_time) != pdTRUE) {
        ESP_LOGW(TAG, "Falha ao adquirir mutex sensor2, liberando sensor1");
        xSemaphoreGive(xMutexSensor1); // IMPORTANTE: rollback!
        return ESP_ERR_TIMEOUT;
    }
    
    ESP_LOGI(TAG, "Ambos os locks adquiridos com sucesso");
    return ESP_OK;
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando sincronização segura com mutexes");
    
    // Criar mutexes
    xMutexSensor1 = xSemaphoreCreateMutex();
    xMutexSensor2 = xSemaphoreCreateMutex();
    
    if (xMutexSensor1 == NULL || xMutexSensor2 == NULL) {
        ESP_LOGE(TAG, "Falha ao criar mutexes");
        return;
    }
    
    // Criar múltiplas tasks que competem pelos mesmos recursos
    // Todas seguem ordem consistente de aquisição
    xTaskCreate(task_safe_locking, "TaskA", 3072, "TaskA", 5, NULL);
    xTaskCreate(task_safe_locking, "TaskB", 3072, "TaskB", 5, NULL);
    xTaskCreate(task_safe_locking, "TaskC", 3072, "TaskC", 5, NULL);
    
    ESP_LOGI(TAG, "Tasks de sincronização criadas");
}

Comunicação Entre Tasks via Filas

Filas representam mecanismo elegante e seguro para comunicação entre tasks, permitindo transferência de dados de forma assíncrona sem necessidade de sincronização explícita através de mutexes. O FreeRTOS implementa filas thread-safe que gerenciam automaticamente bloqueio e desbloqueio de tasks conforme dados são enviados e recebidos.

Filas funcionam como buffers FIFO onde produtores inserem dados em uma extremidade e consumidores removem da outra. O tamanho da fila e o tamanho de cada item são especificados durante criação, permitindo otimização de uso de memória baseado nas necessidades específicas da aplicação. Quando a fila está cheia, tentativas de envio bloqueiam até que espaço fique disponível ou timeout expire.

Este padrão de comunicação é particularmente útil para desacoplar produtores de consumidores de dados, permitindo que operem em velocidades diferentes sem perda de informação. Sensors tasks podem alimentar fila com leituras à medida que ficam disponíveis, enquanto processing task consome dados no seu próprio ritmo sem bloquear aquisição contínua.

📬 Padrões Comuns de Uso de Filas

O padrão produtor-consumidor é implementação clássica usando filas onde uma ou mais tasks produtoras geram dados que são processados por tasks consumidoras. Este desacoplamento temporal permite que cada lado opere em sua própria cadência otimizando throughput geral do sistema.

Filas podem implementar pools de trabalho onde múltiplas worker tasks consomem tarefas de fila comum. Quando trabalho chega, primeira worker disponível processa, proporcionando balanceamento automático de carga entre workers. Este padrão é ideal para processar requisições de rede ou processar lotes de dados de sensores.

O padrão de broadcast pode ser implementado usando múltiplas filas onde uma task produtora envia cópia dos mesmos dados para várias filas consumidas por tasks diferentes. Isto permite que múltiplos subsistemas reajam ao mesmo evento mantendo desacoplamento.

Filas também funcionam como buffers entre domínios de tempo diferentes, como entre interrupt service routine que captura dados rapidamente e task que processa dados mais lentamente. A ISR envia dados para fila sem bloquear, e task consome quando capaz.

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

static const char *TAG = "QUEUE_EXAMPLE";

// Definir estrutura de dados para comunicação via fila
typedef struct {
    int sensor_id;
    float value;
    uint64_t timestamp;
    char unit[8];
} sensor_reading_t;

// Handle global para a fila
static QueueHandle_t xSensorQueue = NULL;

/**
 * Task produtora que simula leituras de múltiplos sensores
 * 
 * Esta task representa aquisição de dados de sensores que ocorre
 * periodicamente. Dados são enviados para fila sem aguardar
 * processamento, permitindo aquisição contínua.
 */
void sensor_acquisition_task(void *pvParameters)
{
    int sensor_id = (int)pvParameters;
    ESP_LOGI(TAG, "Task de aquisição do sensor %d iniciada", sensor_id);
    
    sensor_reading_t reading;
    reading.sensor_id = sensor_id;
    strncpy(reading.unit, "°C", sizeof(reading.unit));
    
    TickType_t xLastWakeTime = xTaskGetTickCount();
    
    while (1) {
        // Simular leitura do sensor
        reading.value = 20.0f + (float)(esp_random() % 100) / 10.0f;
        reading.timestamp = esp_timer_get_time();
        
        ESP_LOGD(TAG, "Sensor %d: Leitura = %.2f%s", 
                 reading.sensor_id, reading.value, reading.unit);
        
        // Enviar para fila com timeout
        // Se fila estiver cheia, aguarda até 100ms
        if (xQueueSend(xSensorQueue, &reading, pdMS_TO_TICKS(100)) != pdTRUE) {
            ESP_LOGW(TAG, "Sensor %d: Fila cheia, leitura descartada!", 
                     sensor_id);
        }
        
        // Aguardar até próxima aquisição (frequência diferente por sensor)
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(500 + sensor_id * 200));
    }
}

/**
 * Task consumidora que processa dados dos sensores
 * 
 * Esta task demonstra processamento de dados recebidos via fila.
 * Ela pode operar em velocidade diferente dos produtores,
 * com a fila funcionando como buffer.
 */
void data_processing_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de processamento iniciada");
    
    sensor_reading_t reading;
    int readings_processed = 0;
    float sum = 0.0f;
    
    while (1) {
        // Bloquear aguardando dados na fila
        // portMAX_DELAY = aguardar indefinidamente
        if (xQueueReceive(xSensorQueue, &reading, portMAX_DELAY) == pdTRUE) {
            // Processar leitura recebida
            uint64_t latency = esp_timer_get_time() - reading.timestamp;
            
            ESP_LOGI(TAG, "Processando: Sensor=%d Valor=%.2f%s Latência=%lluμs",
                     reading.sensor_id, reading.value, reading.unit, latency);
            
            // Acumular estatísticas
            sum += reading.value;
            readings_processed++;
            
            // A cada 10 leituras, mostrar média
            if (readings_processed % 10 == 0) {
                float avg = sum / readings_processed;
                ESP_LOGI(TAG, "Estatísticas: %d leituras processadas, média=%.2f°C",
                         readings_processed, avg);
            }
            
            // Simular processamento que leva tempo
            vTaskDelay(pdMS_TO_TICKS(50));
        }
    }
}

/**
 * Task monitora que verifica estado da fila periodicamente
 * 
 * Esta task demonstra como obter informações sobre fila
 * sem remover dados, útil para debugging e monitoramento.
 */
void queue_monitor_task(void *pvParameters)
{
    ESP_LOGI(TAG, "Task de monitoramento iniciada");
    
    while (1) {
        // Obter número de itens na fila sem remover
        UBaseType_t items_waiting = uxQueueMessagesWaiting(xSensorQueue);
        
        // Obter espaços disponíveis
        UBaseType_t spaces_available = uxQueueSpacesAvailable(xSensorQueue);
        
        if (items_waiting > 0) {
            ESP_LOGI(TAG, "Monitor: %u itens na fila, %u espaços livres",
                     items_waiting, spaces_available);
            
            // Alerta se fila está ficando cheia
            if (spaces_available < 3) {
                ESP_LOGW(TAG, "Monitor: Fila quase cheia! Processamento lento?");
            }
        }
        
        vTaskDelay(pdMS_TO_TICKS(2000)); // Monitorar a cada 2 segundos
    }
}

/**
 * Exemplo de peek (espiar) sem remover da fila
 * Útil quando você quer ver próximo item sem consumi-lo
 */
void demonstrate_queue_peek(void)
{
    sensor_reading_t reading;
    
    // Espiar próximo item sem remover da fila
    if (xQueuePeek(xSensorQueue, &reading, 0) == pdTRUE) {
        ESP_LOGI(TAG, "Peek: Próximo item é do sensor %d com valor %.2f",
                 reading.sensor_id, reading.value);
        // Item ainda está na fila!
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando comunicação via filas");
    
    // Criar fila para 10 leituras de sensores
    xSensorQueue = xQueueCreate(10, sizeof(sensor_reading_t));
    
    if (xSensorQueue == NULL) {
        ESP_LOGE(TAG, "Falha ao criar fila");
        return;
    }
    
    ESP_LOGI(TAG, "Fila criada: capacidade para 10 leituras de %d bytes cada",
             sizeof(sensor_reading_t));
    
    // Criar múltiplas tasks produtoras (sensores)
    xTaskCreate(sensor_acquisition_task, "Sensor1", 3072, (void*)1, 5, NULL);
    xTaskCreate(sensor_acquisition_task, "Sensor2", 3072, (void*)2, 5, NULL);
    xTaskCreate(sensor_acquisition_task, "Sensor3", 3072, (void*)3, 5, NULL);
    
    // Criar task consumidora (processamento)
    xTaskCreate(data_processing_task, "Processor", 4096, NULL, 4, NULL);
    
    // Criar task de monitoramento
    xTaskCreate(queue_monitor_task, "Monitor", 2048, NULL, 2, NULL);
    
    ESP_LOGI(TAG, "Sistema de aquisição e processamento iniciado");
}

Timers de Software

Timers de software no FreeRTOS permitem execução de callbacks em intervalos regulares ou após delays específicos sem necessidade de criar tasks dedicadas. Isto economiza memória e simplifica código para operações periódicas simples que não justificam overhead de task completa.

Cada timer de software possui período que pode ser configurado como one-shot para execução única ou auto-reload para execução periódica contínua. Quando timer expira, função callback é executada no contexto da timer service task, thread especial do FreeRTOS dedicada ao gerenciamento de timers.

É importante compreender que callbacks de timers executam no contexto da timer service task e devem seguir regras estritas. Eles devem ser rápidos e nunca bloquear por períodos prolongados, pois isso impediria outros timers de executar pontualmente. Para processamento extenso, callback deve enviar notificação para task dedicada via fila ou semáforo.

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

static const char *TAG = "TIMER_EXAMPLE";

// Handles para timers
static TimerHandle_t xPeriodicTimer = NULL;
static TimerHandle_t xOneShotTimer = NULL;

// Contador para demonstração
static int periodic_counter = 0;

/**
 * Callback para timer periódico
 * 
 * IMPORTANTE: Esta função executa no contexto da timer service task
 * Deve ser RÁPIDA e NUNCA bloquear por tempo prolongado
 * 
 * @param xTimer Handle do timer que expirou
 */
void periodic_timer_callback(TimerHandle_t xTimer)
{
    // Obter ID do timer (útil se callback serve múltiplos timers)
    uint32_t timer_id = (uint32_t)pvTimerGetTimerID(xTimer);
    
    periodic_counter++;
    
    ESP_LOGI(TAG, "Timer periódico executado: contador=%d (ID=%lu)",
             periodic_counter, timer_id);
    
    // Exemplo: alternar LED
    static int led_state = 0;
    led_state = !led_state;
    gpio_set_level(GPIO_NUM_2, led_state);
    
    // A cada 10 execuções, disparar timer one-shot
    if (periodic_counter % 10 == 0) {
        ESP_LOGI(TAG, "Disparando timer one-shot");
        xTimerStart(xOneShotTimer, 0);
    }
}

/**
 * Callback para timer one-shot
 * 
 * Este timer executa apenas uma vez quando iniciado
 * Útil para delays ou timeouts
 */
void oneshot_timer_callback(TimerHandle_t xTimer)
{
    ESP_LOGI(TAG, "Timer one-shot executado após 3 segundos");
    ESP_LOGI(TAG, "Realizando operação especial...");
    
    // Simular operação que não deve bloquear muito
    // Para processamento pesado, envie notificação para task
}

/**
 * Demonstração de controle dinâmico de timers
 */
void demonstrate_timer_control(void)
{
    ESP_LOGI(TAG, "Demonstrando controle de timers");
    
    // Pausar timer periódico
    ESP_LOGI(TAG, "Pausando timer periódico por 5 segundos");
    if (xTimerStop(xPeriodicTimer, pdMS_TO_TICKS(100)) != pdPASS) {
        ESP_LOGW(TAG, "Falha ao pausar timer");
    }
    
    vTaskDelay(pdMS_TO_TICKS(5000));
    
    // Retomar timer
    ESP_LOGI(TAG, "Retomando timer periódico");
    if (xTimerStart(xPeriodicTimer, pdMS_TO_TICKS(100)) != pdPASS) {
        ESP_LOGW(TAG, "Falha ao iniciar timer");
    }
    
    // Modificar período do timer dinamicamente
    ESP_LOGI(TAG, "Alterando período para 500ms");
    if (xTimerChangePeriod(xPeriodicTimer, pdMS_TO_TICKS(500), 
                           pdMS_TO_TICKS(100)) != pdPASS) {
        ESP_LOGW(TAG, "Falha ao alterar período");
    }
    
    vTaskDelay(pdMS_TO_TICKS(5000));
    
    // Restaurar período original
    ESP_LOGI(TAG, "Restaurando período original de 1000ms");
    xTimerChangePeriod(xPeriodicTimer, pdMS_TO_TICKS(1000), pdMS_TO_TICKS(100));
}

void app_main(void)
{
    ESP_LOGI(TAG, "Demonstrando timers de software FreeRTOS");
    
    // Configurar GPIO para demonstração
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,
        .mode = GPIO_MODE_OUTPUT,
        .pin_bit_mask = (1ULL << GPIO_NUM_2),
    };
    gpio_config(&io_conf);
    
    // Criar timer periódico que executa a cada 1 segundo
    xPeriodicTimer = xTimerCreate(
        "PeriodicTimer",                // Nome para debug
        pdMS_TO_TICKS(1000),            // Período em ticks (1000ms)
        pdTRUE,                         // Auto-reload (periódico)
        (void *)1,                      // Timer ID
        periodic_timer_callback         // Função callback
    );
    
    if (xPeriodicTimer == NULL) {
        ESP_LOGE(TAG, "Falha ao criar timer periódico");
        return;
    }
    
    // Criar timer one-shot que executa uma vez após 3 segundos
    xOneShotTimer = xTimerCreate(
        "OneShotTimer",
        pdMS_TO_TICKS(3000),            // 3 segundos
        pdFALSE,                        // One-shot (não recarrega)
        (void *)2,
        oneshot_timer_callback
    );
    
    if (xOneShotTimer == NULL) {
        ESP_LOGE(TAG, "Falha ao criar timer one-shot");
        return;
    }
    
    // Iniciar timer periódico
    if (xTimerStart(xPeriodicTimer, 0) != pdPASS) {
        ESP_LOGE(TAG, "Falha ao iniciar timer periódico");
        return;
    }
    
    ESP_LOGI(TAG, "Timers criados e iniciados");
    
    // Aguardar um pouco antes de demonstrar controles
    vTaskDelay(pdMS_TO_TICKS(10000));
    
    // Demonstrar controle dinâmico
    demonstrate_timer_control();
}