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 in FreeRTOSConfig.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():

C
#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 returns NULL. 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.

C
// 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 a uint32_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): Returns pdPASS 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().

C
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: If pdTRUE, the bits specified in uxBitsToWaitFor that were set (causing the wait to complete) will be automatically cleared within the event group before the function returns. If pdFALSE, the bits remain set.
  • xWaitForAllBits: If pdTRUE, the function will only return if ALL bits specified in uxBitsToWaitFor are set (logical AND). If pdFALSE, the function will return if ANY single bit specified in uxBitsToWaitFor 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 was pdTRUE).
    • 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.

Clearing Bits

Manually clear bits using xEventGroupClearBits() (usually from a task).

C
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

C
//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.
  • All event bits are initially 0.
  • Uses dynamically allocated memory.
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).
  • xEventGroup: Handle of the event group.
  • uxBitsToSet: Bitmask of bits to set (e.g., BIT0 | BIT4).
EventBits_t: The event group value after bits are set.
xEventGroupSetBitsFromISR(xEventGroup, uxBitsToSet, pxHigherPriorityTaskWoken) Sets bits from an ISR.
  • pxHigherPriorityTaskWoken: Set to pdTRUE if a higher priority task was unblocked.
BaseType_t: pdPASS or pdFAIL.
xEventGroupWaitBits(xEventGroup, uxBitsToWaitFor, xClearOnExit, xWaitForAllBits, xTicksToWait) Waits for a combination of bits to be set.
  • uxBitsToWaitFor: Bitmask of bits to wait for.
  • xClearOnExit: pdTRUE to clear bits that caused the wait to complete.
  • xWaitForAllBits: pdTRUE for AND logic (all bits), pdFALSE for OR logic (any bit).
  • xTicksToWait: Max blocking time.
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.
  • uxBitsToClear: Bitmask of bits to clear.
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.
%%{ 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 with ulTaskNotifyTake().Ideal for emulating binary or counting semaphores.

C
// 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).

C
// 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 how ulValue updates the target’s notification value:
    • eSetValueWithOverwrite: Overwrites the target’s notification value with ulValue. (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. Returns pdFAIL if already pending.
    • eSetBits: Performs a bitwise OR of ulValue with the target’s notification value. (Like setting bits in an event group).
    • eIncrement: Increments the target’s notification value. (Like xTaskNotifyGive).
    • 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.

C
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.

C
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). Use 0 to clear nothing. Use ULONG_MAX (or UINT32_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. Use 0 to clear nothing. Use ULONG_MAX (or UINT32_MAX) to clear the value after it has been copied to pulNotificationValue.
  • pulNotificationValue: Pointer to a uint32_t where the notification value will be copied upon successful receipt (before exit bits are cleared). Can be NULL 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’.
  • xTaskToNotify: Handle of the task to notify.
Designed for use with ulTaskNotifyTake().
BaseType_t: pdPASS (always succeeds if handle is valid).
vTaskNotifyGiveFromISR(xTaskToNotify, pxHigherPriorityTaskWoken) (From ISR) Same as xTaskNotifyGive, but for ISR context.
  • pxHigherPriorityTaskWoken: For scheduler.
None.
Sending Notifications (Flexible/Value-based)
xTaskNotify(xTaskToNotify, ulValue, eAction) (From Task) Most flexible way to send a notification.
  • xTaskToNotify: Handle of the task.
  • ulValue: Value to use in the action.
  • eAction: Action to perform (e.g., eSetValueWithOverwrite, eSetBits, eIncrement, eNoAction).
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.
  • pxHigherPriorityTaskWoken: For scheduler.
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.
  • xClearCountOnExit: pdTRUE to clear notification value to 0; pdFALSE to decrement it by one.
  • xTicksToWait: Max blocking time.
Designed for use with 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.
  • ulBitsToClearOnEntry: Bitmask of notification bits to clear before waiting (e.g., ULONG_MAX to clear all).
  • ulBitsToClearOnExit: Bitmask of notification bits to clear after receiving and before returning.
  • pulNotificationValue: Pointer to store the received notification value. Can be NULL.
  • xTicksToWait: Max blocking time.
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:

C
#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:

  1. Save the code (e.g., main/event_group_example.c).
  2. Update main/CMakeLists.txt.
  3. Build, Flash, and Monitor.

Observe:

  • The NetInit and SensorInit 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 in MainApp 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:

C
#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:

  1. Save the code (e.g., main/notify_binary_example.c).
  2. Update main/CMakeLists.txt.
  3. 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:

C
#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:

  1. Save the code (e.g., main/notify_value_example.c).
  2. Update main/CMakeLists.txt.
  3. Build, Flash, and Monitor.

Observe:

  • The CmdProc task starts and waits via xTaskNotifyWait.
  • Every 3 seconds, the CmdSend task sends a different command ID using xTaskNotify with eSetValueWithOverwrite.
  • The CmdProc task unblocks, receives the command ID in received_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 than CmdProc 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 Logic
Symptom:
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.
  • Verify Logic: Carefully review if the task needs ALL specified bits (AND logic -> pdTRUE) or if ANY of the specified bits is sufficient (OR logic -> pdFALSE).
  • Test Scenarios: Test with different combinations of bits being set to ensure behavior matches expectations.
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.
  • One-Shot vs. Persistent: Decide if bits represent one-shot events (clear after processing) or persistent states (remain set until explicitly cleared for other reasons).
  • Automatic Clear: Use xClearOnExit = pdTRUE in xEventGroupWaitBits() if the bits that caused the unblock should be cleared.
  • Manual Clear: If xClearOnExit = pdFALSE is used, or if bits need to be cleared based on other logic, call xEventGroupClearBits() or xEventGroupClearBitsFromISR() explicitly after the event is handled.
Using xEventGroupWaitBits() in an ISR
Symptom:
System crash, assertion failure, or instability.
ISRs must not block. xEventGroupWaitBits() is a blocking function.
  • ISR-Safe Functions: ISRs can use xEventGroupSetBitsFromISR() to set bits or xEventGroupGetBitsFromISR() (less common) to read bits.
  • Deferred Processing: If an ISR needs to trigger logic based on event group state that involves waiting, it should signal a task (e.g., via queue, semaphore, or task notification). The task can then safely call xEventGroupWaitBits().
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.
  • Check Return Value: Always check if xEventGroupCreate() returns a valid handle before using it.
  • Heap Space: Ensure enough FreeRTOS heap memory is available.
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.
  • Intended Pairs:
    • Use xTaskNotifyGive() / vTaskNotifyGiveFromISR() with ulTaskNotifyTake() (for semaphore-like behavior).
    • Use xTaskNotify() / xTaskNotifyFromISR() with xTaskNotifyWait() (for event-flag or value-passing behavior).
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).
  • Buffering Needed?: If all messages must be processed, use a FreeRTOS Queue instead.
  • Latest Value Only: If only the most recent value/event matters, eSetValueWithOverwrite is appropriate.
  • Check Pending: eSetValueWithoutOverwrite can be used if the sender needs to know if a notification is already pending (it will fail if so).
  • Bitwise OR: Use eSetBits if different senders/events should accumulate as flags in the notification value.
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.
  • Reset Before Wait: To wait for a fresh, distinct notification, set ulBitsToClearOnEntry to ULONG_MAX (or UINT32_MAX). This resets the notification state/value before the task blocks.
  • Clear After Read: To ensure the notification is consumed, set ulBitsToClearOnExit to ULONG_MAX (or the specific bits you consider processed).
  • Specific Bit Logic: If using the notification value as a set of event flags, clear only the specific bits you’ve processed on exit.
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.
  • Store Handle Correctly: Ensure the task handle of the receiver is correctly stored and accessible to the sender.
  • Task Existence: Verify the target task hasn’t been deleted before the notification is sent.
  • Check Return Values: Sender functions often return pdFAIL if the handle is invalid.

Exercises

  1. 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!”.
  2. Event Group – Error Broadcasting:
    • Create an event group. Define one bit: SYSTEM_ERROR_BIT.
    • Create three “listener” tasks. Each listener task waits (xEventGroupWaitBits) for SYSTEM_ERROR_BIT to be set (use xWaitForAllBits = pdFALSE and xClearOnExit = 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 using xEventGroupSetBits.
    • Observe how all three listener tasks unblock and react when the single error bit is set.
  3. 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 notify worker_task using xTaskNotifyGive().
    • Modify worker_task to call ulTaskNotifyTake(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 often timer_task sends notifications.
  4. Task Notification – Configuration Update:
    • Create a config_manager task.
    • Create two worker tasks (Worker A, Worker B). Store their TaskHandle_ts.
    • 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 use xTaskNotify with eSetValueWithOverwrite to send the new configuration value directly to both Worker A and Worker B (two separate calls to xTaskNotify).
    • Each worker task should loop, waiting for a notification using xTaskNotifyWait. When a notification arrives, it should retrieve the value (*pulNotificationValue) and print a message like “Worker A updated config to [value]”. Ensure you handle ulBitsToClearOnEntry/Exit appropriately in xTaskNotifyWait to wait for new values.

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.
  • 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.
  • 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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top