Efficient Inter-Task Communication with FreeRTOS Task Notifications

FreeRTOS task notifications offer a highly optimized mechanism for inter-task communication (IPC) and synchronization, serving as a lighter alternative to traditional message queues, semaphores, or mutexes. Unlike these kernel objects, task notifications do not require explicit creation or allocation of separate kernel memory. Instead, the necessary storage is intrinsically part of each task's Task Control Block (TCB).

During task creation via xTaskCreate, a dedicated region within the TCB_t structure is reserved for notification management. This includes a notification value and a notification state.

#if ( configUSE_TASK_NOTIFICATIONS == 1 )
    volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif

This design choice means there are no linked lists for blocked tasks waiting to send or recieve, simplifying the internal mechanisms. Consequently, the blocking behavior for task notifications differs from other IPC primitives:

  • A task sending a notification will never block; the operation either succeeds or fails immediately.
  • A task waiting to receive a notification can enter a blocked state until a notification arrives, at which point it is unblocked.

Each task maintains an internal notification state, primarily used when a task attempts to receive a notification. The possible states are:

  • taskNOT_WAITING_NOTIFICATION: The task is not currently suspended or blocked, expecting a notification.
  • taskWAITING_NOTIFICATION: The task has called a notification receive function and is currently blocked, awaiting a notification.
  • taskNOTIFICATION_RECEIVED: A notification has been dispatched to a task that was previously waiting or is about to processs it.

Consider a scenario where Task A sends a notification to Task B. When Task B invokes a notification reception function, its internal state transitions to taskWAITING_NOTIFICATION, and it may enter a blocked state. Upon Task A sending a notification, Task B's notification value is updated, and its state changes to taskNOTIFICATION_RECEIVED. This action immediately unblocks Task B. Once Task B processes the notification, its state reverts to taskNOT_WAITING_NOTIFICATION.

Notification API Functions

FreeRTOS provides a set of functions for sending and receiving task notifications. These functions can be broadly categorized into simpler, common-use macros and more versatile generic functions. The simpler macros often wrap the generic functions with default parameters.

Sending Notifications

  • xTaskNotifyGive( TaskHandle_t xTaskToNotify ): This macro is used to send a notification to a specific task (xTaskToNotify) and implicitly increments the target task's notification value by one. It's often used to signal an event or as a lightweight counting semaphore. This function will never cause the calling task to block. Internally, it maps to xTaskGenericNotify with eIncrement action.

  • xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ): This macro provides more control over how the notification value in the target task's TCB is updated. It also relies on xTaskGenericNotify but allows specifying a ulValue and an eAction. The eAction parameter determines the effect of the notification on the target task's ulNotifiedValue:

    • eNoAction: Sends a notification without modifying the target task's notification value. The ulValue parameter is ignored.
    • eSetBits: Performs a bitwise OR operation. The target task's notification value becomes (current_value | ulValue).
    • eIncrement: Increments the target task's notification value by one. Similar to xTaskNotifyGive. The ulValue parameter is ignored.
    • eSetValueWithoutOverwrite: Sets the target task's notification value to ulValue, but only if the target task's current notification value is 0. If it's non-zero, the value is not changed, preventing new data from overwriting unread data.
    • eSetValueWithOverwrite: Always sets the target task's notification value to ulValue, overwriting any previous unread value.

Receiving Notifications

  • ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ): This macro is designed for simple notification reception, particularly when notifications are used as binary or counting semaphores. It waits for a notification, returning the task's current notification value.

    • xClearCountOnExit: A boolean (pdTRUE or pdFALSE) indicating how the notification value should be handled upon exit:
      • pdTRUE: The notification value is reset to 0.
      • pdFALSE: The notification value is decremented by one.
    • xTicksToWait: The maximum time (in ticks) the task should wait in the blocked state for a notification. portMAX_DELAY can be used for an indefinite wait. The function returns the notification value before any clearing or decrementing operation.
  • xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t * pulNotificationValue, TickType_t xTicksToWait ): This macro offers the most flexible way to wait for and handle task notifications, especially useful when notifications carry specific bit patterns.

    • ulBitsToClearOnEntry: A bitmask. Any bits set in this mask will be cleared from the task's notification value before waiting for a new notification. This allows resetting specific flags.
    • ulBitsToClearOnExit: A bitmask. Any bits set in this mask will be cleared from the task's notification value after a notification has been received and the function is about to return.
    • pulNotificationValue: A pointer to a uint32_t variable where the notification value will be copied before ulBitsToClearOnExit is applied. If this parameter is NULL, the value is not copied.
    • xTicksToWait: The maximum time (in ticks) to wait for a notification.

Practical Example: Emulating a Counting Semaphore

Task notifications can effectively serve as lightweight counting semaphores. The following example demonstrates this using xTaskNotifyGive to "give" (increment) a semaphore-like count and ulTaskNotifyTake to "take" (decrement) it.

#include "FreeRTOS.h"
#include "task.h"
#include "stdio.h" // For printf

// Handles for our tasks
static TaskHandle_t xSenderTaskHandle = NULL;
static TaskHandle_t xReceiverTaskHandle = NULL;

// Sender task function
void vSenderTask(void *pvParameters) {
    (void)pvParameters; // Unused parameter

    const int notificationsToSend = 5; // Number of notifications to send

    while (1) {
        printf("Sender: Dispatching %d notifications...\r\n", notificationsToSend);
        for (int i = 0; i < notificationsToSend; i++) {
            // Increment the receiver's notification value
            xTaskNotifyGive(xReceiverTaskHandle);
            printf("Sender: Notification %d sent.\r\n", i + 1);
            vTaskDelay(pdMS_TO_TICKS(100)); // Small delay to show progression
        }
        printf("Sender: All notifications dispatched. Terminating sender task.\r\n");
        vTaskDelete(NULL); // Self-terminate after sending
    }
}

// Receiver task function
void vReceiverTask(void *pvParameters) {
    (void)pvParameters; // Unused parameter
    uint32_t receivedNotificationCount;

    while (1) {
        printf("Receiver: Awaiting notifications...\r\n");
        // Wait indefinitely for a notification. Decrement the count on exit.
        receivedNotificationCount = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
        printf("Receiver: Notification received. Current count (before decrement): %lu\r\n", receivedNotificationCount);
        // Simulate some work after receiving a notification
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// Main entry point for the FreeRTOS demo (rename if part of a larger project)
int main_task_notify_demo(void) {
    // Create the sender task with a higher priority
    xTaskCreate(vSenderTask, "Sender", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &xSenderTaskHandle);

    // Create the receiver task with a lower priority
    xTaskCreate(vReceiverTask, "Receiver", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, &xReceiverTaskHandle);

    // Start the scheduler, which will never return
    vTaskStartScheduler();

    // Should not reach here
    return 0;
}

This setup demonstrates that the vReceiverTask will block until vSenderTask sends a notification. Each xTaskNotifyGive increments the internal notification value of vReceiverTask. When ulTaskNotifyTake is called with pdFALSE, it retrieves the current notification count and then decrements it. If the count becomes zero, ulTaskNotifyTake will block again until another notification arrives. This effectively mimics the behavior of a counting semaphore, allowing multiple notifications to be queued implicitly and processed one by one.

Tags: FreeRTOS Task Notifications IPC rtos Synchronization

Posted on Fri, 14 Aug 2026 16:48:33 +0000 by dbakker