The direct task notification is an event sent directly to a task. It is an efficient method for communication and synchronization between tasks, unlike other methods that use intermediary objects such as queues, event groups, and semaphores to send notifications indirectly.

Each task has an array of task notifications. Each task notification has a state that can be either “Pending” or “Not Pending”. It also has a 32-bit value that can be sent directly to a task. Sending a direct task notification to a task sets the state of the target task notification to “Pending”. The receiver task will then be unblocked and can read the notification. If the state is “Not Pending,” it means that no notification has been sent, and the receiver task will remain blocked until a new notification arrives.

Direct task notification value can also optionally be updated in one of the following ways:

  • Overwriting the value regardless of the target task not read the value yet.
  • Overwrite the value just if the target task has read the value.
  • Sets one or more bits in the value.
  • Increment by one the value.

Direct Task Notification functionality

Let's create a program with two tasks, one to send notifications and the other to process the data. In the next image, you can observe the task and the notification array, Let consider for this example that we only use the first index. The picture shows that task1 has data to send to the task2, observe that the task2 is currently in the blocked state because no notification has been received yet.

Now that Task1 sends the data to the notification array of task2, observe that task2 is in the blocked state yet.

When the notification is the array of task notifications, the task 2 is unblocked to receive the data.

When the data is received to be processed in task 2, the notification data can be cleared or overwritten according to the settings.

This explanation is about the base for using direct task notifications, the functionality explains the use of a task waiting for a notification, blocking and unblocking execution, and Notification management, where explanation is provided since the data is the task that sends the notification until the data is processed in the task receiver. Other types of uses as semaphores or arrays have a similar behavior explained in the examples provided.

The next API function is the basic function to send a task notification.

BaseType_t xTaskNotify( 
    TaskHandle_t xTaskToNotify,     /* Handle of the freeRTOS task that going to receive the notification */
    uint32_t ulValue,               /* Used to change the notification value of the target task */
    eNotifyAction eAction           /* Set the action to do with the notification value, actions is defined in the enum eNotifyAction */
);

When the task uses the next API function, to receive the notification, the task can change the state to “not pending“.

 BaseType_t xTaskNotifyWait( 
    uint32_t ulBitsToClearOnEntry,  /* Clear specific bits of task notification value when task start to waiting */
    uint32_t ulBitsToClearOnExit,   /* Clear specific bits of task notification value when task finish to waiting */
    uint32_t *pulNotificationValue, /* The variablke were the Task notification value will be passed */
    TickType_t xTicksToWait         /* The maximum time to wait in the Blocked state for a notification to be received */
);
💡
Info: Stram and message buffer use the task notification at array index 0. If you want to maintain the state of a task notification across a call to a Stream or Message Buffer API function then use a task notification at an array index greater than 0.

To use Task notification remember to enable it setting in 1 the definition value:

#define configUSE_TASK_NOTIFICATIONS            1

Code Example:

#include "bsp.h"

static void vTask1( void *parameters );     /* Creation of task function */
static void vTask2( void *parameters );     /* Creation of task function */

TaskHandle_t TaskH1 = NULL;                 /* Create the handle to task1 */
TaskHandle_t TaskH2 = NULL;                 /* Create the handle to task2 */

int main( void )
{
    HAL_Init( );
    /*enable RTT and system view*/
    SEGGER_SYSVIEW_Conf( );
    SEGGER_SYSVIEW_Start( );

    xTaskCreate( vTask1, "Task", 128u, NULL, 1u, &TaskH1 ); /* Register on Kernel the Task and pass the handler of the task1 */
    xTaskCreate( vTask2, "Task", 128u, NULL, 1u, &TaskH2 ); /* Register on Kernel the Task and pass the handler of the task2 */

    vTaskStartScheduler( );     /* Init the Kernel */

    return 0u;
}

static void vTask1( void *parameters )
{
    UNUSED( parameters );

    for( ;; )
    {
        xTaskNotify(TaskH2, 0, eNoAction);  /* Send a nptification to Task2 */
        vTaskDelay( 1000u );                /* Wait a second */
    }
}

static void vTask2( void *parameters )
{
    UNUSED( parameters );
    uint32_t ulNotifiedValue;                                               /* Variable where the Task notification value will be received */

    for( ;; )
    {
        xTaskNotifyWait(0, 0, &ulNotifiedValue, portMAX_DELAY);             /* Wait the notification from Task1 */
        SEGGER_SYSVIEW_PrintfHost( "Notification Received" );               /* Print the a message to indicate that notification is received */
        
        vTaskDelay( 100u );                                                 /* Wait a notification each 100ms */
    }
}

This simple example is to demonstrate how to use the API function described previously. Observe that just Task1 sends a notification to be received by Task2, when Task2 receives the notification this will print the message, but when Task2 does not receive anything the task enters in the blocked state until Task1 sends another notification.

Benefits and Usage Restrictions

Direct task notifications offer more flexibility compared to creating additional objects like queues, binary semaphores, or event groups. Unblocking an RTOS task with direct task notifications is 45% faster and uses less RAM than unblocking a task with intermediary objects like binary semaphores. Direct task notifications do not require additional memory allocation, unlike queues or semaphores, which need memory for their control structures and data storage. Additionally, the API for direct task notifications is straightforward and easy to use, reducing the complexity of your code.

As would be expected, these performance benefits require some use case limitations:

  1. RTOS task notifications can only be used when there is only one task that can be the recipient of the event. This condition is however met in the majority of real world use cases, such as an interrupt unblocking a task that will process the data received by the interrupt.
  2. Only in the case where an RTOS task notification is used in place of a queue: While a receiving task can wait for a notification in the Blocked state (so not consuming any CPU time), a sending task cannot wait in the Blocked state for a send to complete if the send cannot complete immediately.

Use cases

Previously was explained as a basic example but Direct task notification can be used in other ways, for example:

  • Task synchronization: A task can wait for a notification before continuing its execution, guaranteeing that the other task has completed a critical, operation. An example can be the event groups.
  • Interruptions: The direct notifications are useful to manage interruptions. An ISR can send a notification to a task to process an event, reducing the time in which the ISR needs to execute.
  • Counters and Semaphores: These can be used as binary or counter semaphores. Can increment the value task notification to count events and after a task can wait until the counter reaches a specific value.
  • Data Transfer: Although it's not their primary use, notifications can transfer simple data (such as an integer) between tasks. This is useful for quick signals or status indicators. An example of use is the mailbox.

Code Snippets

Exercises(Printf)

  1. Create a program that sends data using the Task notifications as a mailbox, The data sent must be a letter, task sender will send a different letter every 500ms, and must be sent until the message is complete when all the data is received print the full message on the terminal. Message to write “Hello world“.
  2. Create a program to send multiple notifications to a single task, use the array of notifications to manage the different notifications. Create just 3 tasks to send a different notification value to the receiver task, data can be numbers or letters, the receiver task must be able to manage the different values and print the data received.
  3. Create a program to execute an action when a signal is received, create 2 tasks, the first task is to send the signal, and the second task is to receive and process an action when the signal is received. First task sends a notification to the receiver task, when the receiver task detects the signal then print a message indicating that the signal is received. Use the direct task notifications as a binary semaphore.

Exercises

  1. Using the task notification as a mailbox, create a program that sends a pin LED as a message to the receiver task. The receiver task must process the data and switch the state of the LED each time the data is received.
  2. Modify the first Exercise(printf) to send different LED pin each execution, instead of sending a letter to a task. The receiver task must process each notification value by turning on the LED pin received. When all the pins are received then change the LED state to turn OFF.
  3. Create a program that processes a certain number of events to execute an action. Create a task to receive 3 events (signals), that will not execute anything until the 3 signals are received, and then print a message on the terminal to indicate that the 3 events occurred. To generate a notification use a button to trigger an interruption to send the notification to the receiver task.
  4. Use the notification index array to process different values, create a task to read each element of the index array of notifications to process the message, use 3 buttons to generate interruptions, each button must send a different pin LED in a different array position, Receiver task must process the data receiver in any of the array position. Avoid blocking the task if no notification is received.