FreeRTOS is a preemptive, fixed-priority scheduler. At any moment, the highest-priority task that is not blocked or suspended runs. If two tasks share the same priority, they alternate in round-robin slices of one tick period (typically 1ms). The scheduler makes no decisions — it mechanically enforces the priority table you define.
Getting that table wrong causes priority inversion, starvation, and missed deadlines. These failures are deterministic and reproducible once you understand the model.
Task states
A FreeRTOS task is always in one of four states:
┌─────────────┐
create │ │ higher-priority
─────────▶ READY │ task unblocks
│ │◀──────────────┐
└──────┬──────┘ │
│ scheduler selects │
▼ │
┌─────────────┐ │
│ │ preempted │
│ RUNNING │───────────────┘
│ │
└──────┬──────┘
│ blocks on queue/
│ semaphore/delay
▼
┌─────────────┐
│ │ event or
│ BLOCKED │ delay expires
│ │──────────────▶ READY
└─────────────┘
A task in BLOCKED state consumes no CPU. This is the correct way to wait — not a spin loop.
Creating tasks
void vSensorTask(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(10); // 100Hz
for (;;) {
read_sensor_and_publish();
vTaskDelayUntil(&xLastWakeTime, xPeriod);
}
}
// At startup:
xTaskCreate(
vSensorTask, // Task function
"Sensor", // Debug name
256, // Stack depth in words (not bytes)
NULL, // Parameter
3, // Priority (higher = more urgent)
NULL // Handle (not needed here)
);
vTaskDelayUntil blocks until the next period deadline, accounting for execution time. vTaskDelay just adds a fixed delay from the current moment — it drifts over time. Use vTaskDelayUntil for periodic tasks.
Priority assignment
Assign priorities based on deadline urgency, not importance. The task with the tightest timing constraint gets the highest priority.
| Task | Period | Priority |
|---|---|---|
| Motor control loop | 1ms | 5 (highest) |
| Sensor acquisition | 10ms | 4 |
| State machine | 50ms | 3 |
| UART logging | 100ms | 2 |
| Telemetry transmit | 1000ms | 1 |
| Idle | — | 0 (reserved) |
Never block inside a high-priority task longer than its period. A 1ms motor control task that acquires a mutex held by a low-priority task will miss deadlines. This is priority inversion — solve it by enabling priority inheritance on the mutex:
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); // priority inheritance built in
Stack sizing
Stack overflow is the most common silent failure mode in FreeRTOS. The task's stack must hold: local variables, function call frames, interrupt context (if the task can be preempted mid-function), and the FreeRTOS task context block.
Use the high watermark API to measure actual usage:
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
// Returns remaining free words. If close to 0, increase stack.
Enable stack overflow detection in FreeRTOSConfig.h:
#define configCHECK_FOR_STACK_OVERFLOW 2
And define the hook:
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
// Log pcTaskName, halt, or reset
for(;;);
}
A common starting point: 128 words for simple tasks, 512 for tasks doing printf or floating-point, 1024 for tasks using a TCP/IP stack.
The idle task
The idle task runs at priority 0 and executes only when all other tasks are blocked. Two rules:
- Never starve the idle task — if any task runs at priority 0 without blocking, the idle task never runs, and
vTaskDeletememory cleanup stalls - The idle hook (
vApplicationIdleHook) must never block and must return. Use it for sleep/low-power entry if needed
If your application never idles (all tasks runnable all the time), your priorities are wrong or your task loads are miscalculated.