Learning FreeRTOS: Task Notifications

Scan to FollowLearn Embedded Together, learn and grow together

Learning FreeRTOS: Task Notifications

This is a series of introductory articles on FreeRTOS. While organizing my own knowledge, I hope to help beginners quickly get started and master the basic principles and usage methods of FreeRTOS.

Quick Start with FreeRTOS – Exploring the System

FreeRTOS Official Chinese Website is Now Live

FreeRTOS Coding Standards and Data Types

Quick Start with FreeRTOS – Task Management

Detailed Explanation of Message Queues in FreeRTOS

Learning FreeRTOS: Counting Semaphores

Learning FreeRTOS: Mutex Semaphores

Learning FreeRTOS: Event Groups

This article introduces the relevant content of FreeRTOS task notifications.

Task Notifications in FreeRTOS are a lightweight inter-task communication mechanism that allows tasks, interrupt service routines (ISRs), or other kernel objects to send event notifications or data directly to specific tasks.

Since its introduction in FreeRTOS version 8.2.0, task notifications have become an efficient alternative to traditional communication mechanisms (such as queues, semaphores, event groups, etc.) in many application scenarios.

Basic Features

  • Lightweight and Efficient: Task notifications operate directly on the task control block (TCB) without requiring additional storage structures, saving memory.
  • Fast Communication: Faster than traditional queue or semaphore operations, especially suitable for performance-sensitive scenarios.
  • Versatility: Can simulate binary semaphores, counting semaphores, event groups, and lightweight queues.
  • One-to-One Communication: Each notification is sent directly to a specific task, unlike global event groups or semaphores.
  • Low Latency: On most architectures, sending a notification requires only 10-30 instructions.

Components

Each task has a 32-bit notification value and a set of notification status flags:

  • Notification Value: Can be used to pass data or counts.
  • Notification Status:
    • eNoAction: No pending notifications.
    • eSetBits: Certain bits have been set.
    • eIncrement: Notification value has been incremented.
    • eSetValueWithOverwrite: Notification value has been overwritten.
    • eSetValueWithoutOverwrite: Notification value has been set (only if it was not previously set).

Working Principle

Internal Implementation Mechanism

The implementation of task notifications directly utilizes two fields in the FreeRTOS task control block (TCB):

typedef struct tskTaskControlBlock 
{
    // ...other fields...
    volatile uint32_t ulNotifiedValue;  // Notification value
    volatile uint8_t ucNotifyState;    // Notification status
    // ...other fields...
} tskTCB;

When a notification is sent, the kernel directly modifies these fields of the target task without needing to copy data or perform complex synchronization operations like queues.

Delivery Methods

Task notifications support multiple delivery methods:

  1. No Data Notification: Only changes the task’s notification status (similar to a semaphore).
  2. Data Notification: Updates the notification value and changes the status (similar to a message queue).
  3. Bitwise Operation Notification: Sets specific bits of the notification value (similar to event groups).
  4. Counting Notification: Increments the notification value (similar to counting semaphores).

API Details

FreeRTOS provides a rich set of APIs to operate task notifications, mainly divided into sending and receiving categories.

Sending Task Notifications

xTaskNotifyGive / vTaskNotifyGiveFromISR

Used for simple semaphore-style notifications (increasing the notification value):

BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
void vTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken);

xTaskNotify / xTaskNotifyFromISR

More comprehensive notification functions that can specify different operations:

BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, 
                      uint32_t ulValue, 
                      eNotifyAction eAction);

BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify,
                             uint32_t ulValue,
                             eNotifyAction eAction,
                             BaseType_t *pxHigherPriorityTaskWoken);

Among them, the <span>eAction</span> parameter can be:

  • <span>eNoAction</span>: Only marks the notification as arrived, does not modify the notification value.
  • <span>eSetBits</span>: Performs a bitwise OR operation on the notification value.
  • <span>eIncrement</span>: Increments the notification value (same as xTaskNotifyGive).
  • <span>eSetValueWithOverwrite</span>: Unconditionally overwrites the notification value.
  • <span>eSetValueWithoutOverwrite</span>: Only overwrites if the notification has not been processed.

xTaskNotifyAndQuery / xTaskNotifyAndQueryFromISR

Queries the previous notification value while sending a notification:

BaseType_t xTaskNotifyAndQuery(TaskHandle_t xTaskToNotify,
                              uint32_t ulValue,
                              eNotifyAction eAction,
                              uint32_t *pulPreviousNotifyValue);

Receiving Task Notifications

ulTaskNotifyTake

Used for semaphore-style reception (can clear or decrement the notification value):

uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
                         TickType_t xTicksToWait);
  • <span>xClearCountOnExit</span>: pdTRUE indicates that the notification value will be cleared upon exit, pdFALSE indicates decrementing.
  • <span>xTicksToWait</span>: Waiting timeout.

xTaskNotifyWait

A more general notification waiting function that can obtain the notification value and clear specific bits:

BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
                          uint32_t ulBitsToClearOnExit,
                          uint32_t *pulNotificationValue,
                          TickType_t xTicksToWait);
  • <span>ulBitsToClearOnEntry</span>: Which bits of the notification value to clear before waiting.
  • <span>ulBitsToClearOnExit</span>: Which bits of the notification value to clear before exiting.
  • <span>pulNotificationValue</span>: Used to return the received notification value.

Usage Patterns

Replacing Binary Semaphores

Sender:

xTaskNotifyGive(xTaskHandle);  // Send notification
// or
xTaskNotify(xTaskHandle, 0, eNoAction);

Receiver:

ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // Wait for notification

Replacing Counting Semaphores

Sender:

xTaskNotifyGive(xTaskHandle);  // Each call increases the notification value

Receiver:

ulTaskNotifyTake(pdFALSE, portMAX_DELAY);  // Each call decrements the notification value

Replacing Event Groups

Sender:

// Set specific bits
xTaskNotify(xTaskHandle, (1 << BIT_NUMBER), eSetBits);

Receiver:

uint32_t ulNotifiedValue;
xTaskNotifyWait(0, 0, &ulNotifiedValue, portMAX_DELAY);
if(ulNotifiedValue & (1 << BIT_NUMBER))
{
    // Handle event
}

Replacing Lightweight Queues

Sending Data:

xTaskNotify(xTaskHandle, dataValue, eSetValueWithOverwrite);

Receiving Data:

uint32_t receivedData;
xTaskNotifyWait(0, 0, &receivedData, portMAX_DELAY);

Advanced Usage

Multi-State Combined Notifications

Utilize different bits of the notification value to represent different states:

#define TASK_NOTIFY_BIT_ERROR    (1 << 0)
#define TASK_NOTIFY_BIT_DATA     (1 << 1)
#define TASK_NOTIFY_BIT_COMMAND  (1 << 2)

// Send multiple states
xTaskNotify(xTaskHandle, 
           TASK_NOTIFY_BIT_DATA | TASK_NOTIFY_BIT_COMMAND,
           eSetBits);

Priority-Based Message Passing

Utilize the high bits of the notification value to represent priority:

#define MSG_PRIORITY_HIGH   0x80000000
#define MSG_PRIORITY_MEDIUM 0x40000000
#define MSG_PRIORITY_LOW    0x20000000

void send_message(TaskHandle_t xTask, uint32_t msg, uint32_t priority) {
    xTaskNotify(xTask, msg | priority, eSetValueWithOverwrite);
}

Integration with RTOS Objects

Many FreeRTOS objects (such as timers, queues) can send task notifications upon completion of operations:

// Specify the task to receive notification when creating a timer
TimerHandle_t xTimer = xTimerCreate(
    "MyTimer",
    pdMS_TO_TICKS(1000),
    pdTRUE,
    NULL,
    vTimerCallback);
xTimerStart(xTimer, 0);

// Automatically send notification in the callback
void vTimerCallback(TimerHandle_t xTimer) {
    // The timer expiration will automatically notify the specified task
}

Usage

Applicable Scenarios

  1. High-Frequency Low-Latency Communication: Scenarios requiring rapid event or small data transmission.
  2. Memory-Constrained Environments: Where the storage overhead of queues or event groups cannot be afforded.
  3. One-to-One Communication: Direct communication between specific tasks.
  4. Replacing Simple Semaphores: Reducing the overhead of object creation and management.

Inapplicable Scenarios

  1. Broadcast Communication: Task notifications are one-to-one and not suitable for one-to-many communication.
  2. Large Data Transmission: The notification value is only 32 bits, not suitable for transmitting large amounts of data.
  3. Multi-Receiver Scenarios: When multiple tasks need to wait for the same event.
  4. FIFO Required Scenarios: Task notifications do not have a built-in queue mechanism.

Performance Optimization Tips

  1. Prefer ulTaskNotifyTake: Lighter than xTaskNotifyWait.
  2. Choose eAction Wisely: Select the most appropriate operation type based on the scenario.
  3. Reduce Blocking Time: When sending notifications in ISR, consider using the FromISR version.
  4. Bitwise Operation Optimization: Utilize the 32-bit notification value to pack multiple state flags.

Common Issues and Solutions

Issue 1: Notification Loss

  • Cause: Using eSetValueWithOverwrite will cause new notifications to overwrite unprocessed notifications.
  • Solution: Use eSetValueWithoutOverwrite or eSetBits/eIncrement instead.

Issue 2: Task Priority Inversion

  • Cause: High-priority tasks waiting for low-priority tasks to send notifications.
  • Solution: Design task priorities reasonably or use timeout mechanisms.

Issue 3: Long Waits in ISR

  • Cause: Incorrectly calling blocking APIs in ISR.
  • Solution: Only use FromISR version APIs in ISR and do not block.

Comparison of Task Notifications and Traditional Mechanisms

Feature Task Notifications Queues Semaphores Event Groups
Memory Usage Minimal (0 extra memory) Moderate (queue storage) Small (control structure) Small (control structure)
Speed Fastest Moderate Fast Fast
Data Transfer Capability 32-bit value Multi-byte None 32-bit flags
Multi-Receiver Support Not supported Supported Supported Supported
Broadcast Notification Not supported Limited support Not supported Supported
FIFO Order None Yes Yes No
Blocking Send Not supported Supported Supported Not supported

Application Examples

Sensor Data Collection and Processing

// Collection Task (Low Priority)
void vSensorTask(void *pvParameters) 
{
    while(1) 
    {
        uint32_t sensorData = read_sensor();
        xTaskNotify(xProcessingTaskHandle, sensorData, eSetValueWithOverwrite);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// Processing Task (High Priority)
void vProcessingTask(void *pvParameters) 
{
    uint32_t receivedData;
    while(1) 
    {
        if(xTaskNotifyWait(0, 0, &receivedData, portMAX_DELAY) == pdPASS) 
        {
            process_sensor_data(receivedData);
        }
    }
}

Command Processing and Status Feedback

// Command Sending
void send_command(TaskHandle_t target, uint8_t cmd, uint8_t param) 
{
    uint32_t notification = (cmd << 16) | param;
    xTaskNotify(target, notification, eSetValueWithOverwrite);
}

// Command Receiving and Processing
void command_task(void *pvParameters) 
{
    uint32_t notifValue;
    while(1) 
    {
        xTaskNotifyWait(0, 0, &notifValue, portMAX_DELAY);
        uint8_t cmd = (notifValue >> 16) & 0xFF;
        uint8_t param = notifValue & 0xFF;
        
        switch(cmd) 
        {
            case CMD_RESET: handle_reset(); break;
            case CMD_SET_PARAM: handle_set_param(param); break;
            // ...other command handling...
        }
    }
}

Conclusion

FreeRTOS task notifications are an efficient and flexible inter-task communication mechanism. When used correctly, they can significantly improve system performance and reduce resource consumption.

Through this detailed introduction, the main content learned includes:

  1. The working principles and advantages of task notifications.
  2. Usage scenarios and methods of various APIs.
  3. Replacing traditional communication mechanisms in appropriate scenarios.
  4. Avoiding common usage pitfalls and traps.
  5. Designing more efficient RTOS application architectures.

Although task notifications are powerful, they are not a panacea.

In actual projects, the appropriate communication mechanism should be selected based on specific needs, and sometimes it may be necessary to combine task notifications with other RTOS features to achieve the best results.

Learning FreeRTOS: Task Notifications

Follow 【Learn Embedded Together】 to become better together..

If you find the article good, click “Share”, “Like”, “Recommend”!

Leave a Comment