systemref

FreeRTOS Task Scheduling — A Practical Reference

H. Maqsood Aug 24, 2026 3 min read rtos freertos embedded scheduling stm32
FreeRTOS uses a fixed-priority preemptive scheduler. Higher-priority tasks always preempt lower ones. Most firmware bugs involving FreeRTOS come from incorrect priority assignment, stack underallocation, or blocking the idle task.

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:

  1. Never starve the idle task — if any task runs at priority 0 without blocking, the idle task never runs, and vTaskDelete memory cleanup stalls
  2. 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.