
EEWorld
Electronic News Sharp Interpretation
Technical Insights Updated Daily

With FreeRTOS providing memory resources for tasks through stack management mechanisms, it can perform context switching based on task status and priority, and offers communication channels for necessary task synchronization and mutual exclusion, multiple tasks can work together. However, since it is called a Real-Time Operating System, it must also quickly respond to external (hardware) events. Especially for applications on microcontrollers, after a hardware interrupt (IRQ) is generated, it is essential for the operating system to wake up a task immediately to handle this event. From the task’s perspective, many tasks need to be scheduled based on hardware events (such as transfer completion, device readiness, data reception, etc.), otherwise, they will continuously poll the hardware device status register flags, wasting CPU time.
The time-slicing management in FreeRTOS actually utilizes timer interrupts. Otherwise, it would be impossible for a task to be interrupted and execute other tasks of the same priority without scheduling requests. Similarly, whenever a hardware interrupt occurs, the corresponding Interrupt Service Routine (ISR) is executed. After the ISR completes, whether to return to the current task or schedule and execute other tasks is entirely determined by the ISR.
1. ISRs Are Independent of All TasksAlthough it may seem that the ISR, or Interrupt Service Routine, serves the function of a specific task, it is crucial to emphasize that ISR code is not part of any FreeRTOS task code. Each ISR is a C language function, but it is not a task and will not be called by any task.
ISRs use the stack differently than tasks. As previously mentioned, FreeRTOS allocates independent stack space for each task to save local variables and so on. When an interrupt occurs, certain CPU registers are saved onto the current stack (not a designated task’s stack) and then the ISR is executed. If the current code being executed is that of a task, it will occupy that task’s stack; if the code of another ISR is currently executing (interrupt nesting), it may continue using the stack of the earlier interrupted task (note: this varies by platform. For ARM Cortex-M series platforms, FreeRTOS allows tasks to run in thread mode, using PSP as the stack pointer, while ISRs switch to handler mode, using MSP as the stack pointer, thus all ISRs share a single stack).
ISRs can execute independently of the FreeRTOS kernel. As long as FreeRTOS APIs are not used within the ISR, FreeRTOS will not be aware of the interrupt occurrence, because it can save and restore the context regardless of where the current stack is. Similarly, the execution of the ISR itself does not lead to any task switching. When integrating FreeRTOS code into an existing project, existing ISRs can operate without modification.ISRs do not change the state of the current task. Even though the currently running task is paused after the IRQ occurs and the CPU executes the ISR code, the state of the current task remains Running and does not change to another state—this is distinctly different from task preemption. Even if FreeRTOS APIs are called within the ISR, waking up other tasks with higher priority (changing to Ready state), task switching will only occur after the ISR returns, when the scheduler will select the running task again. In fact, the ISR does not know what the currently running task is, and it is meaningless to actively change the state of the current task.
2. Critical Section ConceptEarlier, when analyzing FreeRTOS implementation details, I encountered the calls taskENTER_CRITICAL() and taskEXIT_CRITICAL() multiple times. From the name, it suggests that a critical operation is being performed that should not be interrupted, such as accessing the task state list. If not handled this way, there is a risk that the data being accessed may be rewritten midway or that data modification may be incomplete and accessed by other tasks or the FreeRTOS kernel, leading to erroneous results. Therefore, a segment of code is defined as a critical section, protected by taskENTER_CRITICAL() and taskEXIT_CRITICAL(), prohibiting task scheduling and access to FreeRTOS core data by other interrupt ISRs.
After this handling, the segment of code is temporarily given a high priority, regardless of the current task’s priority. One might guess that interrupt masking can be done first, then allowed afterward, but it is not that simple. Let’s see how FreeRTOS defines these two operations.In the task.h header file, these two macros are defined as follows:#define taskENTER_CRITICAL() portENTER_CRITICAL()#define taskEXIT_CRITICAL() portEXIT_CRITICAL()Next, in the (CM3 platform) portmacro.h file, they are defined as:#define portENTER_CRITICAL() vPortEnterCritical()#define portEXIT_CRITICAL() vPortExitCritical()
In the port.c file, the implementations of vPortEnterCritical() and vPortExitCritical() functions are found:
-
void vPortEnterCritical( void )
-
{
-
portDISABLE_INTERRUPTS();
-
uxCriticalNesting++;
-
if( uxCriticalNesting == 1 )
-
{
-
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
-
}
-
}
-
void vPortExitCritical( void )
-
{
-
configASSERT( uxCriticalNesting );
-
uxCriticalNesting–;
-
if( uxCriticalNesting == 0 )
-
{
-
portENABLE_INTERRUPTS();
-
}
-
}
This adds a bit more operation than simply masking interrupts: it uses a counting variable. The configASSERT() code can be removed, so it is not a concern. Why count? The answer is for nested calls; after how many times vPortEnterCritical() is called, the same number of vPortExitCritical() calls are required to allow interrupts. Looking at how the interrupt masking operation is performed on the Cortex-M3 platform:#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)Carefully examining the assembly code implementation of the function:
-
portFORCE_INLINE static void vPortRaiseBASEPRI( void )
-
{
-
uint32_t ulNewBASEPRI;
-
__asm volatile
-
(
-
” mov %0, %1 undefined” \
-
” msr basepri, %0 undefined” \
-
” isb undefined” \
-
” dsb undefined” \
-
:”=r” (ulNewBASEPRI) : “i” ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
-
);
-
}
This operation modifies the BASEPRI register, masking certain hardware interrupts: those with priority equal to or lower than configMAX_SYSCALL_INTERRUPT_PRIORITY. Why only some are masked? Because if a certain interrupt ISR does not access FreeRTOS core data and does not call any FreeRTOS API, its interruption is harmless. However, partial interrupt masking requires hardware support; for example, on the ARM Cortex-M0 platform, there is no BASEPRI register, and the corresponding implementation code is simplified:#define portDISABLE_INTERRUPTS() __asm volatile ( ” cpsid i ” )#define portENABLE_INTERRUPTS() __asm volatile ( ” cpsie i ” )ISRs can also have critical sections, but they require the calls taskENTER_CRITICAL_FROM_ISR() and taskEXIT_CRITICAL_FROM_ISR(), with different parameters and return values, needing to save and restore the current interrupt level status. On the Cortex-M3 platform, this corresponds to saving and restoring the BASEPRI register.
The value of configMAX_SYSCALL_INTERRUPT_PRIORITY means that only ISRs with a priority not exceeding this can call FreeRTOS APIs, which is why they must be masked when entering a critical section. As for whether a higher interrupt priority corresponds to a larger or smaller value, it depends on the hardware platform. It is essential not to confuse interrupt priority (a hardware concept) with FreeRTOS task priority.3. FreeRTOS API Functions Usable in ISRsFreeRTOS documentation emphasizes that in ISRs, only API functions ending with FromISR should be called, and not regular APIs. This is because the execution environment of an ISR differs from that of a task; aside from efficiency considerations, some APIs must differentiate.ISRs must call APIs that return quickly and cannot wait. The system does not allow interrupt handling to take too long and cannot wait for other interrupts to occur. Some APIs, due to their blocking functionality, cannot be used within ISRs; they either change functionality or include parameter passing requirements.Task scheduling within ISRs is optional. For instance, operations on communication objects may wake up other tasks with higher priority than the current task; if done within a task, it would immediately trigger a task switch. However, within an ISR, it may not be necessary to switch tasks as frequently, making it a beneficial optional operation for running efficiency. Such xxxxFromISR() APIs will have a BaseType_t *pxHigherPriorityTaskWoken parameter to determine if a higher priority task has been awakened, allowing the ISR to decide whether to perform a task switch.I have extracted the ISR-specific API functions from the manual, along with their corresponding regular API versions, listed in the table below. Some regular API versions have a parameter specifying wait time, which is removed in the ISR versions.
ISR Specific Function Name | Corresponding Regular API | Other Features |
xTaskGetTickCountFromISR | xTaskGetTickCount | |
xTaskNotifyFromISR | xTaskNotify | Additional Parameters |
xTaskNotifyAndQueryFromISR | xTaskNotifyAndQuery | Additional Parameters |
vTaskNotifyGiveFromISR | xTaskNotifyGive | Additional Parameters |
xTaskResumeFromISR | vTaskResume | Return Value |
xQueueIsQueueEmptyFromISR | — | |
xQueueIsQueueFullFromISR | — | |
uxQueueMessagesWaitingFromISR | uxQueueMessagesWaiting | |
xQueueOverwriteFromISR | xQueueOverwrite | Additional Parameters |
xQueuePeekFromISR | xQueuePeek | Cancel Waiting |
xQueueReceiveFromISR | xQueueReceive | Additional Parameters, Cancel Waiting |
xQueueSelectFromSetFromISR | xQueueSelectFromSet | Cancel Waiting |
xQueueSendFromISR | xQueueSend | Additional Parameters, Cancel Waiting |
xQueueSendToBackFromISR | xQueueSendToBack | Additional Parameters |
xQueueSendToFrontFromISR | xQueueSendToFront | Additional Parameters |
xSemaphoreGiveFromISR | xSemaphoreGive | Additional Parameters |
xSemaphoreTakeFromISR | xSemaphoreTake | Additional Parameters, Cancel Waiting |
xTimerChangePeriodFromISR | xTimerChangePeriod | Additional Parameters, Cancel Waiting |
xTimerPendFunctionCallFromISR | xTimerPendFunctionCall | Additional Parameters, Cancel Waiting |
xTimerResetFromISR | xTimerReset | Additional Parameters, Cancel Waiting |
xTimerStartFromISR | xTimerStart | Additional Parameters, Cancel Waiting |
xTimerStopFromISR | xTimerStop | Additional Parameters, Cancel Waiting |
xEventGroupClearBitsFromISR | xEventGroupClearBits | Executed in Daemon Task |
xEventGroupGetBitsFromISR | xEventGroupGetBits | Executed in Daemon Task |
xEventGroupSetBitsFromISR | xEventGroupSetBits | Additional Parameters, Executed in Daemon Task |
When an ISR requires task scheduling (for example, when a certain API returns *pxHigherPriorityTaskWoken equal to pdTRUE), portYIELD_FROM_ISR(pdTRUE) should be executed before returning from the ISR to switch tasks. For the Cortex-M3 platform, portYIELD_FROM_ISR() checks if the parameter is true and implements scheduling in the same way as portYIELD(), which sets the PendSV bit in the NVIC (interrupt controller). Thus, after all hardware interrupt request ISRs return, the PendSV interrupt’s ISR is executed, and the scheduler performs task switching. (Refer to my previous post “FreeRTOS Study Notes (3) Task States and Switching”) Using ISR to trigger task scheduling logically delegates part of the external interrupt event handling to a certain (or certain) task, performing only urgent and non-time-consuming processing (like reading hardware device registers, clearing flags, and transferring buffer data) within the ISR. The remaining tasks are managed by the FreeRTOS scheduler according to task priorities, making it seem as if the tasks are waiting for interrupts to occur and then immediately processing them.4. Daemon TaskIt is certainly reasonable to delegate more complex and time-consuming hardware interrupt processing to a separate task, but FreeRTOS also provides a mechanism to avoid creating separate tasks. This is achieved through the system’s Daemon Task. The xTimerPendFunctionCallFromISR() function submits a regular function as a parameter to be executed by the system’s built-in Daemon Task. When submitting, two parameters are specified to be passed to this function. The Daemon Task is managed by the scheduler, and its task priority is specified by configTIMER_TASK_PRIORITY. When the Daemon Task executes the submitted function depends on whether the system is idle; when it gets a chance to execute, it will retrieve the function entry address and parameters from the command queue for execution. Borrowing a diagram from the manual:
-
static void prvTimerTask( void *pvParameters )
-
{
-
TickType_t xNextExpireTime;
-
BaseType_t xListWasEmpty;
-
#if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
-
{
-
extern void vApplicationDaemonTaskStartupHook( void );
-
}
-
#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */
-
for( ;; )
-
{
-
xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
-
prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
-
prvProcessReceivedCommands();
-
}
-
}
The loop processes software timer events, handling them one by one in order of expiration time (executing corresponding functions). This involves software timers—one of FreeRTOS’s features, which we will study later. To clarify how functions submitted from ISRs are executed, let’s first look at what xTimerPendFunctionCallFromISR() does:
-
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken )
-
{
-
DaemonTaskMessage_t xMessage;
-
BaseType_t xReturn;
-
xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;
-
xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;
-
xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;
-
xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;
-
xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
-
tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );
-
return xReturn;
-
}
It is easy to understand that the function address and parameters to be executed are filled in the DaemonTaskMessage_t data structure and added to the xTimerQueue. In the task loop above, there is a line (the complete code is not listed here) that calls:vQueueWaitForMessageRestricted(xTimerQueue, (xNextExpireTime – xTimeNow), xListWasEmpty);which waits for a message in the xTimerQueue until the next software timer expires. Thus, when the Daemon Task receives a message sent from the ISR, it will execute the command (function call) specified by the message.SummaryTo support real-time responses to hardware events, interrupt service routines (ISRs) must be executed as early as possible. Since there may be various interrupts occurring, ISRs need to be written as short as possible, performing critical operations and returning promptly to allow other interrupts to be processed. FreeRTOS provides a series of mechanisms that allow ISRs to delegate operations that need to be handled but are not urgent to tasks, thus rationally distributing CPU resources.
Recommended Reading
Insights | FreeRTOS Study Notes — Application Scenarios
Insights | FreeRTOS Study Notes — Stack (Key to Task Switching)
Insights | FreeRTOS Study Notes — Task States and Switching
Insights | FreeRTOS Study Notes — Inter-Task Communication
Insights | 12 Details Easily Overlooked in PCB Layout
Insights | 7 Uses of Diodes Every Engineer Must Master
Insights | Engineers Use This Ingenious and Inexpensive Current Detection Circuit!
Insights | PCB Design Specifications Are Really About “How to Place” and “How to Connect”!
Insights | Why a Low-Jitter Clock Is Needed for High-Performance ADC Evaluation?
Insights | Understanding Microcontroller Communication Timing Analysis Correctly

All WeChat public accounts belong to
EEWorld (www.eeworld.com.cn)
Welcome to long press the QR code to follow!
EEWorld Subscription Account: Electronic Engineering World
EEWorld Service Account: Electronic Engineering World Welfare Society