Feb 19 / Wadix Technologies

Task Context vs Interrupt Context in FreeRTOS: Why FromISR APIs Exist

Task Context vs Interrupt Context in FreeRTOS: Why FromISR APIs Exist


1.Introduction

In real embedded products, interrupts rarely exist just to flip a bit or increment a counter. They usually signal work to the rest of the system. A UART interrupt indicates that new data has arrived. A DMA interrupt signals that a transfer has completed. A timer interrupt marks the expiration of a deadline. In all of these cases, the interrupt itself is not where the application logic should live. Its role is to notify the appropriate task so that the work can be handled in normal task context.

FreeRTOS is built around this model. Tasks perform the bulk of the application work, and interrupts are used to wake the right task at the right time. To make this practical, the kernel provides APIs for sending data to queues, giving semaphores, and notifying tasks. At first glance, it is tempting to treat interrupts as just another place where these APIs can be called. In practice, that assumption breaks down.

FreeRTOS therefore provides a parallel set of functions whose names end in FromISR. These functions exist because interrupt context is fundamentally different from task context. To use them correctly, it helps to understand what problem they solve and how they fit into the FreeRTOS scheduling model.


2. Two execution contexts in a FreeRTOS system

A FreeRTOS-based system executes code in two very different contexts.

The first is task context. Task code runs under the control of the scheduler. A task can block waiting for a queue, a semaphore, or a notification. It can be preempted by a higher-priority task. From the kernel’s point of view, there is always a “current task” whose state can be changed, suspended, or moved between ready and blocked lists.

The second is interrupt context. An interrupt runs asynchronously to the scheduler. It is not a task, it does not have a stack managed by the RTOS, and it cannot be put to sleep or switched out in the same way a task can. An interrupt handler runs to completion and then returns control back to whatever was running before.

This distinction is not just conceptual. Many RTOS services rely on the assumptions of task context: that there is a current task, that the caller may block, and that the scheduler can make immediate decisions about which task should run next. None of these assumptions hold inside an ISR.

Fig 1. Task context and interrupt context in a FreeRTOS system.


3. Why task-level APIs do not apply to ISRs

The standard FreeRTOS APIs for queues, semaphores, and other synchronization objects are designed around task behavior. They assume a schedulable caller that can be blocked, placed on a wait list, and later resumed by the scheduler. When a task sends to a full queue, it may sleep until space becomes available. When it waits for a semaphore, it may be moved to a blocked state. When it unblocks another task, the scheduler may decide to switch execution immediately.

None of these assumptions hold in interrupt context. An ISR is not a schedulable entity. It cannot block, it cannot be placed on a wait list, and it does not have a task control block that the scheduler can manipulate. An interrupt handler runs to completion and then returns control to the code that was interrupted. There is no meaningful way for the kernel to “switch away” from an ISR in the same sense that it switches between tasks.

There is also a lower-level, practical reason for this separation. The kernel protects its internal data structures using critical sections whose rules depend on whether the kernel is entered from task context or from interrupt context. In an ISR, the kernel must respect the current interrupt priority and follow different, carefully bounded paths when updating its state. The normal APIs are written assuming task-context entry and rely on protection mechanisms that do not apply in interrupt context.

For these reasons, calling task-level RTOS APIs from an ISR is not just a matter of style or convention. Those APIs are built on assumptions about blocking, scheduling, and kernel entry rules that do not hold in interrupt context. This is why FreeRTOS provides a separate set of FromISR functions: they offer a well-defined and safe way for interrupts to interact with the kernel without violating those assumptions.


4. What the FromISR APIs are

To bridge this gap, FreeRTOS provides a set of APIs specifically designed for use in interrupt context. These are the functions that end with FromISR. Conceptually, they perform the same high-level operations as their task-level counterparts: they can send data to a queue, give a semaphore, or notify a task. The difference lies in how they interact with the kernel.

FromISR APIs are written with three key constraints in mind. They never block, they use interrupt-safe kernel paths, and they report whether a higher-priority task was unblocked by the operation.

They also do not replace queues, semaphores, or notifications. The data and synchronization objects remain the same. The FromISR APIs only define a safe way for an interrupt to signal the kernel.

Fig 2. Normal RTOS APIs assume task context, while FromISR APIs provide a safe path for interrupts .


5. A concrete example: xQueueSend() vs xQueueSendFromISR()

The difference between task-level APIs and their FromISR counterparts becomes clearer when looking at a concrete pair of functions. Consider the common case of sending data to a queue from normal task code versus from an interrupt handler.

When a task calls xQueueSend(), the kernel is free to apply the full set of task-level semantics. If the queue is full, the calling task can block until space becomes available or until a specified timeout expires. While the task is blocked, the kernel may move it to a wait list associated with the queue, select another ready task to run, and later resume the original task when space becomes available. This behavior relies on the fact that the caller is a schedulable entity managed by the kernel.

In interrupt context, none of this is possible. An ISR cannot block, cannot be placed on a wait list, and cannot yield the processor in the same way a task can. For this reason, xQueueSendFromISR() is defined with different rules. It attempts to place the item into the queue, but it never waits. If the queue is full at the time of the call, the function simply reports failure. There is no concept of delaying or retrying inside the interrupt handler.

There is also an important difference in how scheduling is handled. When a task calls xQueueSend(), the kernel may decide to perform a context switch immediately if the send operation unblocks a higher-priority task that is waiting on the queue. In contrast, when xQueueSendFromISR() unblocks a higher-priority task, the context switch cannot happen immediately. The ISR must run to completion first, and the kernel performs the actual context switch after the interrupt exits.

From the point of view of the queue, nothing special is happening. The same queue object is used, and the same data structures are updated. The two functions exist not because the operation itself is different, but because the kernel must follow different rules depending on the execution context.


6. Waking a task and rescheduling

A common pattern in FreeRTOS systems is that an interrupt makes some data or event available and then wakes a task that is waiting for it. When this happens, there is an important timing question: should the newly woken task run immediately, or should the system continue running whatever was interrupted?

FreeRTOS separates these concerns. A FromISR API updates the kernel state and records whether a higher-priority task has been unblocked. The actual context switch does not happen in the middle of the ISR. Instead, it is deferred until the ISR finishes and control returns to the kernel at a safe point. This is what “yield from ISR” means in practice: the interrupt requests a reschedule, and the scheduler performs it after the interrupt completes.

This mechanism becomes clearer in a concrete example. Consider a simple communication driver that receives data using an interrupt. Each time a byte arrives, the hardware triggers an ISR. The application does not want to process protocol logic inside the interrupt. That work belongs in a task.

A typical design is for the ISR to place the received data into a queue and then signal a communication task that new data is available. The task wakes up, drains the queue, and performs the necessary processing. The ISR’s role is limited to making data available and notifying the task. The notification is done using a FromISR API, and the task then performs a normal receive operation in task context using the standard FreeRTOS APIs.

Conceptually, the code looks like this:

/* Queue used to pass received bytes to the task */

static QueueHandle_t qRx;



/* Communication task */

void CommsTask(void *arg)

{

  uint8_t byte;



  for (;;)

  {

    /* Block until a byte is available */

    if (xQueueReceive(qRx, &byte, portMAX_DELAY) == pdPASS)

    {

      ProcessReceivedByte(byte);

     }

   }

}



/* UART receive ISR */

void UART_IRQHandler(void)

{

  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  uint8_t byte = ReadUartDataRegister();


  /* Send the byte to the queue from interrupt context */

  xQueueSendFromISR(qRx, &byte, &xHigherPriorityTaskWoken);



 /* Request a context switch if a higher-priority task was unblocked */

  portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

}

When the interrupt fires, the ISR copies the received byte into the queue using xQueueSendFromISR(). If the communication task was blocked waiting for data, this operation may unblock it. The ISR does not switch context immediately. Instead, it records whether a higher-priority task was woken and then returns. When the interrupt exits, the kernel checks this condition and, if necessary, switches to the newly unblocked task.

This keeps the interrupt handler short and bounded in execution time, while still allowing the communication task to run as soon as possible when new data arrives.

Fig 3. An ISR places data into a queue using a FromISR API. If a higher-priority task is unblocked, the context switch is performed after the ISR completes.


7. Conclusion

FromISR APIs exist because interrupt context and task context serve different roles in a FreeRTOS system. Tasks are schedulable entities that can block and wait under the control of the kernel. Interrupts are asynchronous events that must run quickly and cannot participate in that scheduling model.

In addition, the kernel must follow different rules when it is entered from an interrupt, particularly with respect to critical sections and interrupt priorities. The FromISR APIs provide a safe and well-defined entry point that respects these constraints.

The result is a system where interrupts remain simple and bounded, and tasks remain the place where application logic and control flow are expressed.

Created with