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_TICKSfor 1, a variável terá 16 bits, restando 8 bits para o usuário. - Se
configUSE_16_BIT_TICKSfor 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:
- 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.
- 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.