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 toxTaskGenericNotifywitheIncrementaction. -
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 onxTaskGenericNotifybut allows specifying aulValueand aneAction. TheeActionparameter determines the effect of the notification on the target task'sulNotifiedValue:eNoAction: Sends a notification without modifying the target task's notification value. TheulValueparameter 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 toxTaskNotifyGive. TheulValueparameter is ignored.eSetValueWithoutOverwrite: Sets the target task's notification value toulValue, 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 toulValue, 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 (pdTRUEorpdFALSE) 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_DELAYcan 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 auint32_tvariable where the notification value will be copied beforeulBitsToClearOnExitis applied. If this parameter isNULL, 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.