If you've worked with FreeRTOS for any length of time, you've likely hit a situation where a low-priority task simply stops running. The system isn't frozen — your high-priority tasks are humming along — but something in the background has silently starved. Let's unpack why this happens and how to prevent it.
FreeRTOS uses a preemptive, priority-based scheduler by default. The highest-priority ready task always runs. If two tasks share the same priority, they round-robin with each tick. Simple enough — but the implications catch people off guard.
// Two tasks at different priorities
xTaskCreate(vHighTask, "High", 256, NULL, 3, NULL);
xTaskCreate(vLowTask, "Low", 256, NULL, 1, NULL);
void vHighTask(void *pv) {
for (;;) {
// If this never blocks, vLowTask never runs
processData();
}
} If vHighTask never yields — no vTaskDelay(), no semaphore wait, no queue receive with a timeout — then vLowTask is permanently starved.
The classic scenario: Task H (high priority) waits on a mutex held by Task L (low priority). Meanwhile, Task M (medium priority) preempts Task L. Now Task H is blocked waiting for Task L, which is blocked by Task M. The effective priority is inverted.
FreeRTOS offers priority inheritance mutexes via xSemaphoreCreateMutex(). When Task H blocks on a mutex held by Task L, the scheduler temporarily boosts Task L to Task H's priority.
First, every task's main loop should include at least one blocking call. Even a vTaskDelay(1) is enough to yield the processor.
void vSensorTask(void *pv) {
TickType_t xLastWake = xTaskGetTickCount();
for (;;) {
readSensor();
// Guaranteed yield point every 100ms
vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(100));
}
} Second, use configUSE_PREEMPTION and configUSE_TIME_SLICING intentionally. If you disable time slicing, equal-priority tasks won't round-robin.
Third, instrument your system. FreeRTOS's runtime stats (vTaskGetRunTimeStats()) will show you exactly how much CPU time each task is consuming.
The scheduler does exactly what you tell it to. Starvation is a design problem, not a bug. Build every task around explicit blocking points, use priority inheritance mutexes, and monitor runtime stats.