11. Notifications
1. Preamble
Without any prior discussion, let us rewrite (differently), the simple two-tasks synchronization case that we used earlier to illustrate binary semaphores:
That simple case was:
Task_1 : toggles a LED on a periodic vTaskDelay() basis, and increment a cycling counter. Every time that counter reaches 10, it is reset and a binary semaphore xSem is given.
Task_2 : Is blocked waiting for the xSem semaphore. When it is successfully taken, a message is printed into the console.
So we start by declaring two tasks:
// FreeRTOS tasks
void vTask1 (void *pvParameters);
void vTask2 (void *pvParameters);
But this time, we will need "handles" for these tasks, so we also declare (as global) these handles:
xTaskHandle vTask1_handle;
xTaskHandle vTask2_handle;
Then, in the main() function, we basically only creates the two tasks and start the scheduler. Note that handle address is provided to the xTaskCreate() function. One can assume that this function initializes and sets this handle.
// Main function
int main()
{
// Configure System Clock
SystemClock_Config();
// Initialize LED pin
BSP_LED_Init();
// Initialize Debug Console
BSP_Console_Init();
my_printf("Console Ready!\r\n");
// Start Trace Recording
xTraceEnable(TRC_START);
// Create Tasks
xTaskCreate(vTask1, "Task_1", 128, NULL, 2, &vTask1_handle);
xTaskCreate(vTask2, "Task_2", 128, NULL, 1, &vTask2_handle);
// Start the Scheduler
vTaskStartScheduler();
while(1)
{
// The program should never be here...
}
}
That's it. No synchronization kernel object has neither be declared nor created. And now the two tasks:
/*
* Task_1
* - Toggles LED every 100ms
* - Sends a notification to Task_2 every 1s
*/
void vTask1 (void *pvParameters)
{
uint16_t count;
count = 0;
while(1)
{
BSP_LED_Toggle();
count++;
// Notify Task_2 every 10 count
if (count == 10)
{
// Direct notification to Task_2
xTaskNotifyGive(vTask2_handle);
count = 0;
}
// Wait
vTaskDelay(100);
}
}
/*
* Task_2
* - Sends a message to console when a notification is received
*/
void vTask2 (void *pvParameters)
{
uint16_t count;
count = 0;
while(1)
{
// Wait here for a notification
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Reaching this point means that a notification has been received
// Display console message
my_printf("Hello %2d from task2\r\n", count);
count++;
}
}
Before going any further, make sure that notifications are enabled in your instance of FreeRTOS by looking in FreeRTOSConfig.h:
#define configUSE_PREEMPTION 1
#define configUSE_TICKLESS_IDLE 0
#define configCPU_CLOCK_HZ ( SystemCoreClock )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES 5
#define configMINIMAL_STACK_SIZE ( ( uint16_t ) 128 )
#define configMAX_TASK_NAME_LEN 16
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
#define configUSE_TASK_NOTIFICATIONS 1 // <-- Check this definition
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 0
#define configUSE_COUNTING_SEMAPHORES 0
#define configQUEUE_REGISTRY_SIZE 10
#define configUSE_QUEUE_SETS 0
#define configUSE_TIME_SLICING 0
#define configSTACK_DEPTH_TYPE uint16_t
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
#define configHEAP_CLEAR_MEMORY_ON_FREE 1
Save all , build , start the debugger and run the application.
You'll get the console message and the recorded trace. Everything works as expected.
As you can see, it works just the same as with binary semaphore, but without any kernel object involved in the process. There is no intermediary in that case. Task_1 directly "speaks" to Task_2:
So a question you may have is: If that's so simple, why haven't we started here before anything else? We could have. A binary semaphore is more versatile: once it has been given, it can be taken from any task.
When FreeRTOS was first released in 2003, only semaphores existed. In order to keep the source code compact, both binary semaphores, mutexes and message queues shared the same queue data structure (something we still have today). It was then observed over the course of FreeRTOS development that binary semaphores were 90% of the time employed to implement basic direct synchronization between two fixed tasks. While versatile, the queue data structure is not very efficient in both performance (CPU cycles) and memory usage for such a simple matter. Direct to task notifications (or simply notifications) were then first introduced in 2015 with the 8.2.0 release of FreeRTOS. The idea was to provide the best possible efficiency for simple two-tasks synchronization purposes.
Task notifications can therefore (and should) replace binary semaphores every time you only need to synchronize to known tasks.
If you are interested in FreeRTOS history, you can find the complete changelog (from the early releases, to latest version) here: https://www.freertos.org/History.txt
2. How does it work?
When you create a task by calling xTaskCreate() function, FreeRTOS allocates two memory regions:
The Task Control Block (TCB): It is basically a structure (literally, a C struct) that holds the necessary data members to control the task execution. The size of the TCB is fixed and under FreeRTOS responsibility. By providing a handle to the xTaskCreate() function, we get the address of the TCB region (i.e., a pointer).
The Stack: It is a memory reservoir that is attached to the task. It holds all the variables local to the task. When a task is preempted, its context is also pushed onto that stack so that is can be restored when the task resumes. It is your duty to specify the size of the stacks as FreeRTOS can't guess the amount of variables your task may manipulate at every moment. In the above example, the stack is 128 words (512 bytes).
By stepping over the xTaskCreate() functions you can use the Expressions view to spy TCB initialization:
We can notice:
- Both Task_1 and Task_2 TCBs are 92 bytes long
- The TCB has two members (arrays) related to notifications : ulNotifiedValue (32-bit) and ucNotifyState (8-bit). By default these arrays are of size 1.
We might as well have a look in the Memory view and locate both TCB and stack for the Task_2. As you can see, the stack has been initialized ("painted") with a 0xA5A5A5A5 pattern. That's not innocent, but that's a story for a separate tutorial (coming later, be reassured).
OK, so now that you know (almost) everything concerning tasks and their memory layout, let us see what happens when Task_1 "notifies" Task_2. We set a breakpoint in Task_1 at line xTaskNotifyGive(vTask2_handle) and step over while looking at Task_2 TCB... et voilĂ !
Task_1 directly wrote into Task_2 TCB with a notify value that is now '1', and a notification state that is now '2':
Notification states are defined as shown below, therefore when state = 2, it means that Task_2 "knows" that a notification has been received and is pending.
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 )
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )
You can easily guess what happens next... The ulTaskNotifyTake() in Task_2 that initially sets the notification state to taskWAITING_NOTIFICATION while entering a blocked state will now succeed and turn the task to the ready mode, waiting for the scheduler to grant CPU and resume. Very simple indeed, don't you think?
- Commit name "Notification as a binary semaphore" - Push onto Gitlab |
3. More on notifications
The two FreeRTOS API functions:
xTaskNotifyGive()
ulTaskNotifyTake()
are only there to provide a simple way to mimic binary semaphores with notifications. Actually, you can do a little more with notifications given that:
First, The notification value is a 32-bit integer, therefore you may use it for more than a Boolean value. It can be used to pass a data value, a pointer, or be defined as an array of bits (e.g., flags, similar to the Event Group approach) in a scheme for instance where different senders would use dedicated bits to communicate. As a matter of fact, the notified task does not know who is the sender unless you setup something for that.
Second, we have seen that ulNotifiedValue and ucNotifyState members in the TCB are defined as arrays of size 1. We can actually resize these arrays to get multiple 'slots' for notifications. That opens up many possibilities working with notifications.
3.1. Passing a value with notification
Say that we want Task_1 to communicate a single data value to Task_2. Edit vTask1() as follows:
/*
* Task_1
*
* - Toggles LED every 100ms
* - Sends a notification to Task_2 every 1s
*/
void vTask1 (void *pvParameters)
{
uint16_t count;
uint32_t time;
count = 0;
time = 0;
while(1)
{
BSP_LED_Toggle();
count++;
time++;
// Notify Task_2 every 10 count
if (count == 10)
{
// Direct notification to Task_2
xTaskNotify(vTask2_handle, time, eSetValueWithOverwrite );
count = 0;
}
// Wait
vTaskDelay(100);
}
}
The xTaskNotify() function is an alias to the generic xTaskGenericNotify() function, where argument #2 uxIndexToNotify is always 0, and argument #5 is always NULL:
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue ) PRIVILEGED_FUNCTION;
Argument #4 concerns the possible actions that you can associate with the notification, and are defined as follows:
/* Actions that can be performed when vTaskNotify() is called. */
typedef enum
{
eNoAction = 0, /* Notify the task without updating its notify value. */
eSetBits, /* Set bits in the task's notification value. */
eIncrement, /* Increment the task's notification value. */
eSetValueWithOverwrite, /* Set the task's notification value to a specific value even if the previous value has not yet been read by the task. */
eSetValueWithoutOverwrite /* Set the task's notification value if the previous value has been read by the task. */
} eNotifyAction;
So in the above example, we just want Task_1 to fill the notified value of Task_2 with the actual value of its local variable time.
Then edit vTask2():
/*
* Task_2
*
* - Sends a message to console when a notification is received
*
*/
void vTask2 (void *pvParameters)
{
uint16_t count;
uint32_t time;
count = 0;
while(1)
{
// Wait here for a notification
time = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Reaching this point means that a notification has been received
// Display console message
my_printf("Hello %2d from task2 - Time @task1 = %d\r\n", count, time);
count++;
}
}
Demonstration:
We can suspend execution anytime, and look at the upcoming notification from Task_1 by stepping over the xTaskNotify(vTask2_handle, time, eSetValueWithOverwrite ) function. We can observe that the value of the local time variable is actually written in the Task_2 TCB:
- Commit name "Notification with data value" - Push onto Gitlab |
3.2. Using multiple notification slots
As we already observed, both ulNotifiedValue and ucNotifyState members of the TCB are arrays with a dimension we can set by configuring FreeRTOS. Open FreeRTOSConfig.h and set the configTASK_NOTIFICATION_ARRAY_ENTRIES to 2:
#define configUSE_PREEMPTION 1
#define configUSE_TICKLESS_IDLE 0
#define configCPU_CLOCK_HZ ( SystemCoreClock )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES 5
#define configMINIMAL_STACK_SIZE ( ( uint16_t ) 128 )
#define configMAX_TASK_NAME_LEN 16
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
#define configUSE_TASK_NOTIFICATIONS 1
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 2 // <- 2 notification entries per TCB
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 0
#define configUSE_COUNTING_SEMAPHORES 0
#define configQUEUE_REGISTRY_SIZE 10
#define configUSE_QUEUE_SETS 0
#define configUSE_TIME_SLICING 0
#define configSTACK_DEPTH_TYPE uint16_t
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
#define configHEAP_CLEAR_MEMORY_ON_FREE 1
Now, we will consider a 3-tasks scenario where:
- Task_1 sends a notification to Task_3 on slot [0] every 500ms. The notification value is a pointer to a string to display.
- Task_2 sends a notification to Task_3 on slot [1] every 1s. The notification value is also a pointer to a string to display.
- Task_3 periodically polls its notification state on slot[0] and slot[1], and prints the messages upon notification.
Here we go. We first declare 3 tasks, and 3 handles:
// FreeRTOS tasks
void vTask1 (void *pvParameters);
xTaskHandle vTask1_handle;
void vTask2 (void *pvParameters);
xTaskHandle vTask2_handle;
void vTask3 (void *pvParameters);
xTaskHandle vTask3_handle;
Then in main() function, we create those 3 tasks. Note that Task_3 receives the lowest priority level because its ingrate job is printing...
// Main function
int main()
{
// Configure System Clock
SystemClock_Config();
// Initialize LED pin
BSP_LED_Init();
// Initialize Debug Console
BSP_Console_Init();
my_printf("Console Ready!\r\n");
// Start Trace Recording
xTraceEnable(TRC_START);
// Create Tasks
xTaskCreate(vTask1, "Task_1", 128, NULL, 3, &vTask1_handle);
xTaskCreate(vTask2, "Task_2", 128, NULL, 2, &vTask2_handle);
xTaskCreate(vTask3, "Task_3", 128, NULL, 1, &vTask3_handle);
// Start the Scheduler
vTaskStartScheduler();
while(1)
{
// The program should never be here...
}
}
Now we write vTask1() and vTask2(). Both tasks are basically doing the same thing with different activation periods. Because we now have more than one notification slot, we must use the xTaskNotifyIndexed() function that allows for specifying the slot index:
/*
* Task_1
* - Sends a notification to Task_3 every 500ms
*/
void vTask1 (void *pvParameters)
{
uint8_t msg[] = "Hello from task #1\r\n";
while(1)
{
// Notify Task_3 on slot #0
xTaskNotifyIndexed(vTask3_handle, 0, (uint32_t)msg, eSetValueWithOverwrite );
// Wait
vTaskDelay(500);
}
}
/*
* Task_2
* - Sends a notification to Task_3 every 1000ms
*/
void vTask2 (void *pvParameters)
{
uint8_t msg[] = "Hello from task #2\r\n";
while(1)
{
// Notify Task_3 on slot #1
xTaskNotifyIndexed(vTask3_handle, 1, (uint32_t)msg, eSetValueWithOverwrite );
// Wait
vTaskDelay(1000);
}
}
Then for the receiving Task_3, we can't easily setup a blocking condition on notifications because we now have two notification independent channels (slots). Blocking on one slot would prevent the detection of notifications arrival on the other slot. So the tactic is to activate Task_3 on a periodic basis using a simple vTaskDelay(), and then to poll the notification channels one after one with a zero timeout so that blocking never happens in attempts to read for notifications:
/*
* Task_3
* - Sends a message to console when a notification is received
*/
void vTask3 (void *pvParameters)
{
BaseType_t notif_pending;
uint8_t *pmsg;
uint8_t slot_index;
while(1)
{
BSP_LED_Toggle();
for(slot_index = 0; slot_index<2; slot_index++)
{
// Poll notification on slot #0 with no timeout
notif_pending = xTaskNotifyWaitIndexed(slot_index, 0, 0, (uint32_t *)&pmsg, 0);
// If a notification was received
if (notif_pending == pdPASS)
{
my_printf("Notification received on slot[%d] : %s", slot_index, pmsg);
}
}
// Polling period
vTaskDelay(100);
}
}
Arguments #2 and #3 of the xTaskNotifyWaitIndexed() are masks that we can use to clear bits in the notification value before and after reading. We're not using such capability here as we want to retrieve a complete 32-bit address, that points to the msg variables local to Task_1 and Task_2.
Save all , build , and fire a debugger session . Firstly, we can have a look on TCBs when the task are created. As expected, the size of the notification members array is now 2 (and consequently TCBs are now 96 bytes in size).
Then, we can make sure that both Task_1 and Task_2 write copies of their local variable msg address into the notification value slots of Task_3:
And finally, make sure everything works as expected:
Task_3 is not activated upon notification arrivals, but it reads and reacts to notification at his own pace (100ms):
- Commit name "Notifications on multiple slots" - Push onto Gitlab |
4. Summary
When to-task direct notifications have been introduced in FreeRTOS v8.2.0, it was a great leap forward for developers. Most of the time, notifications can replace binary semaphores for simple synchronization purposes, with better efficiency and memory savings.
Yet, the binary semaphore offers more flexibility, such as the ability to be taken by any task. Mutexes, Message Queues, Event Groups all offer mechanisms that notifications don't. So you can consider notifications as another weapon in your arsenal. Up to you to make good use of that weapon when it fits. I think I've already said that.
An obvious use case of notifications is the handling of a hardware interrupt, which most of time is used to activate a single waiting task. In a previous tutorial, we did that with a binary semaphore, but it would be better to use a notification instead. Have a look on the 'FromISR' versions of notification API functions and try by yourselves!