Task Synchronization with FreeRTOS Event Groups on ESP32 (ESP-WROVER-KIT Example)

Real-time operating systems often require flexible and efficient mechanisms to synchronize multiple tasks or signal task execution based on multiple conditions. Event groups in FreeRTOS provide a powerful way to manage such task synchronization. Unlike semaphores or mutexes that handle individual signals or locks, event groups can handle multiple binary flags in a single object—allowing tasks to wait for one or more events simultaneously.

In this post, we’ll dive into FreeRTOS event groups and demonstrate how to use them on the ESP-WROVER-KIT, a development board based on the ESP32 microcontroller. You’ll learn what event groups are, how they differ from other synchronization primitives, and how to apply them using a real-world example.


What Are Event Groups?

An event group in FreeRTOS is a collection of binary flags (bits) that tasks can set, clear, and wait on. Each event group contains up to 24 user-definable bits (bits 0–23). A task can:

  • Wait for one or more bits to be set.
  • Specify whether to wait for any or all of the bits.
  • Block until the required condition is met or a timeout occurs.
  • Automatically clear bits once the wait condition is satisfied.

Event groups are ideal for:

  • Task-to-task synchronization based on multiple signals.
  • Waiting on multiple conditions (e.g., Wi-Fi connected + sensor ready).
  • Complex ISR-to-task signaling (via xEventGroupSetBitsFromISR).

Event Groups API Overview

FreeRTOS provides the following API functions for event groups:

Create an event group:

EventGroupHandle_t xEventGroupCreate(void);

Set bits in the group:

EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet);

Clear bits:

EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear);

Wait for bits:

EventBits_t xEventGroupWaitBits(
    EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToWaitFor,
    const BaseType_t xClearOnExit,
    const BaseType_t xWaitForAllBits,
    TickType_t xTicksToWait
);

Set bits from ISR:

BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken);

Example Scenario: Task Synchronization Using Multiple Events

Let’s say we have a system with the following components:

  • Sensor Task: Simulates a sensor becoming ready.
  • Network Task: Simulates a network connection becoming active.
  • Processing Task: Waits until both the sensor is ready and the network is connected before proceeding.

We’ll use event groups to signal these conditions and synchronize the processing task.


Define Event Bits

#define SENSOR_READY_BIT  (1 << 0)
#define NETWORK_READY_BIT (1 << 1)

These bits will be set by the sensor and network tasks respectively.


Declare Global Event Group Handle

EventGroupHandle_t system_event_group;

Sensor Task

void sensor_task(void *pvParameters)
{
    while (true)
    {
        ESP_LOGI("SensorTask", "Initializing sensor...");
        vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate delay
        ESP_LOGI("SensorTask", "Sensor ready");

        xEventGroupSetBits(system_event_group, SENSOR_READY_BIT);
        vTaskDelete(NULL); // One-time initialization
    }
}

Network Task

void network_task(void *pvParameters)
{
    while (true)
    {
        ESP_LOGI("NetworkTask", "Connecting to network...");
        vTaskDelay(pdMS_TO_TICKS(3000)); // Simulate delay
        ESP_LOGI("NetworkTask", "Network connected");

        xEventGroupSetBits(system_event_group, NETWORK_READY_BIT);
        vTaskDelete(NULL); // One-time initialization
    }
}

Processing Task

void processing_task(void *pvParameters)
{
    ESP_LOGI("ProcessingTask", "Waiting for system to be ready...");

    EventBits_t bits = xEventGroupWaitBits(
        system_event_group,
        SENSOR_READY_BIT | NETWORK_READY_BIT,
        pdTRUE,    // Clear bits on exit
        pdTRUE,    // Wait for all bits
        portMAX_DELAY
    );

    if ((bits & (SENSOR_READY_BIT | NETWORK_READY_BIT)) == (SENSOR_READY_BIT | NETWORK_READY_BIT))
    {
        ESP_LOGI("ProcessingTask", "System ready. Starting data processing...");
    }

    while (true)
    {
        ESP_LOGI("ProcessingTask", "Processing data...");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

app_main Function

void app_main(void)
{
    system_event_group = xEventGroupCreate();

    if (system_event_group == NULL)
    {
        ESP_LOGE("Main", "Failed to create event group");
        return;
    }

    xTaskCreate(sensor_task, "SensorTask", 2048, NULL, 2, NULL);
    xTaskCreate(network_task, "NetworkTask", 2048, NULL, 2, NULL);
    xTaskCreate(processing_task, "ProcessingTask", 2048, NULL, 2, NULL);
}

Expected Output

Your serial monitor will show:

SensorTask: Initializing sensor...
NetworkTask: Connecting to network...
SensorTask: Sensor ready
NetworkTask: Network connected
ProcessingTask: System ready. Starting data processing...
ProcessingTask: Processing data...
ProcessingTask: Processing data...
...

This demonstrates how the processing task waits for multiple conditions to be met using event groups.


Tips for Using Event Groups

  • Use bitwise flags for clarity and control.
  • Event bits 24–31 are reserved for internal use by FreeRTOS.
  • Use xEventGroupSetBitsFromISR() when setting bits inside an ISR.
  • Always test edge cases, such as missed signals or simultaneous bit setting.
  • Be mindful of race conditions if multiple tasks are modifying bits simultaneously.

Summary

FreeRTOS event groups are a flexible synchronization tool for embedded systems. They allow multiple tasks to set, clear, and wait for bit-level events efficiently. Unlike semaphores and mutexes, event groups are ideal when multiple conditions must be met before proceeding.

In this post, we used the ESP-WROVER-KIT and ESP-IDF to build a simple system where two tasks signal when their components are ready, and a third task waits for both to proceed. This type of synchronization is essential in many real-world embedded applications, including sensor fusion, communication stacks, and power management systems.

Whether you're synchronizing complex subsystems or managing multiple states, event groups offer a clean and scalable solution for FreeRTOS-based projects on ESP32.