freertos-event-groups-flags

Event Groups en FreeRTOS: esperar varios eventos

  • 6 min

Un Event Group es un conjunto de bits que permite esperar una o varias condiciones de sincronización dentro de FreeRTOS.

Hasta ahora hemos aprendido a sincronizar tareas usando Semáforos. Esto funciona genial si la condición es simple: “Espera a que se pulse el botón” o “Espera a que el buffer esté libre”.

La cosa se complica cuando programamos un dispositivo IoT con ESP32 y queremos que la tarea principal empiece a trabajar SOLO SI:

  1. Nos hemos conectado al WiFi.
  2. Y nos hemos conectado al servidor MQTT.
  3. Y la hora se ha sincronizado por NTP.

Si intentamos hacer esto con semáforos binarios, tendríamos que anidar tres xSemaphoreTake, lo cual es feo, ineficiente y propenso a errores (orden de bloqueo, timeouts parciales…).

Para estas situaciones, FreeRTOS nos ofrece los Event Groups (grupos de eventos), una herramienta pensada para combinar condiciones mediante bits.

¿Qué es un Event Group?

Imagina un Event Group como un conjunto de bits, donde cada uno representa una condición independiente.

Cada bit de ese número actúa como una bandera (Flag) independiente.

  • El Bit 0 puede significar “WiFi Conectado”.
  • El Bit 1 puede significar “Mensaje Recibido”.
  • El Bit 2 puede significar “Motor Parado”.

Lo potente no es almacenar bits (eso lo hace una variable int normal), sino que FreeRTOS nos permite dormir una tarea hasta que una combinación específica de bits se cumpla.

Los 8 bits superiores están reservados para el kernel. Con EventBits_t de 32 bits, como en ESP32, quedan 24 bits disponibles. Los ports configurados con ticks de 16 bits solo ofrecen 8 bits de aplicación.

Comparación con semáforos y task notifications

Resumen para elegir sabiamente:

CaracterísticaSemáforo BinarioTask NotificationEvent Group
Condición1 Evento1 Evento (o valor simple)Múltiples Eventos (AND/OR)
DestinatarioCualquiera que haga TakeTarea específicaMúltiples tareas a la vez
MemoriaObjeto independienteEstado dentro del TCBObjeto independiente
Uso TípicoISR a TareaISR a Tarea (Rápido)Máquinas de Estados, Startup

Definición de los flags

Lo primero es definir qué significa cada bit. Usaremos máscaras de bits (1 << n) o constantes hexadecimales.

// Definimos los bits que vamos a usar
#define BIT_WIFI_CONECTADO    (1 << 0) // 0000 0001
#define BIT_MQTT_CONECTADO    (1 << 1) // 0000 0010
#define BIT_BOTON_PULSADO     (1 << 2) // 0000 0100
Copied!

API de Event Groups

Necesitamos incluir la librería específica (aunque en ESP32 viene incluida por defecto con FreeRTOS).

Creación

#include "freertos/event_groups.h"

EventGroupHandle_t xEventos;

void setup() {
    xEventos = xEventGroupCreate();
}
Copied!

Establecer bits (SetBits)

Cuando ocurre un evento (ej. el WiFi se conecta), una tarea o interrupción “levanta” la bandera correspondiente.

// Desde una Tarea
xEventGroupSetBits(xEventos, BIT_WIFI_CONECTADO);

// Desde una Interrupción (ISR)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(xEventos, BIT_BOTON_PULSADO, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR();
}
Copied!

Esta operación hace un OR a nivel de bits. Si ya había otros bits activos, se mantienen. La variante FromISR difiere el cambio mediante la cola del Timer Service, así que debemos comprobar que devuelve pdPASS si no podemos permitirnos perder la orden.

Esperar bits (WaitBits)

Esta es la función nuclear. Permite a una tarea bloquearse esperando condiciones.

EventBits_t xEventGroupWaitBits(
    EventGroupHandle_t xEventGroup, // El grupo
    const EventBits_t uxBitsToWaitFor, // Qué bits nos interesan
    const BaseType_t xClearOnExit,  // ¿Borramos los bits al salir?
    const BaseType_t xWaitForAllBits, // ¿Esperamos a TODOS (AND) o a UNO (OR)?
    TickType_t xTicksToWait         // Timeout
);
Copied!
  • uxBitsToWaitFor: Una máscara con los bits que vigilamos. Ej: BIT_WIFI | BIT_MQTT.

  • xClearOnExit:

  • pdTRUE: Al despertar, los bits que causaron el desbloqueo se limpian (vuelven a 0). Comportamiento de “evento consumido”.

  • pdFALSE: Los bits se quedan en 1. Útil si queremos que múltiples tareas lean el mismo estado.

  • xWaitForAllBits:

  • pdTRUE (Lógica AND): Espera hasta que todos los bits indicados estén a 1 a la vez.

  • pdFALSE (Lógica OR): Despierta en cuanto cualquiera de los bits se ponga a 1.

Ejemplo Práctico: Secuencia de Arranque

Vamos a simular un sistema que necesita conectarse a WiFi y validar una licencia antes de permitir que la tarea principal funcione. Simularemos las conexiones con tareas temporales.

#include "freertos/event_groups.h"

// 1. Definimos los Flags
#define FLG_WIFI    (1 << 0)
#define FLG_LICENCIA (1 << 1)

// Objeto del grupo
EventGroupHandle_t grupoArranque;

// Tarea que simula conexión WiFi
void TareaWiFi(void *pvParameters) {
    Serial.println("WiFi: Intentando conectar...");
    vTaskDelay(pdMS_TO_TICKS(3000)); // Tarda 3 segundos
    Serial.println("WiFi: ¡Conectado!");
    
    // Activamos el bit 0
    xEventGroupSetBits(grupoArranque, FLG_WIFI);
    
    // La tarea muere (o se queda manteniendo la conexión)
    vTaskDelete(NULL);
}

// Tarea que simula validar licencia
void TareaLicencia(void *pvParameters) {
    Serial.println("Licencia: Validando...");
    vTaskDelay(pdMS_TO_TICKS(1000)); // Tarda 1 segundo
    Serial.println("Licencia: OK.");
    
    // Activamos el bit 1
    xEventGroupSetBits(grupoArranque, FLG_LICENCIA);
    
    vTaskDelete(NULL);
}

// Tarea Principal (Main App)
void TareaPrincipal(void *pvParameters) {
    Serial.println("MAIN: Esperando sistemas...");

    // 2. Esperamos a que (WIFI) Y (LICENCIA) estén listos.
    // WaitForAllBits = pdTRUE (AND)
    // ClearOnExit = pdFALSE (No queremos borrarlos, el estado persiste)
    EventBits_t bits = xEventGroupWaitBits(
        grupoArranque, 
        FLG_WIFI | FLG_LICENCIA, 
        pdFALSE, 
        pdTRUE, 
        portMAX_DELAY
    );

    // Si pasamos de aquí, es que todo está OK
    Serial.println("MAIN: Sistemas listos. Iniciando aplicación.");
    Serial.printf("Valor de los bits actuales: 0x%X\n", bits);

    for(;;) {
        // Bucle principal de la aplicación
        Serial.println("App funcionando...");
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void setup() {
    Serial.begin(115200);
    
    // Crear grupo
    grupoArranque = xEventGroupCreate();

    if (grupoArranque == NULL) {
        Serial.println("No se pudo crear el Event Group");
        return;
    }

    // Lanzar tareas
    xTaskCreate(TareaWiFi, "WiFi", 2048, NULL, 1, NULL);
    xTaskCreate(TareaLicencia, "Lic", 2048, NULL, 1, NULL);
    xTaskCreate(TareaPrincipal, "Main", 2048, NULL, 1, NULL);
}

void loop() {}
Copied!

Análisis del resultado

  1. Arranca MAIN, llega al xEventGroupWaitBits y se bloquea porque los bits están a 0.
  2. Al segundo 1, TareaLicencia pone el bit 1. MAIN sigue dormida porque pedimos AND (WaitAll=True).
  3. Al segundo 3, TareaWiFi pone el bit 0.
  4. En ese instante, los dos bits de la máscara están activos. MAIN despierta y empieza a imprimir “App funcionando…”.

Sincronización de Tareas (Rendezvous)

Existe una función avanzada llamada xEventGroupSync(). Imagina que tienes 3 tareas que hacen partes distintas de un cálculo y quieres que la siguiente fase empiece solo cuando las tres hayan terminado la fase actual.

Es un punto de encuentro (Rendezvous).

  1. Tarea A termina, pone su bit y espera a (A, B, C).
  2. Tarea B termina, pone su bit y espera a (A, B, C).
  3. Tarea C termina, pone su bit… ¡Y BOOM! Se desbloquean las tres a la vez.

Es muy útil para procesamiento paralelo de datos divididos.