We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

Tasks & the Scheduler

A task is a function with its own stack, priority, and state; the scheduler runs the highest-priority ready task. The task lifecycle (Ready/Running/Blocked), the idle task, and vTaskDelay.

22 min read

In this lesson:


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:

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 returns

Task 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

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:

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:

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


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in RTOS & Real-Time Concepts

Bare-Metal vs RTOSPreemptive vs Round-Robin Scheduling
Context Switching
Mutexes & Semaphores
Queues & Inter-Task Communication
Priority Inversion