Mar 1 / Wadix Technologies

FreeRTOS ISR Rules on ARM Cortex-M: configMAX_SYSCALL_INTERRUPT_PRIORITY Demystified



1. Introduction

On ARM Cortex M microcontrollers, interrupts are managed by the NVIC and can be assigned different priority levels. Some interrupts are more urgent than others and can preempt lower priority ones. When FreeRTOS is used on such a system, interrupt priority becomes part of the operating system design, not just a hardware configuration detail.

A very common question is why some interrupt service routines are allowed to call FreeRTOS APIs, while others must never do so. On Cortex M, this is not an arbitrary restriction. It is a direct consequence of how the kernel protects its internal data structures using the processor interrupt masking mechanisms.

In a FreeRTOS system running on Cortex M, interrupt priority defines how, and whether, an interrupt is allowed to interact with the kernel. Understanding this boundary is essential for building systems that are both correct and truly real time.


2. Interrupt priorities on Cortex M

On Cortex M, interrupt priorities are controlled by the NVIC. Each interrupt is assigned a priority value. Lower numerical values represent higher urgency. An interrupt with a higher urgency can preempt the execution of lower urgency interrupts and tasks.

In practice, this often looks like:

/* Lower value means higher urgency on Cortex M */

NVIC_SetPriority(USART2_IRQn, 5);

NVIC_SetPriority(ADC_IRQn, 0); /* More urgent than USART2 */

The processor also provides special registers, such as PRIMASK and BASEPRI, that can be used to block interrupts. FreeRTOS does not globally disable all interrupts in most cases. Instead, it uses BASEPRI to mask interrupts up to a certain priority level.

This detail is crucial. When BASEPRI is set to a given value, all interrupts with a priority numerically equal to or worse than that value are masked. Interrupts with a higher urgency, meaning numerically lower priority values, can still run.

So even when the kernel enters a critical section, some very high priority interrupts can still preempt it.


3. Two kinds of interrupts from the kernel’s point of view

Because of this hardware behavior, FreeRTOS effectively divides interrupts into two conceptual groups on Cortex M.

The first group contains interrupts that run at priorities that are masked by BASEPRI. These interrupts are kernel aware. They are allowed to call FreeRTOS FromISR APIs, wake tasks, and request a context switch. The kernel knows how to protect itself against these interrupts using its critical sections.

The second group contains interrupts that run at priorities higher than the BASEPRI masking level. These interrupts are kernel unaware. They can preempt the kernel even while it is inside a critical section. For this reason, they must never call FreeRTOS APIs and must never touch kernel objects.

This split is not a software convention. It is a direct consequence of how the Cortex M interrupt masking hardware works.

Fig 1. Cortex M NVIC priority space divided into kernel aware and kernel unaware regions


4. Why the kernel cannot allow all interrupts to call it

Inside FreeRTOS, the kernel maintains shared data structures such as ready lists, delayed lists, and the internal representations of queues, semaphores, and tasks. To keep these structures consistent, the kernel uses critical sections.

On Cortex M, these critical sections are typically implemented by raising BASEPRI to a specific value. This masks all kernel aware interrupts while the kernel updates its internal state.

However, very high priority interrupts are not masked by BASEPRI. They can still run at any time, even in the middle of a kernel update. If such an interrupt were allowed to call a FreeRTOS API and modify kernel state, it could do so while the kernel is halfway through updating its own data structures.

This would lead to corrupted lists, inconsistent scheduling state, and failures that are often rare and extremely hard to debug.

For this reason, FreeRTOS must draw a strict boundary. Interrupts that can bypass the kernel’s critical sections must never call into the kernel.


5. The role of configMAX_SYSCALL_INTERRUPT_PRIORITY

On Cortex M, this boundary is defined by configMAX_SYSCALL_INTERRUPT_PRIORITY.

Conceptually, this value defines the highest urgency interrupt priority that is allowed to call FreeRTOS APIs. Any interrupt running at a higher urgency than this must be treated as kernel unaware and must not call the kernel.

In code, this boundary is usually reflected when setting NVIC priorities:

/* Kernel aware interrupt, allowed to call FreeRTOS FromISR APIs */

NVIC_SetPriority(USART2_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY);

/* Kernel unaware interrupt, too urgent to call FreeRTOS */

NVIC_SetPriority(ADC_IRQn, 0);

The name is often confusing. It does not mean the most urgent interrupt. It means the maximum priority value that is allowed to make system calls into the kernel.

In other words, it defines the BASEPRI threshold that FreeRTOS uses to protect its critical sections.

Fig 2. configMAX_SYSCALL_INTERRUPT_PRIORITY as a boundary in the Cortex M NVIC priority space.


6. What happens in a kernel aware ISR:

A kernel aware ISR runs at a priority that is masked by BASEPRI when the kernel enters a critical section. This means the kernel can safely assume that such an interrupt will not interrupt it in the middle of a critical update.

These ISRs are allowed to call FreeRTOS FromISR APIs, wake tasks, and request a context switch. A typical pattern looks like this:

void USART2_IRQHandler(void)

{

BaseType_t xHigherPriorityTaskWoken = pdFALSE;


xSemaphoreGiveFromISR(xRxSem, &xHigherPriorityTaskWoken);


portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

}

This shows two important points. The ISR uses the FromISR variant of the API, and it only requests a context switch if a higher priority task was actually unblocked.

This is exactly why the FromISR APIs exist. They provide a controlled and safe way for these interrupts to interact with the scheduler and kernel objects.


7. What happens in a very high priority ISR

A very high priority ISR on Cortex M runs above the BASEPRI masking level. It can preempt the kernel at any time, even in the middle of a critical section.

Because of this, such an ISR must be completely independent of FreeRTOS. It must not call any FreeRTOS API and must not access any kernel object. A minimal and safe pattern is:

void ADC_IRQHandler(void)

{

gAdcOverrunFlag = 1; /* Record event without touching the RTOS */

/* No FreeRTOS calls here */

}

These interrupts are typically used for extremely time critical hardware handling where the lowest possible latency is required. If they need to communicate with the rest of the system, they usually do so indirectly, for example by setting flags or triggering a lower priority, kernel aware interrupt.

Fig 3. A high priority Cortex M interrupt running outside kernel control, while a lower priority interrupt interacts with the scheduler.


8. Conclusion

On ARM Cortex M, interrupt priority is not just a hardware feature. It defines a contract between your ISRs and the FreeRTOS kernel. The kernel relies on BASEPRI to protect its internal state, and this automatically creates two classes of interrupts.

Some interrupts are allowed to call the kernel. Others must never do so. Respecting this boundary is essential for correctness, stability, and real time performance in any FreeRTOS system on Cortex M.

Created with