FreeRTOS, an open-source real-time operating system, is widely used for embedded systems development. It offers robust multitasking capabilities and is particularly useful for systems where real-time performance is critical. When working with FreeRTOS on platforms like the ESP32, task management becomes central to optimizing performance. Among the various inter-task communication mechanisms, Task Notification stands out as one of the simplest and most efficient ways to send signals or data between tasks.
In this article, we will explore what task notifications are, how they work in FreeRTOS, and how to implement them on the ESP32.
What is Task Notification?
In FreeRTOS, tasks typically interact with each other through queues, semaphores, or direct task notifications. Task notifications are lightweight, low-overhead mechanisms that allow tasks to notify other tasks about events or send small pieces of information without needing complex data structures.
A task notification is essentially a 32-bit value stored within the FreeRTOS kernel for each task. One task can notify another by setting or clearing specific bits in this 32-bit value, which other tasks can then check.
Key Features of Task Notifications:
- Low Overhead: Task notifications require less memory and fewer processing resources compared to other inter-task communication methods like queues or semaphores.
- Atomic Operations: Task notifications are atomic, meaning no race conditions can occur when setting or checking the notification value.
- Single Word Notification: Each task has a single 32-bit notification value, which can be used to send small signals or data.
- Notification Wait with Timeout: Tasks can wait for a notification, either indefinitely or with a timeout.
- Fast Context Switching: The ESP32’s hardware support for FreeRTOS allows for fast context switching, making task notifications a highly efficient method of communication.
How Task Notifications Work
FreeRTOS provides two primary functions for task notifications:
- xTaskNotifyGive(): This function is used by one task to send a notification to another task.
- xTaskNotifyWait(): This function allows a task to wait for a notification or multiple notifications to occur.
There are also variations such as xTaskNotify and xTaskNotifyFromISR which provide additional flexibility for more complex scenarios.
Task Notification Workflow
- Task 1 may wait for a notification (using xTaskNotifyWait()).
- Task 2 may send a notification to Task 1 (using xTaskNotifyGive()).
- When Task 1 receives the notification, it can then perform a specific action or proceed with its execution.
Notification Value and Masking
In FreeRTOS, task notifications allow tasks to send and receive signals or small amounts of data. Each task in FreeRTOS has a 32-bit notification value that can be used to store information. This value is used for communication between tasks.
Notification Value:
- Every task in FreeRTOS has a 32-bit notification value that can hold any kind of information you want (like a flag or a number).
- This value is atomic, meaning it’s managed by FreeRTOS in such a way that no two tasks can change it at the same time, avoiding race conditions.
- A task can set, clear, increment, or read this value, depending on the function you call.
Masking:
- Masking refers to selectively looking at specific bits in the notification value. This allows you to check multiple flags (bits) at once without needing to use multiple notification values.
- You can use bit masks to set, clear, or check specific bits in the notification value, which is useful when you want to signal different events with the same notification.
Practical use of Notification value
A notification value is a 32-bit variable (which you can think of as a number) that a task can use to send a message. The task can set, increment, or clear the bits in this value.
Let’s say Task 1 has a notification value, and Task 2 wants to update that value by sending a notification to Task 1.
- Example: Task 1 might have the notification value set to 0x00 (all bits are cleared).
- Task 2 might send the value 0x01 to Task 1, which could represent that some event has happened (e.g., a sensor reading is ready).
Here’s a simple breakdown of the value:
- Task 1 could check if a particular bit is set to determine if an event has happened. For example:
- Bit 0 (0x01) might represent “Sensor data ready.”
- Bit 1 (0x02) might represent “Button pressed.”
- Bit 2 (0x04) might represent “Timeout occurred.”
Task 1 can then look at the value of the notification to decide what to do.
Practical use of Masking(Checking Specific Bits in the Notification Value)
Masking allows you to check or change only specific bits in the notification value.
- A bit mask is a value that allows you to isolate or change specific bits in a number. By performing a bitwise AND or bitwise OR operation, you can manipulate and check individual bits.
Let’s use a simple example to illustrate this:
Example: Task 1 Waits for Multiple Events Using Bit Masking
Suppose Task 1 has a 32-bit notification value, where:
Bit 0: Sensor data is ready
Bit 1: Button was pressed
Bit 2: Timeout has occurred
Task 1 can use bit masking to check for these events in a single call to xTaskNotifyWait(). When Task 2 sends a notification, Task 1 can selectively react to certain events by checking which bits are set.
Task Notification API in FreeRTOS
Below are some of the key FreeRTOS API functions related to task notifications:
xTaskNotifyGive()
This function is used to notify another task by setting the task’s notification value. Typically, it is called from a task that has completed an operation, signaling another task that it can now continue its work.
BaseType_t xTaskNotifyGive(TaskHandle_t xTask);
- xTask: The handle of the task to notify.
- Return Value: pdTRUE if the notification was sent successfully, or pdFALSE if an error occurred.
xTaskNotifyWait()
This function is used to wait for a notification from another task. It blocks the calling task until the notification is received or a timeout occurs.
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToWaitFor, uint32_t *pulNotificationValue, TickType_t xTicksToWait);
- ulBitsToClearOnEntry: A bit mask that specifies which bits to clear in the task’s notification value when the function is called.
- ulBitsToWaitFor: A bit mask that specifies which bits in the notification value to wait for.
- pulNotificationValue: A pointer to a variable where the current notification value is stored.
- xTicksToWait: The time to wait in ticks before returning if no notification is received.
xTaskNotify()
This function is a more flexible version of xTaskNotifyGive(). It allows you to send a specific value to a task’s notification.
BaseType_t xTaskNotify(TaskHandle_t xTask, uint32_t ulValue, eNotifyAction eAction);
- xTask: The task to notify.
- ulValue: The value to send to the task.
- eAction: The action to take on the notification value. This can be one of the following:
- eSetBits: Set (turn on) the specified bits.
- eClearBits: Clear (turn off) the specified bits.
- eToggleBits: Toggle (flip) the specified bits (i.e., set bits to 1 if 0, and to 0 if 1).
xTaskNotifyFromISR()
This is the ISR-safe version of xTaskNotifyGive(). It allows you to notify tasks from within an interrupt service routine (ISR).
BaseType_t xTaskNotifyFromISR(TaskHandle_t xTask, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken);
- xTask: The task to notify.
- ulValue: The value to set in the notification.
- eAction: The action to perform.
- pxHigherPriorityTaskWoken: Indicates whether a context switch is needed.
Implementing Task Notifications in ESP32
Below are working examples that demonstrate the usage of the FreeRTOS functions xTaskNotify(), xTaskNotifyWait(), xTaskNotifyGive(), and xTaskNotifyFromISR() with the ESP32, using the ESP-IDF framework. The examples include printf for logging, which is a typical way to output information for debugging purposes in embedded systems.
Example1 : Using xTaskNotify()
In this example, we will send a custom notification value from one task to another using xTaskNotify().
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// Task handles
TaskHandle_t task1Handle = NULL;
TaskHandle_t task2Handle = NULL;
// Task 1: Wait for notification and process it
void task1(void *pvParameters) {
uint32_t notificationValue;
while (1) {
// Wait for notification with timeout
BaseType_t result = xTaskNotifyWait(0x00, 0x01, ¬ificationValue, pdMS_TO_TICKS(1000));
if (result == pdPASS) {
printf("Task 1 received notification with value: %lu\n", notificationValue);
} else {
printf("Task 1 timeout, no notification received.\n");
}
}
}
// Task 2: Send notification to Task 1
void task2(void *pvParameters) {
uint32_t notificationValue = 12345;
while (1) {
// Send notification with a custom value
printf("Task 2 sending notification with value: %lu\n", notificationValue);
xTaskNotify(task1Handle, notificationValue, eSetValueWithOverwrite);
// Wait for a while before sending next notification
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void app_main(void) {
// Create Task 1 and Task 2
xTaskCreate(task1, "Task 1", 2048, NULL, 1, &task1Handle);
xTaskCreate(task2, "Task 2", 2048, NULL, 1, &task2Handle);
}
Explanation:
- Task 1 waits for a notification with a timeout of 1000 ms. If it receives a notification, it prints the value of the notification.
- Task 2 sends a custom notification value (12345) to Task 1 every 2 seconds.
- xTaskNotify() is used in Task 2 to send the notification.
Example2 : Using xTaskNotifyWait()
Here’s an example of using xTaskNotifyWait() to wait for a notification to occur and then perform an action based on the received notification.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// Task handles
TaskHandle_t task1Handle = NULL;
TaskHandle_t task2Handle = NULL;
// Task 1: Wait for a notification
void task1(void *pvParameters) {
uint32_t notificationValue;
while (1) {
// Wait for the notification and clear the bits we want to check for
BaseType_t result = xTaskNotifyWait(0x00, 0x01, ¬ificationValue, portMAX_DELAY);
if (result == pdPASS) {
printf("Task 1 received notification. Value: %lu\n", notificationValue);
} else {
printf("Task 1 did not receive a notification.\n");
}
}
}
// Task 2: Send notification to Task 1
void task2(void *pvParameters) {
while (1) {
printf("Task 2 sending notification\n");
xTaskNotify(task1Handle, 0x01, eSetBits); // Notify Task 1 to set the bit 0
// Wait before notifying again
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void app_main(void) {
// Create tasks
xTaskCreate(task1, "Task 1", 2048, NULL, 1, &task1Handle);
xTaskCreate(task2, "Task 2", 2048, NULL, 1, &task2Handle);
}
Explanation:
- Task 1 waits indefinitely (portMAX_DELAY) for a notification using xTaskNotifyWait(). It checks if any bit was set (in this case, bit 0).
- Task 2 sends a notification every 2 seconds, setting bit 0.
Example3 : Using xTaskNotifyGive()
Now let’s use xTaskNotifyGive() in this example. Task 1 will wait for a notification, and Task 2 will call xTaskNotifyGive() to notify Task 1.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Task handles
TaskHandle_t task1Handle = NULL;
TaskHandle_t task2Handle = NULL;
// Task 1: Wait for notification from Task 2
void task1(void *pvParameters) {
while (1) {
// Wait for a notification (no timeout, so it blocks indefinitely)
BaseType_t result = xTaskNotifyWait(0x00, 0x01, NULL, portMAX_DELAY);
if (result == pdPASS) {
printf("Task 1 received notification and proceeding.\n");
} else {
printf("Task 1 did not receive notification.\n");
}
}
}
// Task 2: Notify Task 1
void task2(void *pvParameters) {
while (1) {
printf("Task 2 notifying Task 1\n");
xTaskNotifyGive(task1Handle); // Notify Task 1
// Wait for some time before giving another notification
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void app_main(void) {
// Create tasks
xTaskCreate(task1, "Task 1", 2048, NULL, 1, &task1Handle);
xTaskCreate(task2, "Task 2", 2048, NULL, 1, &task2Handle);
}
Explanation:
- Task 1 waits indefinitely for a notification using xTaskNotifyWait(). It will unblock when xTaskNotifyGive() is called.
- Task 2 calls xTaskNotifyGive() to notify Task 1 every 2 seconds.
Example4 : Using xTaskNotifyFromISR()
Finally, we will demonstrate xTaskNotifyFromISR(), which is used to notify tasks from within an interrupt service routine (ISR). This is useful when you need to notify a task from an interrupt context.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/portmacro.h"
// Task handles
TaskHandle_t task1Handle = NULL;
// Simulate an interrupt-like function (timer, GPIO interrupt, etc.)
void simulated_isr_notify(void) {
BaseType_t higherPriorityTaskWoken = pdFALSE;
// Send notification to Task 1 from ISR
xTaskNotifyFromISR(task1Handle, 0x01, eSetBits, &higherPriorityTaskWoken);
// Force a context switch if needed
portYIELD_FROM_ISR(higherPriorityTaskWoken);
}
// Task 1: Wait for notification from ISR
void task1(void *pvParameters) {
while (1) {
// Wait for notification (blocks indefinitely)
BaseType_t result = xTaskNotifyWait(0x00, 0x01, NULL, portMAX_DELAY);
if (result == pdPASS) {
printf("Task 1 received notification from ISR.\n");
} else {
printf("Task 1 did not receive notification.\n");
}
}
}
void app_main(void) {
// Create Task 1
xTaskCreate(task1, "Task 1", 2048, NULL, 1, &task1Handle);
// Simulate an interrupt (ISR) that notifies Task 1
while (1) {
// Simulate an ISR (for example, by calling this function from a timer interrupt)
simulated_isr_notify();
// Wait for some time before triggering again
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
Explanation:
- Task 1 waits for a notification using xTaskNotifyWait() (indefinitely).
- Simulated ISR (function simulated_isr_notify()) calls xTaskNotifyFromISR() to notify Task 1 when an event occurs (simulating an interrupt).
- The portYIELD_FROM_ISR() function ensures that a context switch occurs if needed.
Example5 – Practical use of notification value and masking
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Task handles
TaskHandle_t task1Handle = NULL;
TaskHandle_t task2Handle = NULL;
// Task 1: Wait for specific event notifications using bit masking
void task1(void *pvParameters) {
uint32_t notificationValue;
while (1) {
// Wait for a notification that sets either bit 0, 1, or 2
BaseType_t result = xTaskNotifyWait(0x00, 0x07, ¬ificationValue, portMAX_DELAY); // Mask 0x07 means check bits 0, 1, and 2
if (result == pdPASS) {
if (notificationValue & 0x01) { // Check if bit 0 is set (Sensor data ready)
printf("Sensor data is ready\n");
}
if (notificationValue & 0x02) { // Check if bit 1 is set (Button pressed)
printf("Button was pressed\n");
}
if (notificationValue & 0x04) { // Check if bit 2 is set (Timeout occurred)
printf("Timeout occurred\n");
}
}
}
}
// Task 2: Notify Task 1 of events using bit masking
void task2(void *pvParameters) {
while (1) {
// Simulate notifying Task 1 about different events
printf("Task 2 notifying Task 1...\n");
// Notify Task 1 that the sensor data is ready (set bit 0)
xTaskNotify(task1Handle, 0x01, eSetBits);
vTaskDelay(pdMS_TO_TICKS(1000));
// Notify Task 1 that the button was pressed (set bit 1)
xTaskNotify(task1Handle, 0x02, eSetBits);
vTaskDelay(pdMS_TO_TICKS(1000));
// Notify Task 1 that a timeout occurred (set bit 2)
xTaskNotify(task1Handle, 0x04, eSetBits);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void) {
// Create Task 1 and Task 2
xTaskCreate(task1, "Task 1", 2048, NULL, 1, &task1Handle);
xTaskCreate(task2, "Task 2", 2048, NULL, 1, &task2Handle);
}
Explanation
- Task 1 waits for a notification using xTaskNotifyWait(). The mask 0x07 means that it will check for any change in the least significant 3 bits (bits 0, 1, and 2).
- Task 2 sends notifications to Task 1, setting bits 0, 1, or 2 using xTaskNotify()
- 0x01 sets bit 0 (Sensor data ready).
- 0x02 sets bit 1 (Button pressed).
- 0x04 sets bit 2 (Timeout occurred).
Key Points:
- Task 1 can react to multiple events by checking the individual bits in the notification value using bit masking.
- In xTaskNotifyWait(), the mask 0x07 means that Task 1 is interested in bits 0, 1, and 2. The notification value passed to xTaskNotify() can contain any combination of these bits.
- Task 1 can then check which bits are set and take action accordingly. For example, if 0x01 is received, it knows that the sensor data is ready.
xTaskNotify vs xTaskNotifyGive
- xTaskNotify() is a more flexible function that allows you to send a 32-bit notification value to another task. This notification value can be used to signal specific events or carry data between tasks.
- Flexibility:You can send any 32-bit value (not just a single bit) to represent an event or condition.
- Bit Masking:You can use bit masks to handle multiple events or flags within a single notification value (like a bitfield)
// Task 1: Wait for notifications and process based on the notification value
void task1(void *pvParameters) {
uint32_t notificationValue;
while (1) {
// Wait for any notification
xTaskNotifyWait(0x00, 0x07, ¬ificationValue, portMAX_DELAY); // Mask: check bits 0, 1, and 2
// Check which bit was set
if (notificationValue & 0x01) {
printf("Sensor data is ready\n");
}
if (notificationValue & 0x02) {
printf("Button was pressed\n");
}
if (notificationValue & 0x04) {
printf("Timeout occurred\n");
}
}
}
// Task 2: Notify Task 1 with a custom notification value
void task2(void *pvParameters) {
while (1) {
// Send a notification to Task 1
xTaskNotify(task1Handle, 0x01, eSetBits); // Set bit 0 (Sensor data ready)
vTaskDelay(pdMS_TO_TICKS(1000));
xTaskNotify(task1Handle, 0x02, eSetBits); // Set bit 1 (Button pressed)
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
- xTaskNotifyGive() is a more specialized version of xTaskNotify() that is designed for task synchronization and is typically used when you only need to notify a task that it should proceed. It essentially increments the notification value by 1
- xTaskNotifyGive() is typically used in producer-consumer situations, where one task signals another task by simply telling it to continue or perform a certain action. The task being notified increments its notification value each time it is given a signal.
- It is equivalent to xTaskNotify() with the following behavior:
- It increments the notification value by 1.
- It uses the eSetBits action implicitly.
// Task 1: Wait for notifications and perform actions
void task1(void *pvParameters) {
uint32_t notificationCount = 0;
while (1) {
// Wait for a notification (notification count will be incremented)
xTaskNotifyWait(0x00, 0x00, ¬ificationCount, portMAX_DELAY);
// Perform an action after receiving a notification
printf("Task 1 received notification %d times\n", notificationCount);
}
}
// Task 2: Give Task 1 a signal (increment the notification value)
void task2(void *pvParameters) {
while (1) {
// Increment the notification value of Task 1
xTaskNotifyGive(task1Handle);
vTaskDelay(pdMS_TO_TICKS(1000)); // Delay for 1 second before giving the next notification
}
}
Conclusion
Task notifications in FreeRTOS are a powerful, lightweight mechanism for inter-task communication, especially on memory-constrained devices like the ESP32. They offer efficient synchronization, low overhead, and easy implementation. Whether you’re building a real-time system or handling multiple tasks concurrently, task notifications can significantly improve your system’s responsiveness and resource usage.
By using task notifications effectively, you can create highly responsive embedded applications on the ESP32 with minimal code complexity.