The task states are the internal situations that a task goes through within the Scheduler while waiting, executing, blocking, or being suspended.
In the previous article, we saw how priorities decide which task runs. But to truly understand how FreeRTOS works, we need to look “under the hood” and see what state each task can be in.
A task is not a binary thing that simply “works” or “doesn’t work”. Throughout its life, it goes through a state machine managed by the Scheduler. Understanding this flow is the difference between firmware that works by chance and one that is robust.
Today we are going to look at the four fundamental states of a task in FreeRTOS.
The State Machine
Imagine a doctor’s waiting room (the processor).
Running: The patient who is inside the consultation room with the doctor.
Ready: The patients in the waiting room who could go in already, but are waiting for their turn (because someone more important is there or they arrived later).
Blocked: Patients who are not in the waiting room because they are waiting for test results. It doesn’t make sense for them to occupy a chair until the results arrive.
Suspended: Patients we have sent home indefinitely until we call them back.
A task usually alternates between Ready and Running. When it waits for time or an event, it goes to Blocked; when the wait ends, it returns to Ready.
Running State
It’s the simplest state: The task is using the CPU at this very moment.
- On a Single Core system, only one task can be in Running at a time.
- On a dual-core ESP32, there can be a maximum of two, one per core.
When a task is in Running, it executes its instructions sequentially until:
- The Scheduler interrupts it (its time runs out or someone with higher priority arrives).
- It decides to wait itself (
vTaskDelay).
Ready State
This is where tasks that want to work but cannot because the CPU is busy with another task of equal or higher priority reside.
- They are “awake” and ready to go into action.
- The Scheduler constantly checks this list. As soon as the CPU becomes free, it selects the Ready task with the highest priority and moves it to Running.
Blocked State
This is, paradoxically, the most important state in an RTOS.
A task enters the Blocked state when it is waiting for a temporal or external event to occur.
- Temporal: “Wait 100ms” (
vTaskDelay). - Event: “Wait for data to arrive in a queue” or “Wait for this semaphore to be released”.
The important thing: When a task is Blocked, it does not consume CPU cycles. The Scheduler removes it from the list of reviewable tasks. It’s as if it didn’t exist. This allows the CPU to dedicate itself to other tasks or, if all are blocked, to enter a low-power mode (IDLE).
If we were to use a constant polling loop, we would be wasting CPU time in Running state. With a queue, a semaphore, or a notification, we can ask the system: “wake me up when the event happens” and move to Blocked.
Suspended State
This is a “deep hibernation” state. A task in this state is not waiting for anything (neither time nor events). It is simply “frozen” because we have explicitly ordered it so.
- It is only entered by calling
vTaskSuspend(). - It is only exited by calling
vTaskResume().
It is useful if we want to temporarily deactivate a device functionality (e.g., turn off sensor management because the battery is low) without destroying the task and losing its state.
Transitions During the Lifecycle
Let’s see how a task moves through these states with a real code example.
TaskHandle_t hTaskLED = NULL;
void TaskLED(void *pvParameters) {
// 1. On creation, the task is born in the READY state.
// If it has sufficient priority, the Scheduler moves it to RUNNING.
for(;;) {
// --- STATE: RUNNING ---
digitalWrite(LED_BUILTIN, HIGH);
Serial.println("LED ON");
// Now we want to wait for 1 second.
// We call vTaskDelay.
// --- Transition: RUNNING -> BLOCKED ---
vTaskDelay(pdMS_TO_TICKS(1000));
// During this second, the task "disappears" from the CPU.
// Other tasks can run.
// After 1000ms, the system fires an internal event.
// --- Transition: BLOCKED -> READY ---
// If we are the task with the highest priority:
// --- Transition: READY -> RUNNING ---
digitalWrite(LED_BUILTIN, LOW);
Serial.println("LED OFF");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}The Case of vTaskSuspend
Let’s see how to freeze a task from another one.
void ControlTask(void *param) {
for(;;) {
if(digitalRead(PAUSE_BUTTON) == LOW) {
// User presses the button
Serial.println("Pausing the LED task...");
// --- Transition: (Any) -> SUSPENDED ---
vTaskSuspend(hTaskLED);
}
else {
// --- Transition: SUSPENDED -> READY ---
vTaskResume(hTaskLED);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}Be careful with vTaskSuspend. If you suspend a task that “holds” a shared resource (like a Mutex), that resource will be blocked indefinitely and no one else can use it. Generally, it is better to use synchronization mechanisms than brute suspensions.
The Idle Task: What Happens When Everyone Sleeps?
If we design our system well, most of the time our tasks should be in the Blocked state waiting for things. What does the processor do if all tasks are blocked at the same time?
FreeRTOS automatically creates a task on startup called the IDLE Task.
- It has priority 0 (the lowest possible).
- It is always in the Ready state (or Running if no one else wants the CPU).
- It provides an executable context when there is no other work and performs internal maintenance tasks.
Although it seems useless, it is important:
- It keeps the Scheduler in a valid state and allows the port to put the CPU in low-power mode when configured.
- It takes care of cleaning up the memory of tasks that have been deleted with
vTaskDelete. - It allows executing “Hooks” to put the microcontroller into Sleep mode and save battery.