In this blog post, we will learn about Task Notification. We will see how task notification can be used in place of a binary semaphore.
In the previous project, we have practiced to use Queue to communicate between tasks and interrupt service routine (ISR). Queue is called a communication object. Data is sent from one task to the communication object. The other task then receives data by reading values of the communication object. The two tasks, sender and receiver, do not talk to each other directly. Instead of using an intermediate object, a task can directly send or receive data to/from other task using task notification. Using task notification is faster and consumes less RAM than using a communication object. However, it does have some limitations such as does not allow multiple tasks (more than two tasks) to communicate to each other, or it can not be used to send events or data to ISR.
In this post, we will implement a project which is similar to the button example in the previous post. Pressing a button will generate an interrupt, and send a notification to a task A. Task A will send a message to the Serial Monitor upon receiving the notification. Figure 1 demonstrates how the program works.
As can be seen from Figure 1, idle task is running most of the time. When a button is pressed, an interrupt signal is generated and ISR is running. In the ISR, we send a notification to task A. When ISR exits, task A receives notification and executes, in which it prints messages to the Serial Monitor. Upon completing sending the message to Serial Monitor, task A attempts to check for notification and goes to blocked state to wait for the next button interrupt.
Task notification APIs
We will have a look at task notification API functions. As can be seen from Figure 1, there are at least 2 functions that we need to look into. The first function is to send a notification from the ISR to a task (task A). The second function is to receive the notification.
Sending a notification
To send a notification to task A from ISR, we call function vTaskNotifyGiveFromISR() which has the function prototype
We’ll need to specify task A handle as the first input parameter in order to send a notification to task A. Sending a notification to task A will increase task A’s notification value by 1 and set the task A’s notification state to pending if it is not already pending.
In task A, we call ulTaskNotifyTake() function to check for notification value. ulTaskNotifyTake() allows task A to wait in Blocked state until its notification value to be greater than 0. The ulTaskNotifyTake() has the prototype:
We can specify to clear task A’s notification value to zero or decrease task A’s notification value by 1 before ulTaskNotifyTake() returns by setting xClearCountOnExit to pdTRUE or pdFALSE, respectively.
Running on Arduino
With the above task notification API functions, we can implement a program in Figure 1 in Arduino as below
How the code works
- Include the freeRTOS library for Arduino by putting #include <Arduino_FreeRTOS.h>. This is necessary to call freeRTOS functions to create tasks and run task notification API functions.
- Declare a variable task_A_handle of type TaskHandle_t. This variable is to keep a reference to task A and will be passed as an argument when we call vTaskNotifyGiveFromISR().
- In task A function, it is simply check for notification value by calling ulTaskNotifyTake() and specify maximum wait time in Blocked state as portMAX_DELAY. If the task notification value is zero, task A enters Blocked state. Otherwise, it sends a message to the Serial Monitor and clears and clear the notification value before it returns.
- The interrupt handler that is called whenever the button is pressed simply does one thing: sends a notification to task A and exits. When the button is pressed, the interrupt handler is called and executes. Task A notification value becomes greater than 0 and task A exits Blocked state and executes. After sending a message to Serial Monitor, task A checks for notification value again (which is now 0) and goes to Blocked state again to wait for the next notification. The result is whenever a button is pressed, a message is printed in the Serial Monitor.
- In the setup() function, the Serial Monitor is initialised. Button interrupt is configured to run interrupt handler whenever the button is pressed. Then task A is created with the task_A_handle address is the last argument in xTaskCreate(). Finally, it starts the scheduler.
- The loop() function is empty and should not be execute.
That’s all for today’s post. We’ll be learning more about FreeRTOS in coming posts.