Hello! Welcome back to our course on embedded driver development.
In our last lesson, we mastered using message queues for safe and efficient communication between tasks. We saw how to pass data by value and by reference, and how to use structs to create versatile communication channels. This is a powerful pattern, but it's only half the story. A huge amount of activity in an embedded system originates not from other tasks, but from hardware peripherals via interrupts.
Today, we'll bridge that gap. This lesson focuses on a critical aspect of RTOS programming and a frequent topic in senior-level interviews. We'll address the learning outcome: Explain the constraints of calling RTOS API functions from an ISR and use the correct 'FromISR' variants.
By the end of this lesson, you will understand the fundamental differences between interrupt context and task context, and you will be able to correctly and safely signal tasks from an ISR using the special interrupt-safe API functions.
1. The Fundamental Constraint: Task Context vs. Interrupt Context
First, let's address a crucial question: why can't we just call a function like xQueueSend() or xSemaphoreTake() from an Interrupt Service Routine (ISR)?
The answer lies in the concept of execution context.
-
Task Context: A task is a formal entity managed by the RTOS scheduler. It has its own stack, a priority, and a state (Running, Ready, Blocked, Suspended). The scheduler can choose to stop a task and place it in the Blocked state—for example, if it's waiting for a queue to receive data. This is a normal, healthy part of multitasking.
-
Interrupt Context: An ISR is a raw hardware response to an event. It is not a task. It preempts the currently running task (and the scheduler) and takes over the CPU. An ISR does not have its own dedicated task state; it "borrows" the context of whatever was running. Crucially, an ISR can never be placed in the Blocked state. If an ISR were to block, it would never be "woken up" by the scheduler because the scheduler itself is preempted. The system would effectively hang, waiting for an interrupt to finish that will never finish.
This is why calling a standard RTOS API function with a non-zero block time from an ISR is strictly forbidden. It would attempt to block a non-task, leading to catastrophic failure.
06 FreeRTOS Tutorial: Handling Interrupt
To start, this short video segment clearly explains the problem with running long or blocking operations inside an ISR.
Watch the first 1 minute and 30 seconds of this video. The speaker provides a great visual analogy of how an ISR that gets 'stuck' can starve all other tasks in the system.
2. The Solution: Interrupt-Safe ...FromISR Functions
To solve this, FreeRTOS provides a special set of API functions that are guaranteed to be safe to call from an ISR. They are easily identifiable by their ...FromISR suffix, for example:
xSemaphoreGiveFromISR()xQueueSendToBackFromISR()xQueueReceiveFromISR()
These functions are different from their standard counterparts in two critical ways:
- They never block. They are designed to execute and return immediately. If a queue is full,
xQueueSendToBackFromISR()will not wait; it will simply return an error (errQUEUE_FULL). - They manage context switching differently. They include an extra parameter,
pxHigherPriorityTaskWoken, which is the key to efficient scheduling from an interrupt.
FreeRTOS-Kernel-Book/ch07.md at main
The official FreeRTOS documentation explains the rationale behind this design. Reading this will solidify your understanding of why this API separation exists.
Please read sections 7.2.1 ('The Interrupt Safe API') and 7.2.2 ('The Benefits of Using a Separate Interrupt Safe API'). This will clarify the core design philosophy.
3. Deferred Context Switching
When an ISR uses a ...FromISR function to, say, give a semaphore, it might unblock a task that was waiting for it. If this newly-unblocked task has a higher priority than the task that was originally interrupted, the scheduler should switch to it as soon as the ISR completes.
However, an ISR can't just call the scheduler directly. Doing so in the middle of an ISR could be inefficient, especially if the ISR gives multiple semaphores or sends multiple queue items. The ideal time to perform a context switch is right at the very end of the interrupt handler.
This is managed by a mechanism called deferred context switching:
- Before calling a
...FromISRfunction, you declare a local variable:BaseType_t xHigherPriorityTaskWoken = pdFALSE; - You pass a pointer to this variable into the
...FromISRfunction (e.g.,xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);). - Inside the API function, if unblocking a task results in a higher-priority task becoming ready to run, the function sets
xHigherPriorityTaskWokentopdTRUE. - At the end of your ISR, you call a special macro,
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);. - This macro checks the value of the flag. If it's
pdTRUE, it requests a context switch. If it'spdFALSE, it does nothing.
This ensures that a context switch is requested only if necessary, and it happens just once before exiting the interrupt, making the system highly efficient.
Introduction to RTOS Part 9 - Hardware Interrupts | Digi-Key Electronics
This video provides a great animation and code walkthrough of this exact mechanism using a binary semaphore.
Watch the segment from 08:08 to 11:23. Pay close attention to the taskWoken parameter (which is the same concept as xHigherPriorityTaskWoken), how it's used with xSemaphoreGiveFromISR, and the subsequent call to yield from the ISR.
4. Practical Implementation Patterns
This "deferred interrupt processing" pattern is fundamental. The ISR does the absolute minimum work (e.g., read a hardware register, clear an interrupt flag) and then signals a dedicated task to do the heavy lifting.
Pattern 1: Synchronizing with a Semaphore
This is the classic way to signal an event. An ISR "gives" a semaphore, and a high-priority task that was "taking" (and thus blocked on) that semaphore wakes up to process the event.
FreeRTOS-Kernel-Book/ch07.md at main
Let's study a complete code example of this pattern from the FreeRTOS book.
Read through section 7.4 ('Binary Semaphores Used for Synchronization') and review the code in 'Example 7.1'. Focus on the structure of vHandlerTask (Listing 7.7) which blocks on xSemaphoreTake, and the ISR ulExampleInterruptHandler (Listing 7.8) which uses xSemaphoreGiveFromISR and portYIELD_FROM_ISR.
Here's the essential code flow:
The Handler Task:
void vHandlerTask(void *pvParameters) {
for (;;) {
// Block indefinitely, waiting for the semaphore from the ISR.
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
// The ISR has occurred. Process the event.
process_the_data();
}
}
}
The Interrupt Service Routine:
void UART_RX_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Minimal work: clear the interrupt flag
UART->SR &= ~UART_FLAG_RXNE;
// Unblock the handler task.
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
// If xHigherPriorityTaskWoken is now pdTRUE, a context switch will be requested.
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
Pattern 2: Passing Data with a Queue
This builds directly on our last lesson. A semaphore just signals an event; a queue can signal an event and pass along associated data.
FreeRTOS-Kernel-Book/ch07.md at main
The FreeRTOS book also provides an excellent example of passing data from an ISR via a queue.
Read section 7.7 ('Using Queues within an Interrupt Service Routine') and review 'Example 7.4'. Note how the ISR in Listing 7.22 reads from one queue and writes to another, all using ...FromISR variants and a single xHigherPriorityTaskWoken variable.
Test your understanding!
You are writing a driver for a CAN peripheral. The peripheral generates an interrupt whenever a new message is received. You need to pass the received CAN message (which is an 8-byte payload plus a 32-bit ID) to a can_processing_task for decoding.
- What FreeRTOS object would you use for this communication?
- How would you define the data structure and create the object?
- Sketch out the essential lines of code inside your
CAN_RX_IRQHandler.
Show answer
-
A message queue is the ideal object because you need to transfer data, not just signal an event.
-
First, define a struct for the CAN message. Then, create a queue to hold these structs.
typedef struct { uint32_t id; uint8_t data[8]; } CAN_Message_t; // In your initialization code: QueueHandle_t xCanRxQueue; xCanRxQueue = xQueueCreate(10, sizeof(CAN_Message_t)); // Queue for 10 messages -
The ISR should be kept very short. It reads the data from hardware registers, populates the struct, and sends it to the queue.
void CAN_RX_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; CAN_Message_t rx_msg; // 1. Read message ID and data from CAN peripheral hardware registers rx_msg.id = CAN->RX_ID_REG; for (int i=0; i<8; i++) { rx_msg.data[i] = CAN->RX_DATA_REG[i]; } // 2. Clear the hardware interrupt flag CAN->INT_FLAG_REG = CAN_RX_FLAG_CLEARED; // 3. Send the message to the queue. No block time is specified (must be 0 for ISR). xQueueSendToBackFromISR(xCanRxQueue, &rx_msg, &xHigherPriorityTaskWoken); // 4. Request a context switch if a higher-priority task was unblocked. portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }
5. A Critical Detail: Interrupt Priorities
Here we move into an advanced topic that is essential for a senior developer role and a common source of hard-to-find bugs if misunderstood.
On ARM Cortex-M3/M4/M7 cores, FreeRTOS does not disable all interrupts when it enters a critical section. Instead, it disables interrupts only up to a certain configurable priority level. This allows very high-priority, low-latency interrupts to run even when the kernel is busy.
This behavior is controlled by configMAX_SYSCALL_INTERRUPT_PRIORITY in FreeRTOSConfig.h.
THE GOLDEN RULE: Any ISR that calls an interrupt-safe FreeRTOS API function (...FromISR) MUST have a hardware interrupt priority that is numerically equal to or greater than configMAX_SYSCALL_INTERRUPT_PRIORITY.
Remember that on ARM Cortex-M, a lower priority number means a higher logical priority. So if configMAX_SYSCALL_INTERRUPT_PRIORITY is 5, your ISR that calls xQueueSendFromISR must be configured with a priority of 5, 6, 7, etc. An ISR with priority 4 would be of a higher urgency and would not be blocked by the RTOS kernel. However, that ISR is forbidden from calling any FreeRTOS API functions, as it could corrupt the kernel's internal state.
ARM Cortex-M Interrupts and FreeRTOS: Part 3
This article from MCU on Eclipse is one of the best explanations of this concept available. It is crucial for your preparation for an advanced role.
Please read this entire article. It's short but dense with critical information. Pay very close attention to: The section on configMAX_SYSCALL_INTERRUPT_PRIORITY and its relation to the BASEPRI register. The example priority layout (USB, I2C, SCI, etc.) and the explanation of which interrupts are masked. The final summary points, which are excellent takeaways.
6. Interview Preparation: Key Questions
-
Question: "Why can't you call a standard FreeRTOS API function like
vTaskDelayorxQueueSendfrom an ISR? What would happen?"- Your Answer: "You can't call blocking functions from an ISR because an ISR doesn't have a task context that can be put into a Blocked state. An ISR preempts the scheduler itself. If it were to block, the system would hang as there is no mechanism for the scheduler to unblock it. Standard APIs might also not be reentrant in a way that is safe from interrupt context. This is why FreeRTOS provides special, non-blocking
...FromISRvariants."
- Your Answer: "You can't call blocking functions from an ISR because an ISR doesn't have a task context that can be put into a Blocked state. An ISR preempts the scheduler itself. If it were to block, the system would hang as there is no mechanism for the scheduler to unblock it. Standard APIs might also not be reentrant in a way that is safe from interrupt context. This is why FreeRTOS provides special, non-blocking
-
Question: "Explain the purpose of the
pxHigherPriorityTaskWokenparameter and theportYIELD_FROM_ISRmacro. Why is this 'deferred' context switch mechanism used?"- Your Answer: "When an ISR unblocks a task, that task might have a higher priority than the task that was interrupted. The
pxHigherPriorityTaskWokenparameter is a flag used by the...FromISRfunction to signal that such a situation has occurred. The ISR checks this flag at the end and callsportYIELD_FROM_ISRto request a context switch. This 'deferred' mechanism is efficient because it ensures the scheduler only runs once at the conclusion of the ISR, rather than potentially multiple times within it, and it ensures the system returns directly to the highest priority task that is now ready to run."
- Your Answer: "When an ISR unblocks a task, that task might have a higher priority than the task that was interrupted. The
-
Question: "You're debugging a system where calls to
xQueueSendFromISRseem to be corrupting kernel data structures, causing random crashes. The ISR itself seems simple. What's one of the first configuration settings you would check?"- Your Answer: "I would immediately check the hardware priority of the ISR in the NVIC and compare it against the
configMAX_SYSCALL_INTERRUPT_PRIORITYsetting inFreeRTOSConfig.h. This type of corruption is a classic symptom of an ISR calling a FreeRTOS API function from a logically higher (numerically lower) interrupt priority than is permitted. Such an interrupt isn't being correctly masked by the kernel's critical sections, allowing it to run concurrently with kernel operations and leading to race conditions and data corruption. The ISR's priority must be numerically equal to or greater thanconfigMAX_SYSCALL_INTERRUPT_PRIORITY."
- Your Answer: "I would immediately check the hardware priority of the ISR in the NVIC and compare it against the
Conclusion
You've now covered one of the most important and nuanced topics in RTOS programming. Properly handling interrupts and synchronizing them with tasks is the hallmark of a robust embedded system.
Key Takeaways:
- ISRs operate in a constrained interrupt context and must never block.
- Always use the non-blocking
...FromISRAPI variants to communicate with the RTOS from an interrupt. - Use the
xHigherPriorityTaskWokenandportYIELD_FROM_ISRpattern to efficiently perform a context switch at the end of an ISR. - Critically, ensure the hardware priority of any ISR calling a FreeRTOS API function is configured correctly with respect to
configMAX_SYSCALL_INTERRUPT_PRIORITY.
Next Lesson Preview:
We've talked a lot about tasks changing states, context switches, and ISRs preempting tasks. In our next lesson, we will learn how to actually see this happening. We will cover the learning outcome: Analyze RTOS behavior, including task states, context switches, and timing, using debugger trace features. This will allow you to visually confirm the concepts we've discussed and provide powerful tools for debugging complex timing issues.