FreeRTOS high-priority task blocks indefinitely on semaphore — priority inheritance not kicking in?
Asked by stale_biscuit_03 ·
Running FreeRTOS 10.4.3 on an STM32G474. Three tasks:
vSensorTaskat priority 1: reads a MAX6675 SPI thermocouple ADC every 200 ms, writes the result to a sharedSensorData_tstruct, then gives a semaphorevDSPTaskat priority 3: runs continuous signal processing on accumulated samples in a loopvAlertTaskat 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?
3 Replies
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.
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.
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.