In this lesson:
- what a task is: a function + its own stack + priority + state
- the task states (Ready, Running, Blocked, Suspended)
- what the scheduler does, and when it runs
- the idle task and
vTaskDelayvsvTaskDelayUntil - the gotchas: tasks that return, stack sizing, starving the idle task
A task is a thread of execution
An RTOS runs several tasks concurrently. A task is an independent thread of execution, to the programmer it looks like an ordinary function with its own infinite loop:
void blink_task(void *arg) {
for (;;) { // a task NEVER returns β it loops forever
gpio_toggle(LED);
vTaskDelay(pdMS_TO_TICKS(500)); // block 500 ms β yields the CPU
}
}Behind it, the kernel gives each task three things:
- its own stack, local variables, call frames, and the saved CPU context live here,
- a priority, how urgent it is relative to other tasks,
- a TCB (Task Control Block), kernel bookkeeping: stack pointer, priority, state, list links.
Creating one in FreeRTOS:
xTaskCreate(
blink_task, // the task function
"blink", // name (for debugging)
128, // stack depth (in words, not bytes)
NULL, // parameter passed to the task
2, // priority (higher number = higher priority in FreeRTOS)
NULL); // optional handle out
// ...create more tasks...
vTaskStartScheduler(); // hands the CPU to the scheduler; never returnsTask states
At any instant a task is in one of a few states. The transitions are the heart of how an RTOS works:
stateDiagram-v2
[*] --> Ready: created
Ready --> Running: scheduler picks it (highest priority ready)
Running --> Ready: preempted / time-sliced
Running --> Blocked: waits (vTaskDelay, queue, semaphore)
Blocked --> Ready: event/timeout occurs
Running --> Suspended: vTaskSuspend
Suspended --> Ready: vTaskResume- Running, currently executing (only one task per core at a time).
- Ready, able to run, waiting for the CPU because something higher-priority has it.
- Blocked, waiting for an event (a delay to expire, a queue item, a semaphore), not using the CPU. This is the key: a blocked task costs nothing.
- Suspended, explicitly parked until resumed.
The crucial idea: a task blocks to wait, which hands the CPU to other Ready tasks. That's why an RTOS multitasks efficiently, waiting is free.
The scheduler
The scheduler is the part of the kernel that decides which Ready task runs. In a priority-based preemptive RTOS (FreeRTOS's default) the rule is simple:
Run the highest-priority task that is Ready. The moment a higher-priority task becomes Ready, preempt the current one.
The scheduler gets a chance to act:
- on every tick (a periodic timer interrupt, the system "heartbeat", e.g. 1 kHz),
- whenever a task blocks (it must pick someone else),
- whenever an event makes a higher-priority task Ready (e.g. an ISR gives a semaphore).
So context switches happen both periodically and event-driven, not just on the tick.
The idle task
When no application task is Ready, the kernel runs the idle task, a built-in lowest-priority (0) task that simply spins (and frees memory from deleted tasks). It exists so the CPU always has something to run. You hook it for two big jobs:
- Low power, the idle hook puts the CPU to sleep (
WFI) until the next interrupt, saving energy on battery devices. - Background housekeeping, and often, kicking the watchdog only if the system is healthy (recall the watchdog lesson, don't kick blindly).
If a high-priority task never blocks, the idle task (and everything below that task) never runs, a real bug (below).
Delaying: relative vs periodic
vTaskDelay(pdMS_TO_TICKS(20)); // block ~20 ms FROM NOW (relative)vTaskDelay blocks for a duration measured from when it's called, so if your work before it takes variable time, the period drifts. For a fixed period (a control loop that must run exactly every 10 ms), use vTaskDelayUntil, which anchors to an absolute wake time:
TickType_t last = xTaskGetTickCount();
for (;;) {
do_control_step();
vTaskDelayUntil(&last, pdMS_TO_TICKS(10)); // exact 10 ms period, no drift
}Gotchas
- A task must never return. Falling off the end of a task function is undefined behavior. Loop forever (
for(;;)) or explicitlyvTaskDelete(NULL)to remove it. - Stack sizing per task. Each task's stack must hold its deepest call chain plus interrupt nesting. Too small β overflow into another task's memory (no MMU = silent corruption). Use stack-overflow checking and high-water-mark APIs during development.
- A never-blocking high-priority task starves everything below it. A CPU-bound task that never calls a blocking API hogs the core; lower-priority tasks and the idle task never run (so no low-power, maybe no watchdog kick). High-priority tasks should block to wait, not spin.
vTaskDelaydrifts; usevTaskDelayUntilfor periodic. Relative delay accumulates jitter from your variable work; absolute wake times keep a fixed period.- Priorities are a design decision. Too many tasks at the same priority devolves into time-slicing; a bad priority ordering causes missed deadlines or inversion (later lessons). Assign priorities deliberately, by deadline urgency.
TL;DR
- A task is an independent thread of execution: a never-returning function with its own stack, a priority, and a kernel TCB; create with
xTaskCreate, thenvTaskStartScheduler(). - A task is always in one state, Running, Ready, Blocked (waiting, using no CPU), or Suspended, and blocking to wait is what lets the RTOS multitask efficiently.
- The scheduler runs the highest-priority Ready task and preempts when a higher-priority task becomes Ready; it acts on the tick, on blocks, and on events (e.g. an ISR).
- The idle task runs when nothing else is Ready, hook it for low-power sleep and health-gated watchdog kicks.
- Watch outs: tasks must never return, size each stack for worst case, don't let a high-priority task never block (it starves the rest), and use
vTaskDelayUntilfor drift-free periodic work.