
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.
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.
Critical sections are appropriate for:
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;}
Avoid critical sections for:
vTaskDelay, queue sends, or semaphore takes inside a critical section+-------------------+----------------------+------------------------+| 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 |+-------------------+----------------------+------------------------+
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.
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.
+--------------------+--------------------------------------------------+| 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 | |+--------------------+--------------------------------------------------+
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 */
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.
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.
Quick Links
Legal Stuff


