Chapter 14: Event Groups and Task Notifications
Chapter Objectives
Upon completing this chapter, you will be able to:
- Explain the limitations of simple semaphores for synchronizing on multiple events.
- Define FreeRTOS Event Groups and their purpose.
- Create and manage Event Groups using the relevant API functions.
- Set, clear, and wait for specific combinations of bits (event flags) within an Event Group, both from tasks and ISRs.
- Implement synchronization patterns where a task needs to wait for multiple conditions to be met.
- Describe FreeRTOS Task Notifications as a lightweight, direct task communication mechanism.
- Explain the different ways Task Notifications can be used (emulating binary/counting semaphores, queues, event groups).
- Send and receive Task Notifications using the appropriate API functions (
xTaskNotify
,xTaskNotifyGive
,xTaskNotifyWait
,ulTaskNotifyTake
). - Appreciate the performance benefits of Task Notifications for simple signaling compared to other RTOS objects.
- Choose the appropriate synchronization mechanism (Queue, Semaphore, Mutex, Event Group, Task Notification) based on the specific requirements of the application.
Introduction
In the preceding chapters, we learned how to use Queues for data transfer and Semaphores/Mutexes for signaling and resource protection. While powerful, these mechanisms sometimes require complex implementations for more intricate synchronization scenarios. For instance, how would a task efficiently wait for multiple distinct events to occur before proceeding? Or how can one task signal another directly without the overhead of creating and managing a dedicated Queue or Semaphore?
FreeRTOS offers two advanced mechanisms designed to handle such situations more effectively: Event Groups and Task Notifications.
Event Groups allow tasks to synchronize based on the state of multiple binary events or flags simultaneously. A task can block until a specific combination of events (bits) has occurred, making them ideal for scenarios like waiting for multiple system initializations (e.g., Wi-Fi connected, Network Time Protocol (NTP) synchronized, sensor calibrated) before starting main application logic.
Task Notifications provide a highly efficient, direct-to-task signaling mechanism. Each task has a built-in 32-bit notification value that can be used by other tasks or ISRs to send information or signals directly, often replacing the need for a separate semaphore or single-item queue with lower RAM usage and faster execution speed.
This chapter explores the theory behind Event Groups and Task Notifications and demonstrates their practical application within the ESP-IDF environment, adding powerful tools to your concurrent programming toolkit.
Theory
Event Groups
Imagine you have a task that should only run after both the Wi-Fi connection is established and the device’s time has been synchronized via NTP. Using simple binary semaphores would require the task to potentially take one semaphore, then try to take the second, possibly releasing the first if the second isn’t available yet – it becomes cumbersome. Event Groups simplify this.
An Event Group is a FreeRTOS object that manages a set of event flags. These flags are essentially individual bits within a variable maintained by the RTOS. Tasks can set, clear, and wait for specific bits or combinations of bits within the group.
- Concept: A collection of binary flags (bits) representing individual events.
- Size: The number of available bits depends on the
configUSE_16_BIT_TICKS
setting inFreeRTOSConfig.h
. If 0 (default for ESP32), 24 bits are available. If 1, only 8 bits are available. - Synchronization: Tasks can block and wait for one or more bits to be set. The waiting logic can be configured:
- Wait for ALL specified bits to be set (logical AND).
- Wait for ANY of the specified bits to be set (logical OR).
- Optionally clear the bits automatically upon successful return from the wait.
- ISR Safe: Bits can be set from Interrupt Service Routines.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans', 'primaryColor': '#6B46C1', 'primaryTextColor': '#0F0F0F', 'primaryBorderColor': '#4C1D95', 'lineColor': '#6B46C1', 'secondaryColor': '#F0F4F8', 'tertiaryColor': '#EEEEF3' } } }%% sequenceDiagram participant TaskA as Task A participant TaskB as Task B participant ISR as Interrupt<br>Service Routine participant EventGroup as Event Group rect rgb(226, 232, 240) Note over EventGroup: Event Group Analogy:<br>A box containing several labeled flags (bits)<br>that tasks can wait for and set/clear. end rect rgb(221, 214, 254) Note over EventGroup: EVENT GROUP<br>┌─────────────────────────┐<br>│ ☐ WiFi Up ☐ NTP Synced │<br>│ ☐ Sensor Ready │<br>└─────────────────────────┘<br>All flags initially cleared (0) end TaskA->>+EventGroup: xEventGroupWaitBits(WiFi Up AND NTP Synced) rect rgb(254, 226, 226) Note over TaskA: Blocked waiting for<br>WiFi Up AND NTP Synced end TaskB->>+EventGroup: xEventGroupWaitBits(Sensor Ready) rect rgb(254, 226, 226) Note over TaskB: Blocked waiting for<br>Sensor Ready end Note over EventGroup: WiFi connects rect rgb(221, 214, 254) Note over EventGroup: EVENT GROUP<br>┌─────────────────────────┐<br>│ ☑ WiFi Up ☐ NTP Synced │<br>│ ☐ Sensor Ready │<br>└─────────────────────────┘<br>WiFi Up bit set (1) end Note over EventGroup: NTP server sync completes rect rgb(221, 214, 254) Note over EventGroup: EVENT GROUP<br>┌─────────────────────────┐<br>│ ☑ WiFi Up ☑ NTP Synced │<br>│ ☐ Sensor Ready │<br>└─────────────────────────┘<br>NTP Synced bit set (1) end EventGroup-->>-TaskA: All required bits set<br>(WiFi Up AND NTP Synced) rect rgb(209, 250, 229) Note over TaskA: Unblocked and running end Note over ISR: Sensor interrupt occurs ISR->>+EventGroup: xEventGroupSetBitsFromISR(Sensor Ready) rect rgb(221, 214, 254) Note over EventGroup: EVENT GROUP<br>┌─────────────────────────┐<br>│ ☑ WiFi Up ☑ NTP Synced │<br>│ ☑ Sensor Ready │<br>└─────────────────────────┘<br>Sensor Ready bit set (1) end EventGroup-->>-TaskB: Required bit set<br>(Sensor Ready) rect rgb(209, 250, 229) Note over TaskB: Unblocked and running end TaskA->>+EventGroup: xEventGroupClearBits(WiFi Up) rect rgb(221, 214, 254) Note over EventGroup: EVENT GROUP<br>┌─────────────────────────┐<br>│ ☐ WiFi Up ☑ NTP Synced │<br>│ ☑ Sensor Ready │<br>└─────────────────────────┘<br>WiFi Up bit cleared (0) end
Creating an Event Group
You create an event group using xEventGroupCreate()
:
#include "freertos/event_groups.h"
EventGroupHandle_t xEventGroupCreate( void );
- Return Value (
EventGroupHandle_t
): Returns a handle to the created event group if successful (memory allocated), otherwise returnsNULL
. This handle is used in all other event group functions. - Initial State: All event bits in the newly created group are initially 0.
- Memory: Uses dynamically allocated memory from the FreeRTOS heap. Must be deleted using
vEventGroupDelete()
when no longer needed.
Setting Bits
You set one or more bits using xEventGroupSetBits()
from a task, or xEventGroupSetBitsFromISR()
from an ISR.
// From a Task
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
// From an ISR
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken );
xEventGroup
: The handle of the event group.uxBitsToSet
: A bitmask specifying which event bits to set. Use the bitwise OR operator (|
) to set multiple bits (e.g.,BIT0 | BIT4
).EventBits_t
is typically auint32_t
.pxHigherPriorityTaskWoken
(ISR version): Pointer to store whether a higher-priority task was unblocked. Used to potentially trigger a context switch (portYIELD_FROM_ISR
).- Return Value (
xEventGroupSetBits
): Returns the value of the event group’s bits after the specified bits have been set. - Return Value (
xEventGroupSetBitsFromISR
): ReturnspdPASS
if successful,pdFAIL
otherwise (e.g., invalid handle). - Behavior: Setting a bit that is already set has no effect. Any tasks waiting for the newly set bits might be unblocked.
Waiting for Bits
This is the core function for synchronization. A task blocks until certain bits are set using xEventGroupWaitBits()
.
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
xEventGroup
: The handle of the event group.uxBitsToWaitFor
: A bitmask specifying the bits the task is interested in.xClearOnExit
: IfpdTRUE
, the bits specified inuxBitsToWaitFor
that were set (causing the wait to complete) will be automatically cleared within the event group before the function returns. IfpdFALSE
, the bits remain set.xWaitForAllBits
: IfpdTRUE
, the function will only return if ALL bits specified inuxBitsToWaitFor
are set (logical AND). IfpdFALSE
, the function will return if ANY single bit specified inuxBitsToWaitFor
becomes set (logical OR).xTicksToWait
: Maximum time (in ticks) to block waiting for the condition.portMAX_DELAY
waits indefinitely.0
performs a non-blocking check.- Return Value (
EventBits_t
):- If the wait condition was met before the timeout: Returns the value of the event group’s bits before any bits were cleared (if
xClearOnExit
waspdTRUE
). - If the timeout occurred: Returns the value of the event group bits at the time of timeout. The bits that time out are indicated by the bits specified in
uxBitsToWaitFor
not being set in the returned value.
- If the wait condition was met before the timeout: Returns the value of the event group’s bits before any bits were cleared (if
Clearing Bits
Manually clear bits using xEventGroupClearBits()
(usually from a task).
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );
xEventGroup
: The handle of the event group.uxBitsToClear
: A bitmask specifying the bits to clear (set to 0).- Return Value (
EventBits_t
): Returns the value of the event group bits before the specified bits were cleared.
Other Functions
//Returns the current value of all bits in the event group (non-blocking).
EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup );
//Atomically sets bits (uxBitsToSet) and then waits for bits (uxBitsToWaitFor). Useful for rendezvous patterns.
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait );
//Deletes the event group and frees its memory.
void vEventGroupDelete( EventGroupHandle_t xEventGroup );
Function Signature | Description & Key Parameters | Return Value |
---|---|---|
xEventGroupCreate() |
Creates a new event group.
|
EventGroupHandle_t : Handle to the group, or NULL on failure. |
xEventGroupSetBits(xEventGroup, uxBitsToSet) |
Sets one or more bits in the event group (from a task).
|
EventBits_t : The event group value after bits are set. |
xEventGroupSetBitsFromISR(xEventGroup, uxBitsToSet, pxHigherPriorityTaskWoken) |
Sets bits from an ISR.
|
BaseType_t : pdPASS or pdFAIL . |
xEventGroupWaitBits(xEventGroup, uxBitsToWaitFor, xClearOnExit, xWaitForAllBits, xTicksToWait) |
Waits for a combination of bits to be set.
|
EventBits_t : Value of event group bits. If timeout, bits not matching uxBitsToWaitFor will indicate this. |
xEventGroupClearBits(xEventGroup, uxBitsToClear) |
Manually clears specified bits in the event group.
|
EventBits_t : Value of event group bits before clearing. |
xEventGroupGetBits(xEventGroup) |
Non-blockingly gets the current value of all bits in the event group. | EventBits_t : Current event group bits. |
xEventGroupSync(xEventGroup, uxBitsToSet, uxBitsToWaitFor, xTicksToWait) |
Atomically sets specified bits (uxBitsToSet ), then waits for specified bits (uxBitsToWaitFor ). Useful for rendezvous synchronization. |
EventBits_t : Value of event group bits upon completion or timeout. |
vEventGroupDelete(xEventGroup) |
Deletes an event group and frees its allocated memory. | None. |
Task Notifications
Task Notifications offer a different approach to inter-task communication and synchronization. Instead of using intermediate objects like queues or semaphores, tasks can directly notify or send data to another specific task.
- Concept: Each task has a dedicated 32-bit unsigned integer
ulNotifiedValue
and a notification state (Pending/Not-Pending). One task can update another task’s notification value and state. - Efficiency: Generally faster and more RAM-efficient than using queues or semaphores for simple, direct signaling or data passing between two tasks or from an ISR to a task. No separate RTOS object needs to be created or managed.
- Directness: A notification is sent directly to a specific target task handle. Only that target task can wait for and receive its own notification.
- Flexibility: The 32-bit notification value can be used in several ways:
- Lightweight Binary Semaphore: Use
xTaskNotifyGive()
/ulTaskNotifyTake()
. - Lightweight Counting Semaphore: Use
xTaskNotifyGive()
/ulTaskNotifyTake()
. - Lightweight Mailbox/Queue (single item): Use
xTaskNotify()
to send a value,xTaskNotifyWait()
to receive. - Lightweight Event Group: Use
xTaskNotify()
with bitwise operations,xTaskNotifyWait()
to wait for bits.
- Lightweight Binary Semaphore: Use
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans', 'primaryColor': '#6B46C1', 'primaryTextColor': '#000', 'primaryBorderColor': '#4C1D95', 'lineColor': '#6B46C1', 'secondaryColor': '#F0F4F8', 'tertiaryColor': '#EEEEF3' } } }%% flowchart TB %% TASK NOTIFICATION subgraph tn["Task Notification (Direct)"] direction LR TaskA1("Task A") -->|"xTaskNotify()"| TaskB1("Task B") subgraph TaskB1["Task B"] NotificationValue["Notification Value & State<br>uint32_t ulValue"] end TaskA1 -.->|"Directly<br>modifies"| NotificationValue note1["Analogy: Directly tapping someone<br>on the shoulder to get attention"] end %% QUEUE/SEMAPHORE subgraph ipc["Traditional IPC (Indirect)"] direction LR subgraph qm["Queue Mechanism"] TaskA2("Task A") -->|"xQueueSend()"| Queue[("Queue<br>Object")] Queue -->|"xQueueReceive()"| TaskB2("Task B") end subgraph sm["Semaphore Mechanism"] TaskA3("Task A") -->|"xSemaphoreGive()"| Semaphore[("Semaphore<br>Object")] Semaphore -->|"xSemaphoreTake()"| TaskB3("Task B") end note2["Analogy: Leaving a message<br>with a third party to deliver"] end %% COMPARISON NOTES comp1["✓ Direct task-to-task communication<br>✓ No intermediate object required<br>✓ Lower RAM usage<br>✓ Faster execution"] comp1 -.- tn comp2["× Requires separate object allocation<br>× Higher RAM usage<br>× Additional overhead<br>× Must be explicitly created"] comp2 -.- ipc %% STYLING classDef titleClass fill:none,stroke:none,color:#4338CA,font-size:18px,font-weight:bold classDef taskClass fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF,rounded:true classDef objectClass fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1,rounded:true classDef notificationClass fill:#F5F3FF,stroke:#6D28D9,stroke-width:2px,color:#5B21B6,rounded:true classDef compDirectClass fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46,rounded:true classDef compIndirectClass fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B,rounded:true classDef noteClass fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E,rounded:true class title titleClass class TaskA1,TaskA2,TaskA3,TaskB1,TaskB2,TaskB3 taskClass class Queue,Semaphore objectClass class NotificationValue notificationClass class comp1 compDirectClass class comp2 compIndirectClass class note1,note2 noteClass
Sending Notifications
There are several ways to send a notification:
xTaskNotifyGive()
/ vTaskNotifyGiveFromISR()
:
- Increments the target task’s notification value (like
xSemaphoreGive
on a counting semaphore).Sets the target task’s notification state to ‘Pending’.Designed to be used withulTaskNotifyTake()
.Ideal for emulating binary or counting semaphores.
// From Task
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
// From ISR
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken );
xTaskNotify()
/ xTaskNotifyFromISR()
:
- The most flexible way. Allows updating the target task’s notification value using various actions (
eNotifyAction
).Can set bits, overwrite the value, increment the value, or send no value (just signal).
// From Task
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
// From ISR
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken );
xTaskToNotify
: Handle of the task to notify.ulValue
: The value to use in the action (e.g., value to send, bits to set).eAction
: Specifies howulValue
updates the target’s notification value:eSetValueWithOverwrite
: Overwrites the target’s notification value withulValue
. (Like sending to a 1-item queue).eSetValueWithoutOverwrite
: Sets the target’s notification value only if the task does not already have a pending notification. ReturnspdFAIL
if already pending.eSetBits
: Performs a bitwise OR ofulValue
with the target’s notification value. (Like setting bits in an event group).eIncrement
: Increments the target’s notification value. (LikexTaskNotifyGive
).eNoAction
: Simply notifies the task without changing its value. (Like giving a binary semaphore).
- Return Value (
pdPASS
/pdFAIL
): Indicates success or failure (e.g.,eSetValueWithoutOverwrite
when already pending).
Waiting for Notifications
There are corresponding ways to wait for notifications:
ulTaskNotifyTake()
:
Designed to be used with xTaskNotifyGive()
.Waits for the calling task’s notification value to be non-zero.If xClearCountOnExit
is pdTRUE
, it clears the notification value to 0 before returning. If pdFALSE
, it decrements the value by one (like taking a counting semaphore).Returns the notification value before it was cleared or decremented.
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
xTaskNotifyWait()
:
- The most flexible waiting function, designed to be used with
xTaskNotify()
.Allows waiting for the notification state to be Pending.Can optionally clear specific bits in the notification value on entry and exit.
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait );
ulBitsToClearOnEntry
: A bitmask of bits to clear in the task’s notification value immediately upon entering the function (before blocking). Use0
to clear nothing. UseULONG_MAX
(orUINT32_MAX
) to clear the entire value (effectively resetting any previously received notification before waiting for a new one).ulBitsToClearOnExit
: A bitmask of bits to clear after receiving a notification and before returning. Use0
to clear nothing. UseULONG_MAX
(orUINT32_MAX
) to clear the value after it has been copied topulNotificationValue
.pulNotificationValue
: Pointer to auint32_t
where the notification value will be copied upon successful receipt (before exit bits are cleared). Can beNULL
if you don’t need the value.xTicksToWait
: Maximum time to block waiting for a notification.- Return Value (
pdTRUE
/pdFALSE
):pdTRUE
if a notification was received,pdFALSE
if the timeout occurred.
Key Considerations for Task Notifications
- Direct: Only the specific task being notified can wait for it. You cannot have multiple tasks waiting for the same notification signal directed at one task.
- Overwriting: If Task A sends a notification value (e.g., 10) to Task B, and then Task C sends another value (e.g., 20) using
eSetValueWithOverwrite
before Task B has processed the first notification, the value 10 is lost. This is different from a queue, which buffers items. - Simplicity: Best suited for simple signaling, event counting, or passing single data values where the overhead of queues/semaphores/event groups is undesirable.
Function Signature | Description & Key Parameters | Return/Output |
---|---|---|
Sending Notifications (Semaphore-like) | ||
xTaskNotifyGive(xTaskToNotify) |
(From Task) Increments the target task’s notification value and sets its state to ‘Pending’.
ulTaskNotifyTake() .
|
BaseType_t : pdPASS (always succeeds if handle is valid). |
vTaskNotifyGiveFromISR(xTaskToNotify, pxHigherPriorityTaskWoken) |
(From ISR) Same as xTaskNotifyGive , but for ISR context.
|
None. |
Sending Notifications (Flexible/Value-based) | ||
xTaskNotify(xTaskToNotify, ulValue, eAction) |
(From Task) Most flexible way to send a notification.
|
BaseType_t : pdPASS or pdFAIL (e.g., if eSetValueWithoutOverwrite and notification already pending). |
xTaskNotifyFromISR(xTaskToNotify, ulValue, eAction, pxHigherPriorityTaskWoken) |
(From ISR) Same as xTaskNotify , but for ISR context.
|
BaseType_t : pdPASS or pdFAIL . |
Waiting for Notifications (Semaphore-like) | ||
ulTaskNotifyTake(xClearCountOnExit, xTicksToWait) |
Waits for the calling task’s notification value to be non-zero.
xTaskNotifyGive() .
|
uint32_t : The task’s notification value before it was cleared/decremented, or 0 on timeout. |
Waiting for Notifications (Flexible/Value-based) | ||
xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait) |
Most flexible way to wait for a notification.
|
BaseType_t : pdTRUE if notification received, pdFALSE on timeout. |
Practical Examples
Example 1: Event Group for System Initialization
Wait for two simulated asynchronous initializations (e.g., Network and Sensor) before proceeding.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
static const char *TAG = "EVENT_GROUP_INIT";
// Define bits for our events
#define NETWORK_INITIALIZED_BIT (1 << 0) // Bit 0
#define SENSOR_INITIALIZED_BIT (1 << 1) // Bit 1
#define ALL_INIT_BITS (NETWORK_INITIALIZED_BIT | SENSOR_INITIALIZED_BIT)
// Declare the Event Group Handle
static EventGroupHandle_t system_event_group = NULL;
// Task simulating network initialization
void network_init_task(void *pvParameter)
{
ESP_LOGI(TAG, "Network Init Task: Starting initialization...");
vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate init time
ESP_LOGI(TAG, "Network Init Task: Network Initialized!");
// Set the network bit in the event group
xEventGroupSetBits(system_event_group, NETWORK_INITIALIZED_BIT);
vTaskDelete(NULL); // Delete self
}
// Task simulating sensor initialization
void sensor_init_task(void *pvParameter)
{
ESP_LOGI(TAG, "Sensor Init Task: Starting initialization...");
vTaskDelay(pdMS_TO_TICKS(3500)); // Simulate init time (longer)
ESP_LOGI(TAG, "Sensor Init Task: Sensor Initialized!");
// Set the sensor bit in the event group
xEventGroupSetBits(system_event_group, SENSOR_INITIALIZED_BIT);
vTaskDelete(NULL); // Delete self
}
// Main application task that waits for initialization
void main_app_task(void *pvParameter)
{
ESP_LOGI(TAG, "Main App Task: Waiting for system initialization...");
// Wait for BOTH network and sensor bits to be set
EventBits_t bits = xEventGroupWaitBits(
system_event_group, // The event group handle
ALL_INIT_BITS, // The bits to wait for
pdFALSE, // Don't clear bits on exit (optional here)
pdTRUE, // Wait for ALL bits (AND logic)
portMAX_DELAY); // Wait indefinitely
ESP_LOGI(TAG, "Main App Task: Received bits: 0x%x", (unsigned int)bits);
// Check if both bits are indeed set (they should be if we didn't time out)
if ((bits & ALL_INIT_BITS) == ALL_INIT_BITS) {
ESP_LOGW(TAG, "Main App Task: System Initialized! Starting main logic...");
// Proceed with application logic here...
} else {
ESP_LOGE(TAG, "Main App Task: Initialization failed or timed out?");
}
// Example finished for demo purposes
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_LOGI(TAG, "App Main: Starting Event Group Example.");
// Create the event group
system_event_group = xEventGroupCreate();
if (system_event_group == NULL) {
ESP_LOGE(TAG, "Failed to create event group.");
return; // Handle error
}
ESP_LOGI(TAG, "System event group created.");
// Create the initialization tasks and the main app task
xTaskCreate(network_init_task, "NetInit", 2048, NULL, 5, NULL);
xTaskCreate(sensor_init_task, "SensorInit", 2048, NULL, 5, NULL);
xTaskCreate(main_app_task, "MainApp", 2048, NULL, 4, NULL); // Lower priority
ESP_LOGI(TAG, "App Main: Tasks created.");
}
Build, Flash, Monitor:
- Save the code (e.g.,
main/event_group_example.c
). - Update
main/CMakeLists.txt
. - Build, Flash, and Monitor.
Observe:
- The
NetInit
andSensorInit
tasks start and simulate work. - The
MainApp
task starts and blocks (“Waiting for system initialization…”). - After 2 seconds, “Network Initialized!” appears, and the network bit is set.
MainApp
remains blocked as it needs both bits. - After another 1.5 seconds (total 3.5s), “Sensor Initialized!” appears, and the sensor bit is set.
- Now that both required bits are set,
xEventGroupWaitBits
inMainApp
unblocks. MainApp
logs “System Initialized!” and would proceed with its logic.
Example 2: Task Notification as Binary Semaphore
An ISR signals a task using vTaskNotifyGiveFromISR
and ulTaskNotifyTake
.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "NOTIFY_BINARY";
// Handle for the task to be notified
static TaskHandle_t worker_task_handle = NULL;
// Task that waits for the notification
void worker_task(void *pvParameter)
{
ESP_LOGI(TAG, "Worker Task started. Waiting for notification...");
while(1) {
// Wait indefinitely for a notification.
// Treat it like a binary semaphore: Clear count on exit (pdTRUE).
uint32_t notification_value = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (notification_value > 0) {
// Notification received! Value is usually 1 if Give was used once.
ESP_LOGW(TAG, "Worker Task: Notification received! (Value: %u)", (unsigned int)notification_value);
// Simulate processing
vTaskDelay(pdMS_TO_TICKS(500));
ESP_LOGI(TAG, "Worker Task: Processing finished.");
} else {
// Should not happen with portMAX_DELAY
ESP_LOGE(TAG, "Worker Task: Wait timed out?");
}
}
}
// Simulate an ISR giving the notification periodically
void simulated_isr_task(void *pvParameter) {
ESP_LOGI(TAG, "Simulated ISR Task started.");
while(1) {
// Simulate an event occurring every 2 seconds
vTaskDelay(pdMS_TO_TICKS(2000));
if (worker_task_handle != NULL) {
ESP_LOGI(TAG, "Simulated ISR: Event occurred! Giving notification.");
// --- This is how you would notify from a REAL ISR ---
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(worker_task_handle, &xHigherPriorityTaskWoken);
// Request context switch if needed
// portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // Correct way
if (xHigherPriorityTaskWoken == pdTRUE) {
// In simulation task, we can just yield
taskYIELD();
}
// --- End of real ISR code ---
}
}
}
void app_main(void)
{
ESP_LOGI(TAG, "App Main: Starting Task Notification (Binary Sema) Example.");
// Create the worker task, saving its handle
// Note: No need to create a semaphore object!
xTaskCreate(worker_task, "Worker", 2048, NULL, 5, &worker_task_handle);
// Create the simulated ISR task
if (worker_task_handle != NULL) {
xTaskCreate(simulated_isr_task, "SimISR", 2048, NULL, 6, NULL); // Higher priority
ESP_LOGI(TAG, "App Main: Tasks created.");
} else {
ESP_LOGE(TAG, "Failed to create worker task.");
}
}
Build, Flash, Monitor:
- Save the code (e.g.,
main/notify_binary_example.c
). - Update
main/CMakeLists.txt
. - Build, Flash, and Monitor.
Observe:
- The “Worker Task” starts and blocks waiting via
ulTaskNotifyTake
. - Every 2 seconds, the “Simulated ISR Task” calls
vTaskNotifyGiveFromISR
targeting the worker task. - The worker task unblocks, logs “Notification received!”, processes, and waits again.
- This achieves the same signaling behavior as Example 1 in Chapter 13 (Binary Semaphore), but without needing to create a separate semaphore object.
Example 3: Task Notification to Pass a Value
One task sends a simple command ID to another task using xTaskNotify
and xTaskNotifyWait
.
Code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "NOTIFY_VALUE";
// Define some command IDs
#define CMD_START 10
#define CMD_STOP 20
#define CMD_RESET 30
// Handle for the task to be notified
static TaskHandle_t command_processor_handle = NULL;
// Task that waits for command notifications
void command_processor_task(void *pvParameter)
{
uint32_t received_command = 0;
ESP_LOGI(TAG, "Command Processor Task started. Waiting for commands...");
while(1) {
// Wait indefinitely for a notification value.
// Clear the notification value on entry (ULONG_MAX) to ensure we wait for a *new* command.
// Clear the notification value on exit (ULONG_MAX) after processing.
BaseType_t result = xTaskNotifyWait(ULONG_MAX, // Clear bits on entry
ULONG_MAX, // Clear bits on exit
&received_command, // Pointer to store received value
portMAX_DELAY); // Wait indefinitely
if (result == pdPASS) {
// Notification received! Process the command value.
ESP_LOGW(TAG, "Command Processor: Received command ID: %u", (unsigned int)received_command);
switch(received_command) {
case CMD_START:
ESP_LOGI(TAG, " Action: Starting operation...");
break;
case CMD_STOP:
ESP_LOGI(TAG, " Action: Stopping operation...");
break;
case CMD_RESET:
ESP_LOGI(TAG, " Action: Resetting system...");
break;
default:
ESP_LOGW(TAG, " Action: Unknown command ID.");
break;
}
// Simulate processing
vTaskDelay(pdMS_TO_TICKS(100));
} else {
ESP_LOGE(TAG, "Command Processor: Wait timed out?");
}
}
}
// Task that sends commands periodically
void command_sender_task(void *pvParameter) {
uint32_t commands[] = {CMD_START, CMD_STOP, CMD_START, CMD_RESET};
int cmd_index = 0;
ESP_LOGI(TAG, "Command Sender Task started.");
while(1) {
// Cycle through commands every 3 seconds
vTaskDelay(pdMS_TO_TICKS(3000));
uint32_t command_to_send = commands[cmd_index];
cmd_index = (cmd_index + 1) % (sizeof(commands)/sizeof(commands[0]));
if (command_processor_handle != NULL) {
ESP_LOGI(TAG, "Command Sender: Sending command ID %u", (unsigned int)command_to_send);
// Send the command value, overwriting any previous notification
BaseType_t notify_result = xTaskNotify(command_processor_handle,
command_to_send,
eSetValueWithOverwrite);
if (notify_result != pdPASS) {
ESP_LOGE(TAG, "Command Sender: Failed to notify task!");
}
}
}
}
void app_main(void)
{
ESP_LOGI(TAG, "App Main: Starting Task Notification (Value Passing) Example.");
// Create the command processor task, saving its handle
xTaskCreate(command_processor_task, "CmdProc", 2560, NULL, 5, &command_processor_handle);
// Create the command sender task
if (command_processor_handle != NULL) {
xTaskCreate(command_sender_task, "CmdSend", 2048, NULL, 5, NULL);
ESP_LOGI(TAG, "App Main: Tasks created.");
} else {
ESP_LOGE(TAG, "Failed to create command processor task.");
}
}
Build, Flash, Monitor:
- Save the code (e.g.,
main/notify_value_example.c
). - Update
main/CMakeLists.txt
. - Build, Flash, and Monitor.
Observe:
- The
CmdProc
task starts and waits viaxTaskNotifyWait
. - Every 3 seconds, the
CmdSend
task sends a different command ID usingxTaskNotify
witheSetValueWithOverwrite
. - The
CmdProc
task unblocks, receives the command ID inreceived_command
, logs it, performs a simulated action based on the ID, and then waits again. - This demonstrates passing simple data values directly between tasks without a queue. Note that if
CmdSend
sent commands faster thanCmdProc
could process them, intermediate commands would be lost due to the overwrite behavior.
Variant Notes
- API Consistency: The FreeRTOS Event Group and Task Notification APIs (
xEventGroup...
,xTaskNotify...
,ulTaskNotifyTake
, etc.) are part of the core FreeRTOS kernel and behave identically across all ESP32 variants supported by ESP-IDF v5.x (ESP32, S2, S3, C3, C6, H2). - Performance: Task Notifications are generally the fastest IPC mechanism provided by FreeRTOS for applicable use cases, as they involve minimal overhead (direct manipulation of the target Task Control Block). Event Groups involve managing a separate RTOS object, similar to semaphores. Performance differences between ESP32 variants due to core type or clock speed exist but don’t change the relative efficiency of these mechanisms.
- Available Bits (Event Groups): As mentioned in the theory, the number of usable bits in an event group (8 or 24) depends on
configUSE_16_BIT_TICKS
. ESP-IDF typically defaults this to 0, providing 24 bits.
Common Mistakes & Troubleshooting Tips
Mistake / Symptom | Detailed Explanation & Impact | Troubleshooting Tips & Prevention |
---|---|---|
Event Groups | ||
Incorrect
xWaitForAllBits LogicSymptom: Task blocks indefinitely or unblocks too early.
|
Using pdTRUE (Wait For All) when pdFALSE (Wait For Any) is needed, or vice-versa, leads to incorrect synchronization logic. The task might wait for conditions that will never all be met simultaneously, or proceed when only a subset of necessary conditions are met.
|
|
Not Clearing Bits (when
xClearOnExit = pdFALSE )Symptom: Task waits correctly the first time, then xEventGroupWaitBits() returns immediately on subsequent calls, causing rapid/unintended processing.
|
If bits are not cleared after being processed (either by setting xClearOnExit to pdTRUE or manually calling xEventGroupClearBits() ), they remain set. Subsequent calls to xEventGroupWaitBits() for the same bits will find them already set and return immediately.
|
|
Using
xEventGroupWaitBits() in an ISRSymptom: System crash, assertion failure, or instability.
|
ISRs must not block. xEventGroupWaitBits() is a blocking function.
|
|
Event Group Handle is NULL
Symptom: Crash when trying to use event group functions.
|
xEventGroupCreate() might fail (e.g., due to insufficient heap memory) and return NULL . Using a NULL handle with other event group functions will cause issues.
|
|
Task Notifications | ||
Mismatched Notify/Wait Functions
Symptom: Notifications lost, task blocks unexpectedly, notification value incorrect.
|
Using xTaskNotify() with ulTaskNotifyTake() , or xTaskNotifyGive() with xTaskNotifyWait() , is generally not recommended as they are designed for different internal mechanics regarding the notification value and state.
|
|
Unexpected Notification Value Overwrite
Symptom: Receiving task only processes the last notification value; intermediate values are lost.
|
If multiple senders use xTaskNotify() with eSetValueWithOverwrite before the target task processes the notification, previous values are overwritten. Task notifications are not a queue; they typically hold only one pending notification value at a time (unless used as a counting semaphore via xTaskNotifyGive ).
|
|
Incorrect
ulBitsToClearOnEntry/Exit in xTaskNotifyWait() Symptom: xTaskNotifyWait() returns immediately on subsequent calls, or received value contains stale data from previous notifications.
|
If the notification value/state is not properly cleared, xTaskNotifyWait() might not block as expected or might return old data.
ulBitsToClearOnEntry : Clears bits in the task’s notification value *before* waiting.
ulBitsToClearOnExit : Clears bits *after* a notification is received and its value copied to *pulNotificationValue .
|
|
Notifying a NULL or Invalid Task Handle
Symptom: Notification functions return pdFAIL , or system instability if assertions are off.
|
The xTaskToNotify parameter must be a valid handle of an existing task. This handle is typically obtained when the target task is created.
|
|
Exercises
- Event Group – Multi-Sensor Ready:
- Create an event group.
- Define three bits:
SENSOR_A_READY
,SENSOR_B_READY
,SENSOR_C_READY
. - Create three separate tasks, each simulating the initialization of one sensor (A, B, C) with different delays (e.g., 1s, 2.5s, 1.8s). When a sensor task finishes init, it sets its corresponding bit in the event group.
- Create a fourth task that waits (
xEventGroupWaitBits
) for all three sensor bits to be set before printing “All sensors initialized and ready!”.
- Event Group – Error Broadcasting:
- Create an event group. Define one bit:
SYSTEM_ERROR_BIT
. - Create three “listener” tasks. Each listener task waits (
xEventGroupWaitBits
) forSYSTEM_ERROR_BIT
to be set (usexWaitForAllBits = pdFALSE
andxClearOnExit = pdFALSE
). When the bit is set, the listener task should print an error message specific to itself (e.g., “Listener A detected error!”) and perhaps enter a safe state (simulated by a loop with delay). - Create one “monitor” task that simulates detecting an error after a delay (e.g., 5 seconds). When it detects the error, it sets
SYSTEM_ERROR_BIT
usingxEventGroupSetBits
. - Observe how all three listener tasks unblock and react when the single error bit is set.
- Create an event group. Define one bit:
- Task Notification – Rate Limiter:
- Create a task (
worker_task
) that performs some action in a loop (e.g., prints a message “Processing item…”). - Create a second task (
timer_task
) that acts as a rate limiter. Every 500ms,timer_task
should notifyworker_task
usingxTaskNotifyGive()
. - Modify
worker_task
to callulTaskNotifyTake(pdTRUE, portMAX_DELAY)
at the beginning of its loop. It should only proceed with its action (“Processing item…”) after successfully taking the notification. - Observe how
worker_task
‘s processing rate is now limited by how oftentimer_task
sends notifications.
- Create a task (
- Task Notification – Configuration Update:
- Create a
config_manager
task. - Create two
worker
tasks (Worker A, Worker B). Store theirTaskHandle_t
s. config_manager
should periodically generate a simple configuration value (e.g., an integer representing a speed setting, cycling 10, 20, 30).- When the configuration changes,
config_manager
should usexTaskNotify
witheSetValueWithOverwrite
to send the new configuration value directly to both Worker A and Worker B (two separate calls toxTaskNotify
). - Each
worker
task should loop, waiting for a notification usingxTaskNotifyWait
. When a notification arrives, it should retrieve the value (*pulNotificationValue
) and print a message like “Worker A updated config to [value]”. Ensure you handleulBitsToClearOnEntry/Exit
appropriately inxTaskNotifyWait
to wait for new values.
- Create a
Summary
- Event Groups provide a way to manage and synchronize on multiple binary events (bits).
- Created using
xEventGroupCreate()
. - Bits are manipulated using
xEventGroupSetBits[FromISR]
,xEventGroupClearBits
. - Tasks wait for combinations of bits (AND/OR logic) using
xEventGroupWaitBits
. - Ideal for waiting for multiple conditions or broadcasting events.
- Created using
- Task Notifications offer a lightweight, direct task-to-task (or ISR-to-task) communication mechanism.
- No separate RTOS object needed; uses internal task state (
ulNotifiedValue
). - Faster and more RAM-efficient than queues/semaphores for simple cases.
- Can emulate binary semaphores (
xTaskNotifyGive
/ulTaskNotifyTake
), counting semaphores (xTaskNotifyGive
/ulTaskNotifyTake
), single-item queues (xTaskNotify
/xTaskNotifyWait
), or event flags (xTaskNotify
/xTaskNotifyWait
). - Key functions:
xTaskNotify[FromISR]
,xTaskNotifyGive[FromISR]
,xTaskNotifyWait
,ulTaskNotifyTake
.
- No separate RTOS object needed; uses internal task state (
- Choose the right tool: Use Queues for buffered data, Semaphores/Mutexes for signaling/locking, Event Groups for multi-event synchronization, and Task Notifications for direct, lightweight signaling or simple value passing.
Further Reading
- FreeRTOS Event Group Documentation: https://www.freertos.org/FreeRTOS-Event-Groups.html
- FreeRTOS Event Group API Reference: https://www.freertos.org/event_groups.h.html
- FreeRTOS Task Notification Documentation: https://www.freertos.org/RTOS-task-notifications.html
- FreeRTOS Task Notification API Reference: See
xTaskNotify
,xTaskNotifyGive
,xTaskNotifyWait
,ulTaskNotifyTake
in the FreeRTOS API docs. - ESP-IDF Programming Guide: FreeRTOS Helpers: (Includes Event Groups and potentially notes on Task Notifications for your specific ESP-IDF version/target) https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/system/freertos_idf.html#freertos-helpers
- “Using the FreeRTOS Real Time Kernel – A Practical Guide” – Chapters on Event Groups and Task Notifications.
