Gerenciamento de Sincronização e Comunicação em FreeRTOS com Grupos de Eventos

Introdução aos Grupos de Eventos

Em sistemas embarcados e ambientes de tempo real (RTOS), a coordenação entre múltiplas tarefas é um requisito fundamental. O FreeRTOS disponibiliza os Grupos de Eventos (Event Groups) como um mecanismo robusto para lidar com cenários que exigem a espera por múltiplas condições simultâneas (lógica E) ou alternativas (lógica OU).

Conceito e Arquitetura

Um grupo de eventos pode ser compreendido como uma variável inteira onde cada bit representa um estado ou evento distinto. O significado de cada bit é definido inteiramente pelo desenvolvedor (por exemplo, o bit 0 pode indicar a leitura de um sensor, enquanto o bit 1 indica a conexão de rede estabelecida). Um valor igual a 1 significa que o evento ocorreu, e 0 indica que não ocorreu.

O kernel do FreeRTOS reserva os bits mais altos dessa variável para uso interno. O tamanho total da variável depende da macro de configuração configUSE_16_BIT_TICKS:

  • Se configUSE_16_BIT_TICKS for 1, a variável terá 16 bits, restando 8 bits para o usuário.
  • Se configUSE_16_BIT_TICKS for 0, a variável terá 32 bits, restando 24 bits para o usuário.

Essa configuração visa otimizar a eficiência do processador alvo, alinhando o tamanho do grupo de eventos com a arquitetura nativa do microcontrolador.

Diferenças em Relação a Filas e Semáforos

Os grupos de eventos possuem duas características distintas quando comparados a outros mecanismos de comunicação do FreeRTOS:

  1. Mecanismo de Broadcast: Enquanto semáforos e filas geralmente despertam apenas uma tarefa bloqueada, um grupo de eventos pode despertar simultaneamente todas as tarefas que estejam aguardando a condição satisfeita.
  2. Persistência do Estado: Filas e semáforos são recursos consumíveis (os dados são lidos e removidos, ou o semáforo é decrementado). Em um grupo de eventos, a tarefa que lê o estado pode escolher se os bits devem ser limpos automaticamente ou mantidos para que outras tarefas também os leiam.

API de Grupos de Eventos

Criação e Remoção

Antes de utilizar um grupo de eventos, é necessário criá-lo para obter seu handle. O FreeRTOS oferece alocação dinâmica e estática.

/* Criação dinâmica */
EventGroupHandle_t xSystemEventGroup = xEventGroupCreate();

/* Criação estática */
StaticEventGroup_t xStaticGroupBuffer;
EventGroupHandle_t xStaticEventGroup = xEventGroupCreateStatic(&xStaticGroupBuffer);

/* Remoção (apenas para grupos criados dinamicamente) */
if (xSystemEventGroup != NULL) {
    vEventGroupDelete(xSystemEventGroup);
}

Definição de Eventos (Set Bits)

Para sinalizar que um ou mais eventos ocorreram, utilizam-se as funções de definição de bits. Se tarefas estiverem bloqueadas aguardando esses bits, elas serão avaliadas para desbloqueio.

/* Uso em tarefas */
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );

/* Uso em rotinas de interrupção (ISR) */
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, 
                                      const EventBits_t uxBitsToSet, 
                                      BaseType_t *pxHigherPriorityTaskWoken );

Nota sobre ISR: Diferente de semáforos, a função de ISR não altera o grupo de eventos diretamente. Como múltiplas tarefas podem ser despertadas, isso geraria incertezas no contexto de interrupção. A função ISR envia os dados para a daemon task (tarefa de fundo do FreeRTOS), que executa a alteração de forma segura. O parâmetro pxHigherPriorityTaskWoken deve ser usado para solicitar um context switch se a daemon task tiver prioridade maior que a tarefa interrompida.

Aguardando Eventos (Wait Bits)

A função xEventGroupWaitBits coloca a tarefa em estado de bloqueio até que a "condição de desbloqueio" (unblock condition) seja atendida.

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait );
Parâmetro Descrição
uxBitsToWaitFor Máscara de bits que representa os eventos aguardados.
xWaitForAllBits pdTRUE: Aguarda todos os bits da máscara (E). pdFALSE: Aguarda qualquer bit da máscara (OU).
xClearOnExit pdTRUE: Limpa os bits aguardados antes de retornar. pdFALSE: Mantém o estado dos bits.
xTicksToWait Tempo máximo de bloqueio. Use portMAX_DELAY para espera infinita.

O uso de xClearOnExit como pdTRUE garante que a leitura e a limpeza dos bits ocorram como uma operação atômica, evitando condições de corrida.

Ponto de Sincronização (Sync)

A função xEventGroupSync é projetada para atuar como uma barreira de sincronização. Ela permite que uma tarefa sinalize sua própria conclusão e, simultaneamente, aguarde a conclusão de outras tarefas.

EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
                             const EventBits_t uxBitsToSet,
                             const EventBits_t uxBitsToWaitFor,
                             TickType_t xTicksToWait );

Ao retornar com sucesso, a função limpa automaticamente os bits especificados em uxBitsToWaitFor.

Exemplo Prático 1: Aguardando Múltiplas Condições

Considere um dispositivo IoT que necessita inicializar seus periféricos antes de iniciar a aplicação principal. A tarefa principle deve aguardar que o sensor e o módulo de rede estejam prontos.

#define BIT_SENSOR_READY   (1 << 0)
#define BIT_NETWORK_READY  (1 << 1)
#define ALL_SYSTEMS_GO     (BIT_SENSOR_READY | BIT_NETWORK_READY)

static EventGroupHandle_t xAppEvents;

void vSensorInitTask(void *pvParameters) {
    for (;;) {
        // Simula inicialização do sensor
        vTaskDelay(pdMS_TO_TICKS(100));
        xEventGroupSetBits(xAppEvents, BIT_SENSOR_READY);
        vTaskDelay(portMAX_DELAY); // Tarefa termina após inicializar
    }
}

void vNetworkInitTask(void *pvParameters) {
    for (;;) {
        // Simula conexão de rede
        vTaskDelay(pdMS_TO_TICKS(200));
        xEventGroupSetBits(xAppEvents, BIT_NETWORK_READY);
        vTaskDelay(portMAX_DELAY);
    }
}

void vMainApplicationTask(void *pvParameters) {
    for (;;) {
        // Aguarda ambos os eventos. Não limpa os bits para não afetar outras possíveis leitores
        xEventGroupWaitBits(xAppEvents, ALL_SYSTEMS_GO, pdFALSE, pdTRUE, portMAX_DELAY);
        
        // Ambos os eventos ocorreram, inicia o processamento
        printf("Sistema IoT pronto. Iniciando telemetria...\n");
        vTaskDelay(portMAX_DELAY);
    }
}

int main(void) {
    prvSetupHardware();
    xAppEvents = xEventGroupCreate();
    
    xTaskCreate(vSensorInitTask, "SensorInit", 1000, NULL, 2, NULL);
    xTaskCreate(vNetworkInitTask, "NetInit", 1000, NULL, 2, NULL);
    xTaskCreate(vMainApplicationTask, "MainApp", 1000, NULL, 1, NULL);
    
    vTaskStartScheduler();
    return 0;
}

Exemplo Prático 2: Sincronização de Tarefas (Barreira)

Em um pipeline de processamento de dados em lotes (batch processing), três tarefas distintas atuam em cadalote: Coleta, Filtragem e Armazenamento. O próximo lote só pode ser iniciado quando todas as três tarefas terminarem o processamento do lote atual.

#define BIT_ACQ_DONE   (1 << 0)
#define BIT_FILT_DONE  (1 << 1)
#define BIT_STORE_DONE (1 << 2)
#define ALL_BATCH_DONE (BIT_ACQ_DONE | BIT_FILT_DONE | BIT_STORE_DONE)

static EventGroupHandle_t xBatchSync;
static uint32_t ulBatchCounter = 0;

void vDataAcquisitionTask(void *pvParameters) {
    for (;;) {
        printf("Coletando dados do lote %lu...\n", ulBatchCounter);
        vTaskDelay(pdMS_TO_TICKS(50)); // Simula coleta
        
        // Sinaliza conclusão e aguarda as outras tarefas
        xEventGroupSync(xBatchSync, BIT_ACQ_DONE, ALL_BATCH_DONE, portMAX_DELAY);
        
        printf("Lote %lu finalizado. Iniciando próximo ciclo.\n", ulBatchCounter++);
    }
}

void vDataFilteringTask(void *pvParameters) {
    for (;;) {
        printf("Filtrando dados do lote %lu...\n", ulBatchCounter);
        vTaskDelay(pdMS_TO_TICKS(80)); // Simula filtragem
        
        xEventGroupSync(xBatchSync, BIT_FILT_DONE, ALL_BATCH_DONE, portMAX_DELAY);
    }
}

void vDataStorageTask(void *pvParameters) {
    for (;;) {
        printf("Armazenando dados do lote %lu...\n", ulBatchCounter);
        vTaskDelay(pdMS_TO_TICKS(120)); // Simula armazenamento
        
        xEventGroupSync(xBatchSync, BIT_STORE_DONE, ALL_BATCH_DONE, portMAX_DELAY);
    }
}

int main(void) {
    prvSetupHardware();
    xBatchSync = xEventGroupCreate();
    
    xTaskCreate(vDataAcquisitionTask, "AcqTask", 1000, NULL, 2, NULL);
    xTaskCreate(vDataFilteringTask, "FiltTask", 1000, NULL, 2, NULL);
    xTaskCreate(vDataStorageTask, "StoreTask", 1000, NULL, 2, NULL);
    
    vTaskStartScheduler();
    return 0;
}

Neste cenário, a função xEventGroupSync atua de forma integrada: cada tarefa define seu respectivo bit de conclusão e bloqueia até que a máscara ALL_BATCH_DONE seja satisfeita. Assim que a última tarefa define seu bit, todas são desbloqueadas simultaneamente, os bits são limpos automaticamente pelo kernel, e o processamento do próximo lote é iniciado de forma sincronizada.

Tags: FreeRTOS EventGroups rtos EmbeddedSystems TaskSynchronization

Publicado em 8-7 19:05