Electronics Design AU
RTOSSolved

FreeRTOS high-priority task blocks indefinitely on semaphore — priority inheritance not kicking in?

5 min read3 replies
Original Question

Asked by stale_biscuit_03 ·

Running FreeRTOS 10.4.3 on an STM32G474. Three tasks:

  • vSensorTask at priority 1: reads a MAX6675 SPI thermocouple ADC every 200 ms, writes the result to a shared SensorData_t struct, then gives a semaphore
  • vDSPTask at priority 3: runs continuous signal processing on accumulated samples in a loop
  • vAlertTask at priority 5: takes the semaphore to read the shared struct and check for over-temperature conditions

Semaphore init in main(), before vTaskStartScheduler():

SemaphoreHandle_t xDataSem;
xDataSem = xSemaphoreCreateBinary();
xSemaphoreGive(xDataSem); // start in 'available' state

Problem: vAlertTask calls xSemaphoreTake(xDataSem, pdMS_TO_TICKS(500)) and it always returns pdFALSE — never gets the semaphore, just times out. Checked the debugger and the task states are: vAlertTask = Blocked, vSensorTask = Ready, vDSPTask = Running.

So vSensorTask is stuck in Ready — it holds the semaphore and wants to run but never gets the CPU. I thought that when vAlertTask (priority 5) blocks waiting for a semaphore held by vSensorTask (priority 1), FreeRTOS should temporarily raise vSensorTask's priority so it can finish writing and release. But vDSPTask at priority 3 just keeps running.

I tried adding configUSE_MUTEXES 1 to FreeRTOSConfig.h thinking that might be the missing config flag, but nothing changed. Is priority inheritance supposed to just work automatically, or is there something else I need to enable?

From the knowledge baseHow Do FreeRTOS Queues, Semaphores, and Mutexes Work?

3 Replies

rtos_rita
Accepted Answer

The problem is xSemaphoreCreateBinary(). Binary semaphores do not implement priority inheritance — only mutexes do. configUSE_MUTEXES 1 enables mutex support in the kernel, but it does not retroactively apply priority inheritance to binary semaphores.

Why binary semaphores can't inherit priority:

Priority inheritance requires the scheduler to track which task owns the synchronisation object. When a higher-priority task blocks waiting for it, the scheduler temporarily raises the owner's priority to match the waiter's, so the owner can preempt any mid-priority tasks and finish quickly.

Binary semaphores have no ownership concept. Any task or ISR can call xSemaphoreGive() on a binary semaphore regardless of whether it ever called xSemaphoreTake(). Without a defined owner, there's nothing to promote.

Mutexes enforce strict ownership — only the task that successfully took the mutex may give it back. That constraint is what makes priority inheritance possible.

The fix:

// Before (no priority inheritance):
xDataSem = xSemaphoreCreateBinary();
xSemaphoreGive(xDataSem); // manual initialisation step needed

// After (priority inheritance active):
xDataSem = xSemaphoreCreateMutex();
// No xSemaphoreGive() needed — mutexes start in the available state

The xSemaphoreTake() and xSemaphoreGive() calls inside your tasks stay exactly the same — only the handle creation changes. Once you make the switch, when vAlertTask (priority 5) blocks on the mutex held by vSensorTask (priority 1), the scheduler raises vSensorTask's effective priority to 5 for the duration. That's higher than vDSPTask at priority 3, so vDSPTask gets preempted, vSensorTask runs, writes the data, and releases.

Secondary issue — vDSPTask's tight loop:

Even after fixing the semaphore, vDSPTask running without any blocking call is a structural problem. Priority inheritance only elevates vSensorTask while vAlertTask is blocked waiting. The moment vAlertTask reads the data and gives back the mutex, the promotion ends. If vDSPTask is still running and never yields, vSensorTask won't get another scheduling slot until vDSPTask blocks on something.

Add at minimum vTaskDelay(1) at the end of vDSPTask's loop to yield once per tick. Better: restructure it to block on a notification or queue item from vSensorTask so it only runs when there's actually new data to process.

The FAQ section in How Do FreeRTOS Queues, Semaphores, and Mutexes Work? covers exactly why replacing a mutex with a binary semaphore when guarding a shared resource causes priority inversion — it is the single most common FreeRTOS synchronisation mistake.

isr_overload

Rita's identified the root cause. For diagnosing this class of problem faster in the future, vTaskList() is worth adding to your debug toolkit.

Call it from a low-priority UART diagnostic task or a button ISR that posts a flag:

char pcBuf[512];
vTaskList(pcBuf);
HAL_UART_Transmit(&huart2, (uint8_t *)pcBuf, strlen(pcBuf), HAL_MAX_DELAY);

The output shows each task's current state, effective priority, minimum remaining stack in words, and task number:

Task          State  Priority  Stack  Num
vAlertTask    B      5         312    3
vSensorTask   R      1         280    2
vDSPTask      X      3         196    1
IDLE          R      0         108    4

X = Running, R = Ready, B = Blocked. A task in Ready state that logically should be unblocking a higher-priority waiter and isn't — that's your tell. Either something is monopolising the CPU (starvation), or the synchronisation object has no priority inheritance.

While you're in FreeRTOSConfig.h, also enable:

#define configCHECK_FOR_STACK_OVERFLOW 2

And implement the hook:

void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    (void)xTask;
    /* Log pcTaskName, assert, or toggle a GPIO before halting */
    while (1);
}

The stack column in vTaskList() showing under ~20 words for any task means you should increase that task's usStackDepth in xTaskCreate(). There's a full sizing reference in How Do You Create and Schedule Tasks in FreeRTOS?.

Note: vTaskList() calls vTaskSuspendAll() internally, briefly freezing the scheduler. Fine for development, not production code.

soggy_waffle42

Two mutex rules that bite people.

One: the task that Takes a mutex must be the task that Gives it back. Ownership is per-task. Give from a different task and the scheduler's priority inheritance state gets corrupted — you'll see intermittent priority promotion failures that don't reproduce under the debugger. FreeRTOS won't assert on this in all configurations, so the corruption can be silent for a long time.

Two: mutexes cannot be used from ISRs. There is no xSemaphoreTakeFromISR(). This is intentional — priority inheritance across an ISR context has no sensible semantics. If an ISR needs to signal the task that owns a mutex to run, send a queue item or call xTaskNotifyGiveFromISR(), then take the mutex inside the task context.

If you have a SEGGER J-Link, SystemView is worth setting up. It gives you a realtime timeline of every task state transition and every semaphore/mutex event. You'd have seen vSensorTask sitting in Ready while vDSPTask ran uninterrupted without needing to halt and read task state registers manually.