Table of Contents
KEY TAKEAWAYS
- RTOS tasks cycle through four states: Ready, Running, Blocked, and Suspended. The scheduler always runs the highest-priority Ready task.
- Tasks enter the Blocked state when calling vTaskDelay(), xQueueReceive(), or xSemaphoreTake(), yielding the CPU without wasting cycles.
- Task creation requires careful stack sizing. Use uxTaskGetStackHighWaterMark() to measure actual usage and set the final size to 1.5-2x the measured value.
- The Idle task runs at priority 0 and handles memory cleanup for deleted tasks. If it never runs, your system has a design problem.
- Priority inversion, starvation, and stack overflow are the three most common RTOS bugs. Mutex priority inheritance and stack overflow hooks catch them early.
Part of the Complete Guide to RTOS for Embedded Systems series.
Understanding task states is fundamental to writing correct RTOS firmware. Every task in an RTOS transitions between specific states, and the scheduler uses these states to determine what runs when. This article explains each state, what triggers transitions, provides complete FreeRTOS code examples, and covers the common mistakes that cause hard-to-debug failures in production.
The Four Task States
Every RTOS task exists in one of four states at any given moment.
Running: The task currently executing on the CPU. On a single-core MCU, exactly one task is in the Running state at any time. The scheduler selected this task because it is the highest-priority Ready task.
Ready: The task is able to run but is not currently executing because a higher-priority (or equal-priority) task holds the CPU. Ready tasks are maintained in a priority-ordered list. When the Running task blocks, suspends, or gets preempted, the scheduler immediately picks the highest-priority Ready task to become the new Running task.
Blocked: The task is waiting for something: a time delay, a semaphore, a queue item, or an event flag. Blocked tasks consume zero CPU time. This is the most important state for system efficiency. A well-designed RTOS application has most tasks in the Blocked state most of the time, with only the Idle task running when nothing else needs the CPU.
Suspended: The task is explicitly paused by a call to vTaskSuspend(). Unlike blocked tasks, suspended tasks are not waiting for any event and will not resume automatically. Only an explicit call to vTaskResume() (or vTaskResumeFromISR() from an interrupt) moves a suspended task back to Ready.
State Diagram: Transitions Explained
Here is how tasks transition between states:
Ready to Running: The scheduler selects this task because it is the highest-priority Ready task. This happens when the previously Running task blocks, when a higher-priority task finishes its work, or when the time-slice expires for round-robin scheduling among equal-priority tasks.
Running to Ready: A higher-priority task becomes Ready (preemption). The current task is not finished but must yield the CPU. It goes back to the Ready list and will resume when it becomes the highest-priority Ready task again.
Running to Blocked: The task calls a blocking API: vTaskDelay(), xQueueReceive(), xSemaphoreTake(), ulTaskNotifyTake(), or xEventGroupWaitBits(). The task specifies a timeout (or portMAX_DELAY for indefinite wait).
Blocked to Ready: The event the task is waiting for occurs (timer expires, semaphore given, queue data available), or the timeout expires. The task moves to Ready and will run when it becomes the highest-priority Ready task.
Running to Suspended: vTaskSuspend() is called, either by the task itself (passing NULL) or by another task passing the task handle.
Suspended to Ready: vTaskResume() is called with the suspended task handle.
Task Creation with xTaskCreate
The xTaskCreate() function dynamically allocates a Task Control Block (TCB) and stack from the FreeRTOS heap. Here is a complete example with all parameters explained:
/* FreeRTOS Task Creation - Complete Example */
#include "FreeRTOS.h"
#include "task.h"
/* Task handles for later reference (suspend, resume, delete) */
TaskHandle_t xSensorTaskHandle = NULL;
TaskHandle_t xCommTaskHandle = NULL;
TaskHandle_t xDisplayTaskHandle = NULL;
/* Sensor reading task - highest priority */
void vSensorTask(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
for (;;) {
read_all_sensors();
process_sensor_data();
/* Periodic execution: runs exactly every 100ms */
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
}
}
/* Communication task - medium priority */
void vCommTask(void *pvParameters) {
uint8_t *config = (uint8_t *)pvParameters; /* Cast parameter */
for (;;) {
send_telemetry_packet();
check_for_commands();
vTaskDelay(pdMS_TO_TICKS(250));
}
}
/* Display update task - low priority */
void vDisplayTask(void *pvParameters) {
for (;;) {
update_lcd_screen();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
/* Create tasks with different priorities */
/* Parameters: function, name, stack (words), param, priority, handle */
xTaskCreate(vSensorTask, "Sensor", 512, NULL, 3, &xSensorTaskHandle);
xTaskCreate(vCommTask, "Comm", 256, NULL, 2, &xCommTaskHandle);
xTaskCreate(vDisplayTask, "Display", 256, NULL, 1, &xDisplayTaskHandle);
/* Start the scheduler - this function never returns */
vTaskStartScheduler();
/* Execution will only reach here if there is insufficient heap */
for (;;);
}The stack size parameter is in words (4 bytes on 32-bit ARM), not bytes. So a stack size of 512 words means 2048 bytes. Start generous and measure with uxTaskGetStackHighWaterMark() after exercising all code paths.
Task Priorities and Preemption
FreeRTOS uses a preemptive priority-based scheduler. When a higher-priority task becomes Ready, it immediately preempts the currently Running task. Here is a concrete example showing how three tasks at different priorities interact:
/* Priority and Preemption Demo */
/* Priority 3 (highest): Safety check */
void vSafetyTask(void *pv) {
for (;;) {
/* Blocks on semaphore until hardware fault detected */
xSemaphoreTake(xFaultSemaphore, portMAX_DELAY);
/* When fault occurs, this task preempts everything else */
disable_motor();
activate_alarm();
log_fault_code();
/* After handling, loop back and block on semaphore again */
}
}
/* Priority 2 (medium): Motor control at 1kHz */
void vMotorTask(void *pv) {
TickType_t xLastWake = xTaskGetTickCount();
for (;;) {
int16_t speed = read_encoder();
int16_t pwm = pid_calculate(speed, target_speed);
set_motor_pwm(pwm);
vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(1));
}
}
/* Priority 1 (lowest): Status display at 2Hz */
void vStatusTask(void *pv) {
for (;;) {
/* This task only runs when both higher-priority tasks
are Blocked. If vMotorTask runs every 1ms and takes
0.2ms, vStatusTask gets the remaining 0.8ms per cycle. */
display_speed();
display_temperature();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* Timeline example:
t=0ms: vMotorTask runs (0.2ms), then blocks via vTaskDelayUntil
t=0.2ms: vStatusTask runs (it is highest-priority Ready task)
t=1ms: vMotorTask becomes Ready, PREEMPTS vStatusTask
t=1.2ms: vMotorTask blocks, vStatusTask RESUMES where it left off
...
If fault occurs at any time: vSafetyTask PREEMPTS everything */Blocked State Triggers
A task enters the Blocked state when it calls any FreeRTOS API that involves waiting. Here are the most common blocking calls with code examples:
/* Common ways to block a task */
/* 1. Time delay: block for a fixed duration */
vTaskDelay(pdMS_TO_TICKS(100));
/* Task moves to Blocked for 100ms, then automatically
moves to Ready when the delay expires */
/* 2. Periodic delay: block until next period */
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(50));
/* Like vTaskDelay but compensates for execution time,
ensuring exact periodicity */
/* 3. Queue receive: block until data available */
SensorData_t data;
if (xQueueReceive(xQueue, &data, pdMS_TO_TICKS(1000)) == pdPASS) {
/* Data received within 1 second */
process(data);
} else {
/* Timed out after 1 second with no data */
handle_timeout();
}
/* 4. Semaphore take: block until semaphore available */
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(500)) == pdTRUE) {
/* Got the semaphore - access shared resource */
write_to_shared_buffer();
xSemaphoreGive(xMutex);
} else {
/* Failed to get semaphore in 500ms */
log_error("Mutex timeout");
}
/* 5. Task notification: lightweight alternative to semaphore */
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
/* Blocks indefinitely until another task or ISR calls
xTaskNotifyGive(xThisTaskHandle) */
/* 6. Event group: wait for combination of flags */
EventBits_t bits = xEventGroupWaitBits(
xEvents,
BIT_SENSOR_READY | BIT_COMM_READY, /* Wait for both bits */
pdTRUE, /* Clear bits on exit */
pdTRUE, /* Wait for ALL bits (AND), not ANY (OR) */
pdMS_TO_TICKS(2000)
);Suspended State: vTaskSuspend and vTaskResume
The Suspended state is for explicitly pausing a task. Unlike Blocked tasks, suspended tasks have no timeout and will never resume on their own. Common use cases include disabling a feature at runtime, implementing a low-power mode, and debugging by freezing specific tasks.
/* Suspend/Resume example: disable logging during OTA update */
TaskHandle_t xLogTaskHandle;
void vOtaUpdateTask(void *pv) {
for (;;) {
if (ota_update_available()) {
/* Suspend non-critical tasks during update */
vTaskSuspend(xLogTaskHandle);
perform_ota_update();
verify_firmware();
/* Resume after update complete */
vTaskResume(xLogTaskHandle);
}
vTaskDelay(pdMS_TO_TICKS(60000)); /* Check every minute */
}
}
/* Resume from ISR (e.g., button press resumes a suspended task) */
void EXTI0_IRQHandler(void) {
BaseType_t xYieldRequired = pdFALSE;
xYieldRequired = xTaskResumeFromISR(xLogTaskHandle);
portYIELD_FROM_ISR(xYieldRequired);
}Task Deletion and Memory Cleanup
Tasks can be deleted with vTaskDelete(), but this requires careful cleanup. Deleting a task that holds a mutex causes a permanent deadlock. Deleting a task that owns dynamically allocated resources causes memory leaks.
/* Safe task deletion pattern */
void vWorkerTask(void *pvParameters) {
ResourceHandle_t res = acquire_resource();
uint8_t *buffer = pvPortMalloc(256);
for (;;) {
if (should_shutdown()) {
/* MUST clean up before deleting */
release_resource(res);
vPortFree(buffer);
vTaskDelete(NULL); /* NULL = delete self */
/* Code after vTaskDelete never executes */
}
do_work(res, buffer);
vTaskDelay(pdMS_TO_TICKS(100));
}
}In practice, most embedded systems create all tasks at startup and never delete them. Tasks that are not always needed can be suspended with vTaskSuspend() and resumed later. This avoids the complexity and heap fragmentation risks of dynamic task creation and deletion.
Stack Overflow Detection
Stack overflow is the most common cause of mysterious crashes in RTOS firmware. When a task uses more stack than allocated, it corrupts the TCB of adjacent tasks or heap metadata, causing seemingly random failures that are extremely difficult to debug.
FreeRTOS provides two levels of stack overflow detection, enabled by setting configCHECK_FOR_STACK_OVERFLOW in FreeRTOSConfig.h:
/* In FreeRTOSConfig.h */
#define configCHECK_FOR_STACK_OVERFLOW 2 /* Method 2: most thorough */
/* Implement the hook function (called when overflow detected) */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
/* WARNING: stack is already corrupted at this point.
Do minimal work here - no printf, no complex logic */
__disable_irq();
/* Blink an error LED in a tight loop */
while (1) {
toggle_error_led();
for (volatile int i = 0; i < 500000; i++);
}
}
/* Measure stack usage at runtime */
void vMonitorTask(void *pv) {
for (;;) {
UBaseType_t sensor_hwm = uxTaskGetStackHighWaterMark(xSensorTaskHandle);
UBaseType_t comm_hwm = uxTaskGetStackHighWaterMark(xCommTaskHandle);
printf("Stack HWM - Sensor: %lu, Comm: %lu wordsrn",
sensor_hwm, comm_hwm);
/* If high water mark is less than 50 words, stack is too small! */
if (sensor_hwm < 50) {
printf("WARNING: Sensor task stack nearly full!rn");
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}Method 1 checks if the stack pointer has exceeded the stack boundary at each context switch. Method 2 fills the stack with a known pattern (0xA5A5A5A5) at creation and checks if the last 20 bytes have been overwritten. Method 2 catches more overflow scenarios but adds slightly more overhead. Always use method 2 during development.
The Idle Task and Its Purpose
FreeRTOS automatically creates an Idle task at priority 0 (the lowest possible). The Idle task runs whenever no other task is Ready. It performs three critical functions:
- Memory cleanup: When a task is deleted with vTaskDelete(), the Idle task frees the deleted task TCB and stack memory. If the Idle task never runs, deleted task memory is never reclaimed, causing a memory leak.
- CPU utilization measurement: The Idle task is where runtime statistics measure the percentage of time the CPU is idle. If the Idle task runs 70% of the time, your CPU utilization is 30%.
- Low-power mode: The Idle hook (vApplicationIdleHook) is the correct place to enter a low-power sleep mode. When no tasks need the CPU, the Idle task can put the MCU into a sleep state to save power, waking on the next tick interrupt or external event.
/* Idle hook for low-power mode */
void vApplicationIdleHook(void) {
/* Enter sleep mode - wakes on next SysTick or interrupt */
__WFI(); /* ARM Wait For Interrupt instruction */
}If the Idle task never runs (because higher-priority tasks never block), you have a design problem. Either a task is polling in a busy loop instead of blocking, or task priorities are misconfigured. Every task must block at some point to let lower-priority tasks and the Idle task execute.
Common Mistakes
Priority inversion occurs when a high-priority task is blocked waiting for a resource held by a low-priority task, while a medium-priority task preempts the low-priority task. The high-priority task is effectively running at the low-priority task priority. The solution is to use mutexes with priority inheritance (xSemaphoreCreateMutex in FreeRTOS automatically supports this), which temporarily raises the low-priority task priority to match the high-priority waiter.
Starvation happens when a high-priority task never blocks, preventing all lower-priority tasks from running. This violates the fundamental RTOS design rule: every task must block. Even a task that runs a tight control loop should use vTaskDelayUntil() to yield the CPU between iterations. If a task truly cannot block, consider running it as a bare-metal timer ISR instead of an RTOS task.
Stack overflow is the number one cause of mysterious RTOS crashes. Functions with large local arrays, deep recursion, and printf() (which can use 200+ bytes of stack) are the usual culprits. Always enable configCHECK_FOR_STACK_OVERFLOW during development, measure with uxTaskGetStackHighWaterMark(), and size the final stack at 1.5-2x the measured high-water mark.
Using blocking APIs from ISRs: Never call xQueueSend, xSemaphoreTake, or vTaskDelay from an ISR. Always use the FromISR variants (xQueueSendFromISR, xSemaphoreGiveFromISR) and call portYIELD_FROM_ISR() if a higher-priority task was woken. Calling the non-ISR version from an interrupt causes undefined behavior and usually a hard fault.
Context Switching: What Happens Under the Hood
When the scheduler switches from one task to another, it performs a context switch. This involves saving the current task CPU registers, stack pointer, and program counter to its Task Control Block (TCB), then restoring the next task saved state. On ARM Cortex-M, this happens via the PendSV exception handler.
Context switch time depends on the MCU and RTOS. Typical values: ARM Cortex-M3/M4 with FreeRTOS takes 10-20 microseconds, ARM Cortex-M7 takes 5-10 microseconds, and ThreadX on Cortex-M4 takes 5-8 microseconds. If your control loop runs at 10 kHz (100 microsecond period) and the context switch takes 15 microseconds, you lose 15% of your CPU time just to switching. In such cases, consider running the control loop directly in a timer ISR.
Related on this site
- For the broader RTOS picture — concepts, choosing one, kernel internals — see the complete guide to RTOS.
- For the worked FreeRTOS API examples that create, suspend, and delete tasks, see introduction to FreeRTOS.
- Tasks that share resources need synchronisation — mutual exclusion in RTOS covers mutexes, critical sections, and atomic operations.

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.






