freertos-event-groups-flags

Event Groups in FreeRTOS: Waiting for Multiple Events

  • 6 min

An Event Group is a set of bits that allows waiting for one or more synchronization conditions within FreeRTOS.

So far, we have learned to synchronize tasks using Semaphores. This works great if the condition is simple: “Wait for the button to be pressed” or “Wait for the buffer to be free”.

Things get complicated when programming an IoT device with an ESP32 and we want the main task to start working ONLY IF:

  1. We have connected to WiFi.
  2. And we have connected to the MQTT server.
  3. And the time has been synchronized via NTP.

If we try to do this with binary semaphores, we would have to nest three xSemaphoreTake calls, which is ugly, inefficient, and prone to errors (locking order, partial timeouts…).

For these situations, FreeRTOS offers us Event Groups, a tool designed to combine conditions using bits.

What is an Event Group?

Imagine an Event Group as a set of bits, where each one represents an independent condition.

Each bit of that number acts as an independent flag.

  • Bit 0 could mean “WiFi Connected”.
  • Bit 1 could mean “Message Received”.
  • Bit 2 could mean “Motor Stopped”.

The powerful aspect is not storing bits (a normal int variable does that), but that FreeRTOS allows us to put a task to sleep until a specific combination of bits is met.

The top 8 bits are reserved for the kernel. With a 32-bit EventBits_t, as on the ESP32, 24 bits are available. Ports configured with 16-bit ticks only offer 8 application bits.

Comparison with Semaphores and Task Notifications

Summary for making a wise choice:

FeatureBinary SemaphoreTask NotificationEvent Group
Condition1 Event1 Event (or simple value)Multiple Events (AND/OR)
RecipientAnyone who TakesSpecific TaskMultiple tasks simultaneously
MemoryIndependent ObjectState within TCBIndependent Object
Typical UseISR to TaskISR to Task (Fast)State Machines, Startup

Defining the Flags

The first step is to define what each bit means. We will use bit masks (1 << n) or hexadecimal constants.

// Define the bits we are going to use
#define BIT_WIFI_CONNECTED    (1 << 0) // 0000 0001
#define BIT_MQTT_CONNECTED    (1 << 1) // 0000 0010
#define BIT_BUTTON_PRESSED    (1 << 2) // 0000 0100
Copied!

Event Groups API

We need to include the specific library (although on ESP32 it is included by default with FreeRTOS).

Creation

#include "freertos/event_groups.h"

EventGroupHandle_t xEvents;

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

Setting Bits (SetBits)

When an event occurs (e.g., WiFi connects), a task or interrupt “raises” the corresponding flag.

// From a Task
xEventGroupSetBits(xEvents, BIT_WIFI_CONNECTED);

// From an Interrupt (ISR)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(xEvents, BIT_BUTTON_PRESSED, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR();
}
Copied!

This operation performs a bitwise OR. If other bits were already active, they are preserved. The FromISR variant defers the change via the Timer Service queue, so we must check that it returns pdPASS if we cannot afford to lose the command.

Waiting for Bits (WaitBits)

This is the core function. It allows a task to block while waiting for conditions.

EventBits_t xEventGroupWaitBits(
    EventGroupHandle_t xEventGroup, // The group
    const EventBits_t uxBitsToWaitFor, // Which bits we care about
    const BaseType_t xClearOnExit,  // Clear the bits upon exit?
    const BaseType_t xWaitForAllBits, // Wait for ALL (AND) or ANY (OR)?
    TickType_t xTicksToWait         // Timeout
);
Copied!
  • uxBitsToWaitFor: A mask with the bits we are monitoring. Example: BIT_WIFI | BIT_MQTT.

  • xClearOnExit:

    • pdTRUE: Upon waking, the bits that caused the unblock are cleared (return to 0). “Consumed event” behavior.
    • pdFALSE: The bits remain at 1. Useful if we want multiple tasks to read the same state.
  • xWaitForAllBits:

    • pdTRUE (AND Logic): Waits until all the specified bits are set to 1 simultaneously.
    • pdFALSE (OR Logic): Wakes up as soon as any of the bits is set to 1.

Practical Example: Startup Sequence

Let’s simulate a system that needs to connect to WiFi and validate a license before allowing the main task to run. We will simulate the connections with temporary tasks.

#include "freertos/event_groups.h"

// 1. Define the Flags
#define FLG_WIFI    (1 << 0)
#define FLG_LICENSE (1 << 1)

// Group object
EventGroupHandle_t startupGroup;

// Task that simulates WiFi connection
void WiFiTask(void *pvParameters) {
    Serial.println("WiFi: Attempting to connect...");
    vTaskDelay(pdMS_TO_TICKS(3000)); // Takes 3 seconds
    Serial.println("WiFi: Connected!");
    
    // Activate bit 0
    xEventGroupSetBits(startupGroup, FLG_WIFI);
    
    // The task dies (or stays to maintain the connection)
    vTaskDelete(NULL);
}

// Task that simulates license validation
void LicenseTask(void *pvParameters) {
    Serial.println("License: Validating...");
    vTaskDelay(pdMS_TO_TICKS(1000)); // Takes 1 second
    Serial.println("License: OK.");
    
    // Activate bit 1
    xEventGroupSetBits(startupGroup, FLG_LICENSE);
    
    vTaskDelete(NULL);
}

// Main Task (Main App)
void MainTask(void *pvParameters) {
    Serial.println("MAIN: Waiting for systems...");

    // 2. Wait for (WiFi) AND (License) to be ready.
    // WaitForAllBits = pdTRUE (AND)
    // ClearOnExit = pdFALSE (We don't want to clear them, state persists)
    EventBits_t bits = xEventGroupWaitBits(
        startupGroup, 
        FLG_WIFI | FLG_LICENSE, 
        pdFALSE, 
        pdTRUE, 
        portMAX_DELAY
    );

    // If we get past here, everything is OK
    Serial.println("MAIN: Systems ready. Starting application.");
    Serial.printf("Current bits value: 0x%X\n", bits);

    for(;;) {
        // Main application loop
        Serial.println("App running...");
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void setup() {
    Serial.begin(115200);
    
    // Create group
    startupGroup = xEventGroupCreate();

    if (startupGroup == NULL) {
        Serial.println("Could not create Event Group");
        return;
    }

    // Launch tasks
    xTaskCreate(WiFiTask, "WiFi", 2048, NULL, 1, NULL);
    xTaskCreate(LicenseTask, "Lic", 2048, NULL, 1, NULL);
    xTaskCreate(MainTask, "Main", 2048, NULL, 1, NULL);
}

void loop() {}
Copied!

Result Analysis

  1. MAIN starts, reaches xEventGroupWaitBits, and blocks because the bits are 0.
  2. At second 1, LicenseTask sets bit 1. MAIN remains asleep because we requested AND (WaitAll=True).
  3. At second 3, WiFiTask sets bit 0.
  4. At that moment, both mask bits are active. MAIN wakes up and starts printing “App running…”.

Task Synchronization (Rendezvous)

There is an advanced function called xEventGroupSync(). Imagine you have 3 tasks that perform different parts of a calculation and you want the next phase to start only when all three have finished the current phase.

It is a Rendezvous point.

  1. Task A finishes, sets its bit, and waits for (A, B, C).
  2. Task B finishes, sets its bit, and waits for (A, B, C).
  3. Task C finishes, sets its bit… And BOOM! All three unlock simultaneously.

This is very useful for parallel processing of divided data.