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.