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:
- We have connected to WiFi.
- And we have connected to the MQTT server.
- 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:
| Feature | Binary Semaphore | Task Notification | Event Group |
|---|---|---|---|
| Condition | 1 Event | 1 Event (or simple value) | Multiple Events (AND/OR) |
| Recipient | Anyone who Takes | Specific Task | Multiple tasks simultaneously |
| Memory | Independent Object | State within TCB | Independent Object |
| Typical Use | ISR to Task | ISR 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 0100Event 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();
}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();
}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
);-
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() {}Result Analysis
MAINstarts, reachesxEventGroupWaitBits, and blocks because the bits are 0.- At second 1,
LicenseTasksets bit 1.MAINremains asleep because we requested AND (WaitAll=True). - At second 3,
WiFiTasksets bit 0. - At that moment, both mask bits are active.
MAINwakes 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.
- Task A finishes, sets its bit, and waits for (A, B, C).
- Task B finishes, sets its bit, and waits for (A, B, C).
- Task C finishes, sets its bit… And BOOM! All three unlock simultaneously.
This is very useful for parallel processing of divided data.