Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Deferred Interrupt Handling with RTOS Tasks

Recommended resources

Hello! Welcome to your next lesson on RTOS fundamentals.

In our last session, we analyzed priority inversion, a critical concurrency hazard, and compared the Priority Inheritance and Priority Ceiling protocols used to solve it. We focused on synchronization challenges between tasks. Today, we shift our attention to the crucial boundary between hardware and software: handling interrupts.

While your embedded systems background has undoubtedly given you extensive experience writing Interrupt Service Routines (ISRs), in an RTOS context, we must follow specific patterns to maintain system stability and real-time performance. Simply doing all the work inside an ISR is a recipe for disaster in a multitasking environment.

This lesson's objective is to: Implement deferred interrupt handling in an RTOS by signaling a high-priority task from an ISR. This is a fundamental design pattern that interviewers at companies like Qualcomm, NVIDIA, and AMD will expect you to know cold.

1. The Problem: Why ISRs Must Be Short

In a preemptive RTOS, an interrupt is the ultimate high-priority event. When a hardware interrupt occurs, the CPU immediately stops whatever task it's running—even the highest-priority one—and jumps to the corresponding ISR.

This means a long-running ISR effectively freezes the entire multitasking system. This leads to several critical problems:

  • Increased System Latency: No other tasks can run, and often, no other interrupts can be serviced, until the current ISR completes. A long ISR can cause other, more critical interrupts to be missed.
  • Task Jitter: The execution of real-time tasks becomes unpredictable. A task might be preempted by an ISR for an unknown and variable amount of time, causing it to miss its deadlines.
  • Unsafe Operations: Many standard C library functions (printf, malloc, free) and most RTOS API functions are not "interrupt-safe" because they are not reentrant or may attempt to block. Calling them from an ISR can lead to data corruption or system crashes.

The solution is to do the absolute minimum work in the ISR and delegate, or "defer," the longer processing to a regular RTOS task.

FreeRTOS Kernel Book: Deferred Interrupt Processing

To understand the rationale behind this pattern, please read the following section from the official FreeRTOS book. It clearly explains why keeping ISRs short is considered best practice.

Read section 7.3, titled 'Deferred Interrupt Processing'. Pay close attention to the list of reasons for keeping ISRs short and the explanation of Figure 7.1.


2. The Deferred Interrupt Handling Pattern

The pattern is a clean division of labor between the ISR and a dedicated "handler" task.

Deferred Interrupt Handling with Semaphore in RTOS
This timeline illustrates the deferred interrupt handling pattern. The ISR does the minimal work and then unblocks a high-priority handler task, which completes the processing. This keeps the time spent in the interrupt context (t2-t3) extremely short.

The sequence of events is as follows:

  1. t1: A normal, lower-priority task (Task 1) is running.
  2. Interrupt Occurs: A hardware event triggers an interrupt. The CPU immediately saves its context and jumps to the ISR.
  3. t2: ISR Execution: The ISR performs only time-critical actions, such as reading a value from a peripheral register and acknowledging the interrupt. Its main job is to signal a waiting handler task that an event has occurred.
  4. t3: ISR Completes: The ISR finishes and must tell the RTOS scheduler that a higher-priority task is now ready. The scheduler sees the unblocked 'Handler Task' has a higher priority than 'Task 1'.
  5. t4: Context Switch: The scheduler preempts 'Task 1' and immediately switches context to the 'Handler Task'.
  6. Handler Task Execution: The 'Handler Task' runs and performs the lengthy processing (e.g., filtering data, sending a network packet, updating a display).
  7. Handler Task Blocks: Once finished, the handler task goes back to a blocked state, waiting for the next signal from the ISR. The scheduler then resumes 'Task 1' or another ready task.

3. Implementation: Signaling from an ISR

The core of this pattern is the signaling mechanism. An ISR cannot simply call vTaskResume(). It must use special, interrupt-safe RTOS functions.

First, let's cover the essential mechanics of how an ISR triggers a context switch in FreeRTOS.

FreeRTOS Kernel Book: Using the FreeRTOS API from an ISR

This section of the FreeRTOS book introduces the three key concepts for ISR-to-task communication: interrupt-safe API functions (ending in 'FromISR'), the pxHigherPriorityTaskWoken parameter, and the macro used to request a context switch.

Read section 7.2, focusing on subsections 7.2.1 ('The Interrupt Safe API'), 7.2.4 ('The xHigherPriorityTaskWoken Parameter'), and 7.2.5 ('The portYIELD_FROM_ISR() ... Macros').

With that foundation, let's look at two primary methods for signaling.

Method 1: The Classic Binary Semaphore

The traditional method for synchronizing a task with an ISR is a binary semaphore.

  • The handler task calls xSemaphoreTake(), blocking indefinitely until the semaphore becomes available.
  • The ISR calls xSemaphoreGiveFromISR() to make the semaphore available, unblocking the task.

Introduction to RTOS Part 9 - Hardware Interrupts | Digi-Key Electronics

This video provides an excellent walkthrough of this exact pattern, showing a time chart and a practical demo using a binary semaphore.

Watch from 07:57 to 12:03. This section introduces the concept with a time chart, explains the use of fromISR functions and the yield call, and provides a full code demo.

The code in the ISR would look something like this:

void GPIO_IRQ_Handler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 1. Minimal work: Clear the interrupt flag
    clear_interrupt_flag();

    // 2. Signal the handler task
    xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);

    // 3. If a higher-priority task was unblocked, yield to it.
    // This must be the LAST thing done in the ISR.
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Method 2: The Modern Task Notification

FreeRTOS offers a lighter, faster, and more memory-efficient mechanism specifically for this purpose: Task Notifications. A notification is sent directly from the ISR to the task without needing an intermediate object like a semaphore.

  • The handler task calls ulTaskNotifyTake(), which blocks until it receives a notification.
  • The ISR calls vTaskNotifyGiveFromISR() to send a notification directly to the handler task, unblocking it.

This approach is highly favored in modern embedded C design and is a great topic to bring up in an interview to show you know more than just the classic patterns.

FreeRTOS Kernel Book: Task Notifications

This chapter from the FreeRTOS book is dedicated to Task Notifications. We will focus on how they are used as a direct replacement for semaphores in deferred interrupt handling.

First, read section 10.3.3 ('The vTaskNotifyGiveFromISR() API Function'). Then, read 'Example 10.1' (section text, Listing 10.4 and 10.5), which directly shows how to replace the semaphore pattern with a task notification.

The ISR code is very similar, just with a different API call:

void GPIO_IRQ_Handler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 1. Minimal work: Clear the interrupt flag
    clear_interrupt_flag();

    // 2. Signal the handler task directly
    vTaskNotifyGiveFromISR(xHandlerTaskHandle, &xHigherPriorityTaskWoken);

    // 3. If a higher-priority task was unblocked, yield to it.
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

4. Practical Implementation & Comparison

Let's solidify this with a complete, practical code example. The following code implements a simple system where a low-priority "periodic task" simulates a hardware event every second. The "ISR" catches this event and signals a high-priority handler task.

You can switch between the semaphore and task notification implementations by changing the USE_TASK_NOTIFICATIONS macro. This allows you to directly compare the two approaches.




#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

// Set to 1 to use Task Notifications, 0 to use Binary Semaphores



#define USE_TASK_NOTIFICATIONS    1

/* --- Task and ISR Prototypes --- */
void vHandlerTask(void *pvParameters);
void vPeriodicTask(void *pvParameters);
void simulated_isr(void);

/* --- Handles and Globals --- */
static TaskHandle_t xHandlerTaskHandle = NULL;



#if !USE_TASK_NOTIFICATIONS
static SemaphoreHandle_t xBinarySemaphore = NULL;



#endif

/* The high-priority task that performs the deferred processing. */
void vHandlerTask(void *pvParameters)
{
    for (;;)
    {



        #if USE_TASK_NOTIFICATIONS
            /* Wait for a notification. ulTaskNotifyTake acts like a binary semaphore
               when xClearCountOnExit is pdTRUE. */
            uint32_t ulNotifiedValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

            if (ulNotifiedValue > 0)
            {
                printf("Handler Task: Received event notification. Processing data...\n");
                // Simulate doing work
                vTaskDelay(pdMS_TO_TICKS(100));
            }



        #else
            /* Wait for the binary semaphore to be given by the ISR. */
            if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS)
            {
                printf("Handler Task: Received event semaphore. Processing data...\n");
                // Simulate doing work
                vTaskDelay(pdMS_TO_TICKS(100));
            }



        #endif
    }
}

/* A low-priority task that periodically simulates a hardware event. */
void vPeriodicTask(void *pvParameters)
{
    for (;;)
    {
        /* Block for 1 second. */
        vTaskDelay(pdMS_TO_TICKS(1000));
        
        printf("\nPeriodic Task: Simulating a hardware interrupt.\n");
        simulated_isr();
    }
}

/* This function simulates a hardware Interrupt Service Routine. */
void simulated_isr(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* In a real ISR, you would read hardware and clear the interrupt flag here. */




    #if USE_TASK_NOTIFICATIONS
        /* Give the notification directly to the handler task. */
        vTaskNotifyGiveFromISR(xHandlerTaskHandle, &xHigherPriorityTaskWoken);



    #else
        /* Give the semaphore to unblock the handler task. */
        xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);



    #endif
    
    /* If xHigherPriorityTaskWoken is now pdTRUE, then a context switch
       should be performed to ensure the interrupt returns to the
       highest priority task that is ready to run. */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}


int main(void)
{



    #if !USE_TASK_NOTIFICATIONS
        /* Create the binary semaphore before using it. */
        xBinarySemaphore = xSemaphoreCreateBinary();
        configASSERT(xBinarySemaphore != NULL);



    #endif

    /* Create the high-priority handler task. We save its handle for Task Notifications. */
    xTaskCreate(vHandlerTask, "Handler", 1000, NULL, 3, &xHandlerTaskHandle);

    /* Create the low-priority periodic task. */
    xTaskCreate(vPeriodicTask, "Periodic", 1000, NULL, 1, NULL);

    printf("System starting...\n");
    vTaskStartScheduler();

    /* Should never reach here. */
    for (;;);
    return 0;
}

Notice how clean the task notification approach is. You don't need to create, manage, or pass around a handle for the semaphore. This reduces RAM usage and simplifies the code, which is always a win in embedded systems.


5. The Bigger Picture: A Universal Pattern

This pattern of splitting interrupt handling into a "fast" part and a "slow" part is universal. While we've focused on an RTOS like FreeRTOS, the exact same principle applies in the Linux kernel. There, it's known as Top Halves and Bottom Halves.

  • Top Half: The fast ISR, analogous to our simulated_isr.
  • Bottom Half: The deferred work, handled by mechanisms like softirqs, tasklets, and workqueues, which are analogous to our vHandlerTask.

We will explore the Linux kernel implementation in Module 8, but recognizing this as a fundamental system design pattern is key for interview success.


Conclusion

You have now learned one of the most important design patterns in real-time embedded systems. Deferring interrupt processing is essential for building responsive, stable, and predictable applications.

Key Takeaways:

  • ISRs must be short and fast to avoid blocking the system and introducing scheduler jitter.
  • Deferred Interrupt Handling is the pattern where an ISR does minimal work and signals a dedicated, high-priority task to perform the longer processing.
  • In FreeRTOS, this signaling is done with interrupt-safe functions (...FromISR).
  • The binary semaphore is the classic implementation mechanism.
  • Task Notifications are a more modern, lightweight, and efficient alternative for ISR-to-task signaling in FreeRTOS.
  • Requesting a context switch from the ISR via portYIELD_FROM_ISR() is critical to ensure the handler task runs immediately.

Preview of the Next Lesson:

Now that we have established patterns for both task-to-task and ISR-to-task synchronization, we will dive into a classic concurrency problem that uses these primitives: the Producer-Consumer problem. We will implement a solution using a shared buffer protected by semaphores, a common scenario in data streaming and processing applications.