Home FreeRTOS Interrupts in a FreeRTOS System: Beginner’s Guide

Interrupts in a FreeRTOS System: Beginner’s Guide

by shedboy71

Interrupts in a FreeRTOS System

Introduction

Interrupts are a core part of embedded programming, and they remain just as important when you use FreeRTOS. In fact, once an RTOS enters the picture, interrupts become even more interesting because they now interact with tasks, scheduling, timing, and inter-task communication. A task may wait for data, a queue may receive an item from an interrupt, and the scheduler may switch tasks immediately after an interrupt finishes if a higher-priority task has just become ready.

This is where many beginners become confused. They understand interrupts in a bare-metal system as fast reactions to hardware events, but they are not always sure what changes once FreeRTOS is involved. The answer is that the basic role of an interrupt stays the same, but the way you design the interrupt handler often changes. In a bare-metal program, an interrupt might do more work directly. In a FreeRTOS system, the preferred design is usually to keep the interrupt short, capture the important event quickly, and hand the heavier work to a task.

That design style is one of the most important concepts in RTOS-based embedded development. It helps the system stay responsive, predictable, and easier to maintain.

This article explains what interrupts do in a FreeRTOS system, how they relate to tasks, what “ISR-safe” APIs mean, how task switching can happen after an interrupt, and what common mistakes to avoid.

What an interrupt still means in FreeRTOS

An interrupt is still a hardware- or peripheral-driven event that temporarily pauses normal execution so the processor can respond quickly. A GPIO pin may change state. A timer may expire. A UART may receive a byte. An ADC may finish a conversion. In each case, the interrupt service routine, or ISR, runs in response.

That basic idea does not change because FreeRTOS is present.

What does change is the software environment around it. Instead of a single superloop being interrupted, you now have a scheduler managing multiple tasks. So an interrupt no longer just interrupts “the program.” It interrupts whichever task or system code is currently running.

That means an ISR in a FreeRTOS system exists alongside tasks rather than replacing them. It is part of the overall system behavior, not the whole design by itself.

Tasks and interrupts have different roles

A useful way to think about a FreeRTOS system is that tasks and interrupts do different jobs.

Tasks are for planned ongoing work. They can wait, block, process data, communicate through queues, and run for longer periods when needed.

Interrupts are for urgent, immediate reaction to events. They should notice that something happened, capture the critical detail, and get out quickly.

This division of responsibility is extremely important.

A task can:

  • wait on a queue
  • delay for a period of time
  • take a semaphore
  • do larger amounts of computation
  • manage communication and application logic

An ISR should usually:

  • acknowledge the hardware event
  • read or write the minimum required data
  • signal a task if more work is needed
  • finish quickly

If you remember only one design rule from this article, it should be this:

In FreeRTOS, interrupts should usually do the minimum possible and let tasks do the rest.

Why ISRs should be short in an RTOS system

Beginners often ask why the ISR should not just do everything directly. The answer is that an interrupt runs outside the normal task scheduling model. While the ISR is running, ordinary task execution is paused. If the ISR takes too long, it can delay the entire system.

A long ISR can cause problems such as:

  • reduced responsiveness for other interrupts
  • delayed task execution
  • timing jitter
  • harder debugging
  • poor overall system behavior

This is true in bare-metal systems too, but in FreeRTOS it matters even more because the RTOS is trying to manage multiple activities fairly and predictably.

A short ISR keeps the urgent part urgent and leaves the rest to the scheduler and tasks, which is exactly what the RTOS is good at managing.

The preferred design pattern

The most common good pattern in FreeRTOS is:

  1. An interrupt occurs.
  2. The ISR captures the event or minimal data.
  3. The ISR signals a task.
  4. The task wakes up and handles the larger job.

For example, imagine a UART receive interrupt. The ISR might place the received byte into a queue or buffer and notify a task. The task then does the more complex processing, such as command parsing or message handling.

This pattern is better than doing full parsing inside the ISR because it keeps interrupt latency lower and makes the design easier to scale.

A simple GPIO interrupt example

Suppose a button connected to a GPIO pin generates an interrupt when pressed. A very basic ISR might just set a flag:

volatile BaseType_t buttonPressed = pdFALSE;

void EXTI_Button_IRQHandler(void)
{
    buttonPressed = pdTRUE;
    clear_button_interrupt_flag();
}

A task can then monitor that condition:

void ButtonTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        if (buttonPressed == pdTRUE)
        {
            buttonPressed = pdFALSE;
            handle_button_press();
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

This is simple and easy to understand, but it is not yet the most FreeRTOS-friendly pattern because the task is polling the flag. A better design is often to use a queue, semaphore, or task notification so the task can block until the interrupt signals it.

From ISR to task using a queue

A more RTOS-oriented design might use a queue. The ISR sends an item to the queue, and a task blocks waiting for it.

Example:

QueueHandle_t buttonQueue;

void EXTI_Button_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t event = 1;

    xQueueSendFromISR(buttonQueue, &event, &xHigherPriorityTaskWoken);
    clear_button_interrupt_flag();

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Task side:

void ButtonTask(void *pvParameters)
{
    uint8_t event;

    (void) pvParameters;

    for (;;)
    {
        if (xQueueReceive(buttonQueue, &event, portMAX_DELAY) == pdTRUE)
        {
            handle_button_press();
        }
    }
}

This is a very important FreeRTOS pattern.

The task blocks when there is nothing to do.

The ISR signals the queue when the event occurs.

The task wakes and handles the real work.

This is often much cleaner than polling a flag.

Why ISR-safe APIs are different

A beginner may notice functions like:

  • xQueueSend()
  • xQueueSendFromISR()

and wonder why there are two versions.

The reason is that code running inside an ISR has different rules from code running inside a task. A normal task API may assume it can block, manipulate scheduler state in a certain way, or behave in ways that are not safe from interrupt context.

So FreeRTOS provides special ISR-safe APIs for use inside interrupt handlers.

Examples include:

  • xQueueSendFromISR()
  • xQueueReceiveFromISR()
  • xSemaphoreGiveFromISR()
  • vTaskNotifyGiveFromISR()
  • xTaskNotifyFromISR()

These functions are designed for interrupt context.

A very important rule is this:

Do not call ordinary task-context FreeRTOS APIs from an ISR unless they are explicitly meant to be ISR-safe.

Using the wrong API in an interrupt is one of the most common and most serious beginner mistakes.

What xHigherPriorityTaskWoken means

Many ISR-safe FreeRTOS functions use a variable like this:

BaseType_t xHigherPriorityTaskWoken = pdFALSE;

This variable allows the ISR-safe function to tell you whether the action you just performed has made a higher-priority task ready to run.

For example, imagine a high-priority task is blocked waiting on a queue. The ISR sends an item into that queue. That task is now ready.

If that newly ready task has a higher priority than the currently running task, the system may want to switch to it as soon as the interrupt finishes.

That is exactly what xHigherPriorityTaskWoken helps communicate.

It is often used like this:

portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

This gives the system a chance to perform a context switch immediately after the ISR if needed.

That is one of the key ways interrupts and task scheduling connect in FreeRTOS.

Interrupts can wake higher-priority tasks immediately

This is one of the most powerful aspects of interrupts in an RTOS system.

Imagine a task is blocked waiting for incoming serial data. The UART receive interrupt fires. The ISR places the data in a queue and wakes the waiting task. If that task has a higher priority than whatever was running before the interrupt, the system can switch directly to it.

So the flow can look like this:

  1. Low-priority task is running.
  2. UART interrupt occurs.
  3. ISR sends data to a queue.
  4. High-priority communication task becomes ready.
  5. ISR finishes.
  6. Scheduler switches to the high-priority task immediately.

This is a very important part of responsive FreeRTOS systems. It lets urgent work happen quickly without forcing the ISR itself to do too much.

Interrupt priority matters on ARM Cortex-M

On ARM Cortex-M systems, interrupt priority configuration is especially important in FreeRTOS projects.

This is an area where many beginners run into trouble because there are two different worlds of priority:

  • task priorities
  • hardware interrupt priorities

These are not the same thing.

Task priorities determine which task runs when multiple tasks are ready.

Interrupt priorities determine which hardware interrupts can preempt others.

In FreeRTOS on Cortex-M, some interrupt priorities are too high to safely call certain FreeRTOS ISR APIs. That means you cannot simply assign any interrupt priority you want and then call queue or semaphore APIs from that ISR.

This is one of the most common sources of confusion and bugs in ARM-based FreeRTOS systems.

A good beginner rule is:

Be very careful with interrupt priority setup, especially if the ISR uses FreeRTOS APIs.

Deferred interrupt processing

A very useful phrase in RTOS design is deferred interrupt processing.

This means the interrupt handles only the urgent minimum, and the bulk of the work is deferred to a task.

For example, a data-ready interrupt from a sensor might do this:

  • clear the interrupt
  • record that data is ready
  • notify a task

Then the task does:

  • sensor read
  • data conversion
  • filtering
  • communication
  • logging

This is better than doing all of that in the ISR.

Deferred interrupt processing is one of the most important practical patterns in FreeRTOS.

Example: UART receive with deferred processing

Imagine a UART interrupt receives bytes one by one.

A poor design might try to parse full commands, validate messages, and respond directly in the ISR.

A better design is:

ISR:

  • read the received byte
  • place it into a queue or ring buffer
  • notify a task

Task:

  • collect bytes into a message
  • parse commands
  • perform the required actions
  • send responses if needed

This design keeps the ISR fast and lets the task handle more complex logic safely.

Example: timer interrupt and task wake-up

A periodic hardware timer can also work with FreeRTOS tasks.

For example, a timer ISR might simply notify a task that it is time to do periodic work:

TaskHandle_t sampleTaskHandle = NULL;

void TIM_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    clear_timer_interrupt_flag();
    vTaskNotifyGiveFromISR(sampleTaskHandle, &xHigherPriorityTaskWoken);

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

The task then waits for that notification:

void SampleTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        sample_sensor();
    }
}

This is a clean and efficient pattern. The timer interrupt provides precise timing, while the task performs the real work.

Interrupts and semaphores

Semaphores are also commonly used between ISRs and tasks.

For example, an ISR can give a binary semaphore to signal that an event happened. A task can block waiting for that semaphore.

ISR:

SemaphoreHandle_t dataReadySemaphore;

void ADC_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    clear_adc_interrupt_flag();
    xSemaphoreGiveFromISR(dataReadySemaphore, &xHigherPriorityTaskWoken);

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Task:

void ProcessingTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        if (xSemaphoreTake(dataReadySemaphore, portMAX_DELAY) == pdTRUE)
        {
            process_adc_result();
        }
    }
}

This is another common interrupt-to-task communication pattern.

Task notifications are often lighter than queues

When an interrupt only needs to wake a specific task or send a simple signal, a task notification is often a very efficient choice.

A queue is useful when actual data items must be transferred.

A semaphore is useful when you want signaling behavior.

A task notification is useful when one task is the intended target and the communication can be represented in a lightweight way.

For many simple ISR-to-task wake-up patterns, task notifications are an excellent choice.

What not to do inside an ISR

There are several things you should generally avoid inside an ISR in a FreeRTOS system.

Do not call blocking FreeRTOS APIs.

Do not use the non-ISR versions of queue, semaphore, or notification functions.

Do not do long loops or heavy processing.

Do not perform complex formatting or large string work.

Do not call delay functions.

Do not treat the ISR like a mini-task.

Do not forget to clear the hardware interrupt condition if required by the peripheral.

The more an ISR behaves like a quick event capture rather than a full processing engine, the healthier the design usually is.

Shared data between ISR and tasks

Sometimes an ISR and a task both access the same variable or data structure. This requires care.

At minimum, shared simple flags are often marked volatile.

Example:

volatile uint32_t pulseCount = 0;

But volatile alone does not solve every problem. If a variable can be updated in one context while being read or modified in another, you may need critical sections or a safer design pattern.

This is one reason queues, notifications, and semaphores are so helpful. They often reduce the amount of fragile shared-state handling you need to write manually.

A good beginner principle is:

Prefer structured ISR-to-task communication over casual shared variables whenever possible.

Critical sections and interrupts

FreeRTOS also uses critical sections to protect some operations. A critical section is a region where interrupts are masked or controlled so that important shared state cannot be corrupted.

This matters because:

  • tasks may protect shared resources
  • the kernel itself must protect internal state
  • ISR and task interactions may depend on correct interrupt masking behavior

Beginners do not need to start by writing lots of manual critical sections, but they should understand that the RTOS and application sometimes need protected regions to manage concurrency safely.

Interrupt nesting and responsiveness

On ARM systems, interrupts can often preempt one another depending on their priorities. That means while one interrupt is running, a higher-priority interrupt may interrupt it.

This is another reason to keep ISRs short. Short handlers reduce latency and make nested interrupt behavior less painful.

In a FreeRTOS system, good interrupt design is part of keeping the whole system responsive, not just the one peripheral you are working on.

Common beginner mistakes

One common mistake is calling the wrong FreeRTOS API from an ISR.

Another is doing too much work in the ISR instead of deferring it to a task.

Another is misunderstanding the difference between task priorities and interrupt priorities.

Another is forgetting to request a context switch after waking a higher-priority task from an ISR.

Another is using shared variables carelessly between tasks and interrupts.

Another is forgetting that the hardware interrupt source often needs to be acknowledged or cleared.

Another is giving an interrupt a priority that is incompatible with the FreeRTOS APIs it uses.

These mistakes are common because interrupts are where hardware behavior and RTOS behavior meet. That boundary is powerful, but it must be handled carefully.

 

Conclusion

Interrupts in a FreeRTOS system still serve their classic embedded role: they respond quickly to hardware events. But in an RTOS-based design, they work best when paired with tasks. The ISR handles the urgent minimum, and the task handles the larger job. That division keeps the system responsive, cleaner, and easier to scale.

The most important ideas are straightforward. Keep ISRs short. Use the ISR-safe FreeRTOS APIs. Understand that an interrupt can wake a higher-priority task and cause an immediate context switch after the ISR finishes. Be careful with interrupt priorities on ARM systems. Prefer deferred interrupt processing over heavy ISR logic.

 

Share

You may also like