HomeAbout UsContact Us

FreeRTOS Critical Sections and Synchronization Primitives

By Jithin Tom
Published in Embedded OS
July 29, 2026
2 min read
FreeRTOS Critical Sections and Synchronization Primitives

Table Of Contents

01
The Critical Section Mechanism
02
When to Use Critical Sections
03
When NOT to Use Critical Sections
04
Mutex vs Critical Section: The Trade-off
05
Priority Inheritance in FreeRTOS Mutexes
06
Task Notifications as Lightweight Synchronization
07
Choosing the Right Primitive
08
Common Pitfalls
09
Measuring Critical Section Impact
10
Summary
11
Related Reading
12
References
13
Frequently Asked Questions

FreeRTOS critical sections are the most primitive synchronization mechanism in the kernel, yet they are frequently misused. Understanding when to use taskENTER_CRITICAL versus a mutex, semaphore, or task notification is essential for building responsive, deterministic real-time systems.

The Critical Section Mechanism

On ARM Cortex-M, FreeRTOS implements critical sections by manipulating the BASEPRI register rather than globally disabling interrupts with PRIMASK. This distinction matters: BASEPRI masks interrupts with priority values equal to or greater than configMAX_SYSCALL_INTERRUPT_PRIORITY (lower urgency in Cortex-M’s inverted numbering), while interrupts with numerically lower priority values (higher urgency) remain enabled — preserving responsiveness for critical ISRs like motor control loops or safety monitors.

/* FreeRTOS critical section macros expand to: */
#define taskENTER_CRITICAL() vPortEnterCritical()
#define taskEXIT_CRITICAL() vPortExitCritical()
/* Simplified Cortex-M implementation: */
void vPortEnterCritical(void) {
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
}
void vPortExitCritical(void) {
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 ) {
portENABLE_INTERRUPTS();
}
}
/* Where portDISABLE_INTERRUPTS() expands to: */
static inline void vPortRaiseBASEPRI(void) {
uint32_t ulNewBASEPRI;
__asm volatile (
"mov %0, %1 \n" /* Load priority threshold */
"cpsid i \n" /* Disable interrupts (PRIMASK) */
"msr basepri, %0 \n" /* Set BASEPRI to mask threshold */
"isb \n" /* Flush pipeline */
"dsb \n" /* Complete all memory accesses */
"cpsie i \n" /* Re-enable interrupts (PRIMASK) */
: "=r" (ulNewBASEPRI)
: "i" (configMAX_SYSCALL_INTERRUPT_PRIORITY)
: "memory"
);
}

The nesting count (uxCriticalNesting) allows recursive entry — each taskENTER_CRITICAL increments the counter, and interrupts only re-enable when the count reaches zero.

When to Use Critical Sections

Critical sections are appropriate for:

  1. Hardware register access — Modifying peripheral registers that aren’t atomic
  2. Small shared variables — Updating a few bytes (flags, counters) where a mutex would add excessive overhead
  3. Interrupt context — ISRs cannot use mutexes; taskENTER_CRITICAL_FROM_ISR/taskEXIT_CRITICAL_FROM_ISR provide the only mutual exclusion option
/* Example: Atomic flag update shared with ISR */
volatile uint32_t g_system_flags = 0;
void set_flag_from_task(uint32_t flag) {
taskENTER_CRITICAL();
g_system_flags |= flag;
taskEXIT_CRITICAL();
}
uint32_t get_and_clear_flags_from_isr(void) {
uint32_t flags;
UBaseType_t saved = taskENTER_CRITICAL_FROM_ISR();
flags = g_system_flags;
g_system_flags = 0;
taskEXIT_CRITICAL_FROM_ISR(saved);
return flags;
}

When NOT to Use Critical Sections

Avoid critical sections for:

  • Long operations — Anything exceeding ~10 µs degrades interrupt latency
  • Complex data structures — Linked lists, queues, trees need mutex protection
  • Blocking operations — Never call vTaskDelay, queue sends, or semaphore takes inside a critical section
  • Cross-task synchronization — Use mutexes, semaphores, or task notifications instead

Mutex vs Critical Section: The Trade-off

+-------------------+----------------------+------------------------+
| Aspect | Critical Section | Mutex |
+-------------------+----------------------+------------------------+
| Interrupt Latency | Blocks all ISRs | Only blocks contending |
| | up to configMAX_ | tasks; ISRs run free |
| | SYSCALL_INTERRUPT_ | |
| | PRIORITY | |
+-------------------+----------------------+------------------------+
| Priority Inversion| None (no ownership) | Prevented by priority |
| | | inheritance |
+-------------------+----------------------+------------------------+
| Overhead | ~20-40 cycles | ~500-2000 cycles |
| | (enter + exit pair) | (kernel calls) |
+-------------------+----------------------+------------------------+
| Scope | Global (all code) | Per-resource |
+-------------------+----------------------+------------------------+
| Usable in ISR | Yes (FromISR vars) | No |
+-------------------+----------------------+------------------------+

Priority Inheritance in FreeRTOS Mutexes

FreeRTOS mutexes implement priority inheritance automatically. When a high-priority task blocks on a mutex held by a low-priority task, the low-priority task temporarily inherits the high priority until it releases the mutex.

SemaphoreHandle_t i2c_mutex;
void i2c_task_high_priority(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(i2c_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
/* High-priority task gets the bus */
i2c_transfer(I2C1, ...);
xSemaphoreGive(i2c_mutex);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void i2c_task_low_priority(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(i2c_mutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
/* Low-priority task inherits high priority while holding mutex */
i2c_transfer(I2C1, ...);
xSemaphoreGive(i2c_mutex); /* Priority drops back here */
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}

Without priority inheritance, a medium-priority task could preempt the low-priority mutex holder, causing unbounded priority inversion — the classic Mars Pathfinder failure mode.

Task Notifications as Lightweight Synchronization

Since FreeRTOS v8.2.0, task notifications provide a faster alternative to binary semaphores for task-to-task and ISR-to-task signaling. Each task has a 32-bit notification value and a state flag.

/* ISR signaling a task */
void EXTI0_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* Task waiting for notification */
void waiting_task(void *pvParameters) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* Blocks until notified */
process_event();
}
}

Notifications are ~45% faster than binary semaphores and use less RAM (no queue structure). However, they only support one-to-one signaling — use semaphores or queues for many-to-one patterns.

Choosing the Right Primitive

+--------------------+--------------------------------------------------+
| Scenario | Recommended Primitive |
+--------------------+--------------------------------------------------+
| Hardware register | taskENTER_CRITICAL / |
| update (few insns) | taskENTER_CRITICAL_FROM_ISR |
+--------------------+--------------------------------------------------+
| Shared buffer / | Mutex (priority inheritance) |
| complex struct | |
+--------------------+--------------------------------------------------+
| ISR -> Task signal | Task notification (ulTaskNotifyTake/Give) |
+--------------------+--------------------------------------------------+
| Task -> Task sync | Binary semaphore or task notification |
| (one-to-one) | |
+--------------------+--------------------------------------------------+
| Task -> Task sync | Counting semaphore or queue |
| (many-to-one) | |
+--------------------+--------------------------------------------------+
| Data transfer | Queue (or stream/message buffer for large) |
| between tasks | |
+--------------------+--------------------------------------------------+
| Mutual exclusion | Mutex (NOT binary semaphore) |
| with priority | |
| inheritance | |
+--------------------+--------------------------------------------------+

Common Pitfalls

1. Calling API functions in critical sections:

/* WRONG - undefined behavior */
taskENTER_CRITICAL();
xQueueSend(queue, &item, 0); /* May yield or re-enable interrupts! */
taskEXIT_CRITICAL();
/* CORRECT - queues are inherently thread-safe */
xQueueSend(queue, &item, pdMS_TO_TICKS(10));

2. Forgetting FromISR variants in ISRs:

/* WRONG - crashes or corrupts kernel */
void BAD_IRQHandler(void) {
taskENTER_CRITICAL(); /* Not ISR-safe! */
...
taskEXIT_CRITICAL();
}
/* CORRECT */
void GOOD_IRQHandler(void) {
UBaseType_t saved = taskENTER_CRITICAL_FROM_ISR();
...
taskEXIT_CRITICAL_FROM_ISR(saved);
}

3. Excessive critical section duration:

/* WRONG - blocks interrupts for milliseconds */
taskENTER_CRITICAL();
for (int i = 0; i < 1000; i++) {
process_byte(buffer[i]);
}
taskEXIT_CRITICAL();
/* CORRECT - copy data quickly, process outside */
taskENTER_CRITICAL();
memcpy(local_buf, shared_buf, sizeof(local_buf));
taskEXIT_CRITICAL();
process_buffer(local_buf); /* No interrupts blocked */

Measuring Critical Section Impact

Instrument your critical sections to measure actual duration:

#include "SEGGER_RTT.h" /* Or use DWT cycle counter */
void measured_critical_section(void) {
uint32_t start = DWT->CYCCNT;
taskENTER_CRITICAL();
/* ... critical work ... */
taskEXIT_CRITICAL();
uint32_t cycles = DWT->CYCCNT - start;
uint32_t us_int = cycles / (SystemCoreClock / 1000000U);
uint32_t us_frac = (cycles % (SystemCoreClock / 1000000U)) * 100
/ (SystemCoreClock / 1000000U);
if (us_int > 10) {
/* SEGGER_RTT_printf does not support %f — use integer math */
SEGGER_RTT_printf(0, "WARN: Critical section took %u.%02u us\n",
us_int, us_frac);
}
}

Enable configUSE_TRACE_FACILITY and configGENERATE_RUN_TIME_STATS for kernel-level profiling.

Summary

  • Critical sections are for ultra-short, interrupt-safe atomic operations — typically hardware register access or a few bytes of shared data
  • Mutexes are the default choice for mutual exclusion between tasks — they provide priority inheritance and don’t block ISRs
  • Task notifications are the fastest ISR-to-task and one-to-one task signaling mechanism
  • Semaphores and queues handle many-to-one synchronization and data transfer
  • Never call FreeRTOS APIs inside critical sections; never use critical sections for long operations
  • Measure your critical section durations — keep them under 10 µs on Cortex-M

The right primitive minimizes both latency and priority inversion while keeping code maintainable. When in doubt, start with a mutex — the overhead is negligible compared to the cost of a subtle race condition or priority inversion bug in production.

  • FreeRTOS Task Notifications vs Queues - When to Use Each
  • Priority Inversion and Inheritance in RTOS
  • Interrupt Handling and ISRs in Embedded Systems

References

  1. FreeRTOS Kernel Developer Guide — Critical Sections and Mutexes. https://www.freertos.org/a00021.html
  2. Barry, R. “Using the FreeRTOS Real Time Kernel — A Practical Guide.” Real Time Engineers Ltd., 2020.
  3. Armstrong, J. “Priority Inversion and Priority Inheritance in FreeRTOS.” Embedded Systems Conference, 2019.
  4. ARM Cortex-M Programming Guide to Memory Barrier Instructions. ARM DAI 0321A, 2017. https://developer.arm.com/documentation/ddi0439/b
  5. Lam, P. “Real-Time Operating Systems for ARM Cortex-M Microcontrollers.” Springer, 2021. ISBN 978-3030823901
  6. Mars Pathfinder Mission — Priority Inversion Case Study. NASA, 1997. https://www.nasa.gov/mars-pathfinder/

Frequently Asked Questions

What is the difference between a critical section and a mutex in FreeRTOS?

A critical section masks interrupts up to configMAX_SYSCALL_INTERRUPT_PRIORITY on Cortex-M (or disables them globally on simpler architectures), providing atomic access but increasing interrupt latency. A mutex uses priority inheritance and only blocks tasks that contend for the same resource, allowing all ISRs to continue executing.

When should I use taskENTER_CRITICAL vs a mutex?

Use taskENTER_CRITICAL for very short operations (a few instructions) where interrupt latency must be minimized, such as accessing hardware registers or updating a few bytes of shared data. Use a mutex for longer critical regions, complex data structures, or when you need priority inheritance to prevent priority inversion.

How does FreeRTOS implement critical sections on ARM Cortex-M?

On Cortex-M, FreeRTOS uses the BASEPRI register to mask interrupts up to configMAX_SYSCALL_INTERRUPT_PRIORITY rather than globally disabling interrupts with PRIMASK. This allows high-priority ISRs (above the syscall threshold) to remain responsive while protecting kernel data structures.

What is the maximum time a critical section should take?

Critical sections should execute in microseconds — typically under 10 µs on modern Cortex-M cores. Longer sections degrade interrupt latency and can cause missed deadlines in hard real-time systems. If your critical section exceeds this, refactor to use a mutex or redesign the data access pattern.

Can I call FreeRTOS API functions inside a critical section?

No. Calling FreeRTOS API functions (like xQueueSend, xSemaphoreGive) inside a critical section is undefined behavior. The API functions themselves use critical sections internally and may re-enable interrupts or yield. Always use the FromISR variants in ISRs and regular API functions from tasks outside critical sections.

Tags

freertoscritical-sectionmutexsemaphoresynchronizationarm-cortex-m

Share


Previous Article
Cortex-M SysTick Timer Configuration and Usage
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

FreeRTOS Heap Implementations Compared: Heap_1 to Heap_5
FreeRTOS Heap Implementations Compared: Heap_1 to Heap_5
July 14, 2026
5 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media