Hello! Welcome to the final lesson in our module on FreeRTOS multitasking.
In previous lessons, we've built a solid toolkit for communication and synchronization between tasks. We used queues to pass copies of data, and mutexes to protect shared resources from race conditions.
Today, we address a very common and critical scenario in embedded systems: communication from an Interrupt Service Routine (ISR) to a task. While you could use a queue or a semaphore for this, FreeRTOS provides a more direct, faster, and memory-efficient tool designed for this exact purpose: Task Notifications.
This lesson will show you how to use these notifications to efficiently signal a task from an ISR, a key pattern for building responsive, event-driven systems.
1. The Challenge: ISR-to-Task Signaling
An Interrupt Service Routine (ISR) is special. It executes in response to a hardware event (like a timer expiring or a button press) and preempts any running task, no matter its priority. A core principle of robust RTOS design is to keep ISRs as short and fast as possible. They should do the absolute minimum work—like clearing the interrupt source and acknowledging the event—and then defer any lengthy processing to a regular task.
This pattern is called deferred interrupt processing. But how does the ISR "defer" the work? It needs a way to signal the handler task that an event has occurred. The classic approach is to use a binary semaphore.
Let's watch a video that demonstrates this pattern. It sets up a hardware timer to trigger an ISR, which then gives a semaphore to unblock a waiting task.
Introduction to RTOS Part 9 - Hardware Interrupts | Digi-Key Electronics
This video from Digi-Key demonstrates the 'deferred interrupt' pattern using a binary semaphore. It's a valid approach and provides the necessary context for understanding why task notifications are an improvement.
Watch the segment from 08:08 to 12:03. Focus on these key concepts: The idea of a 'deferred interrupt' to keep the ISR short. The use of the special FromISR version of the semaphore function: xSemaphoreGiveFromISR(). The xHigherPriorityTaskWoken variable and portYIELD_FROM_ISR(), which tell the scheduler to run the unblocked task immediately if its priority is high enough.
This semaphore-based approach works perfectly well. However, it has some overhead:
- Memory: You must create a semaphore object, which consumes RAM.
- Performance: Giving and taking a semaphore involves more complex logic than is strictly necessary for a simple, direct signal.
When you only need to signal one specific task, there is a better way.
2. A More Efficient Solution: Direct-to-Task Notifications
The video you just watched ends by teasing a faster, more efficient alternative. Let's see that brief conclusion.
Introduction to RTOS Part 9 - Hardware Interrupts | Digi-Key Electronics
The video ends by mentioning a superior method for this specific use case. Let's watch.
Watch from 12:03 to 12:27. The key insight is that direct-to-task notifications are a purpose-built, faster, and more efficient alternative when you know exactly which task you need to signal.
Task notifications are a core feature of FreeRTOS where each task has a built-in 32-bit notification value and a notification state. You don't need to create any extra objects like semaphores or queues. An ISR or another task can interact with this value directly, making it an extremely fast and lightweight method for signaling.
The basic flow is simple:
- A handler task "waits" for a notification. This blocks the task, consuming no CPU time.
- An ISR (or another task) "sends" a notification directly to the waiting task.
- The waiting task is unblocked and continues execution.
This diagram illustrates the process perfectly.

3. Using Notifications as a Lightweight Binary Semaphore
The most common use for a task notification is as a direct replacement for a binary semaphore. An event happens, and the ISR wakes up a single task to handle it.
The primary API functions for this pattern are:
vTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken): Called from an ISR to send a notification. It behaves likexSemaphoreGiveFromISR.ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait): Called from a task to wait for a notification.
Let's examine the official FreeRTOS documentation, which explains this pattern and highlights the performance benefits.
RTOS Task Notifications Used As Light Weight Binary Semaphores
This page from the official FreeRTOS documentation explains how to use a task notification as a high-speed, low-memory replacement for a binary semaphore. It's the definitive guide to this pattern.
Read the entire page. Pay close attention to: The introduction, which states that notifications are up to 45% faster and use less RAM. The explanation of ulTaskNotifyTake()'s first parameter, xClearCountOnExit. Setting it to pdTRUE mimics a binary semaphore. The code example showing an ISR (vTransmitEndISR) notifying a task (vAFunctionCalledFromATask). Note how the task handle must be known by the ISR.
Let's break down the key code structure from that example:
The Handler Task:
// A handle to this task must be stored somewhere the ISR can access it.
TaskHandle_t xHandlingTask = NULL;
void handlingTask(void *pvParameters) {
xHandlingTask = xTaskGetCurrentTaskHandle(); // Store my own handle
for (;;) {
// Block indefinitely waiting for a notification.
// The pdTRUE parameter clears the notification value back to 0,
// making it behave like a binary semaphore that is "taken".
uint32_t ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (ulNotificationValue > 0) {
// A notification was received. Process the event.
printf("Event received!\n");
}
}
}
The ISR:
void IRAM_ATTR my_isr(void* arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Notify the specific handler task.
vTaskNotifyGiveFromISR(xHandlingTask, &xHigherPriorityTaskWoken);
// If xHigherPriorityTaskWoken is now true, a context switch should be requested.
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
Notice the IRAM_ATTR attribute on the ISR. On the ESP32, this places the ISR code in fast internal RAM instead of flash memory, which is crucial for performance.
4. Handling Multiple Events with Counting Notifications
What if interrupts can fire multiple times before your handler task gets to run? A binary semaphore (or a notification used in the binary style) would lose these events. You'd know that at least one event occurred, but not how many.
Task notifications solve this elegantly by using the 32-bit notification value as a counter. Each time vTaskNotifyGiveFromISR() is called, FreeRTOS automatically increments the target task's notification value. The handler task can then read this value to determine how many events are pending.
Let's look at the documentation for this powerful pattern.
RTOS task notifications Used As Light Weight Counting Semaphores
When multiple events might queue up, a task notification can act as a counting semaphore. This documentation page shows two ways a task can handle the accumulated count.
Read the entire page, focusing on the two examples: Example 1: The task calls ulTaskNotifyTake(pdFALSE, ...). This decrements the count by one, allowing the task to process one event at a time within its loop. Example 2: The task calls ulTaskNotifyTake(pdTRUE, ...). This reads and clears the entire count at once, allowing the task to process all pending events in a while loop.
The second approach (Example 2) is often more efficient as it minimizes the number of times the task blocks and unblocks, allowing it to process a burst of events in one go.
Test your understanding!
You are designing a system to measure the RPM of a motor using a quadrature encoder. The encoder generates hundreds of pulses (interrupts) per second. Your task calculateRPMTask needs to process these pulses. Should you use a task notification in a binary or counting style? Why?
Show answer
You should use a task notification in a counting style. The interrupts will arrive very rapidly, likely faster than the calculateRPMTask can run, block, and unblock for each individual pulse. By using the notification as a counter, the ISR can simply increment the count for every pulse. The calculateRPMTask can then wake up periodically (e.g., every 100ms), read the total count by calling ulTaskNotifyTake(pdTRUE, ...) to get the number of pulses in that interval, and calculate the RPM. This is far more efficient than trying to process each pulse individually.
Conclusion
You have now completed the FreeRTOS module and have a powerful set of tools at your disposal for building complex, multitasking applications on the ESP32. Task notifications are a vital part of that toolkit, providing the most efficient way to handle signaling and event deferral from an ISR.
Key Takeaways:
- Deferred Interrupt Processing is a key pattern where an ISR does minimal work and signals a task to do the rest, keeping the ISR fast.
- Task Notifications are a lightweight, fast, and memory-efficient mechanism for signaling directly from an ISR to a specific task.
- They are built into every task and require no separate object creation, unlike semaphores or queues.
- Use
vTaskNotifyGiveFromISR()in the ISR andulTaskNotifyTake()in the handler task. - By setting the
xClearCountOnExitparameter ofulTaskNotifyTake(), you can use notifications to emulate either a binary semaphore (for single events) or a counting semaphore (for multiple queued events).
With your understanding of tasks, queues, mutexes, and now notifications, you are well-equipped to architect robust embedded software. In our next lesson, we will begin applying these concepts to a practical domain, starting the Advanced Motor Control module by looking at how H-bridges are used to control DC motors.