Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Task Blocking State & Task Control Block
Master task state management, TCB design, the task_delay API, and idle task in a bare-metal ARM Cortex-M round-robin scheduler.
1. Why Tasks Need a Blocked State
In a naive round-robin scheduler every task always runs — even when it has nothing to do. Consider four LED tasks where each needs to blink at different rates. Without blocking, each task would just spin in a software delay loop, wasting CPU time and preventing any real cooperation between tasks.
spin delay
spin delay
spin delay
spin delay
doing nothing useful
The fix is introducing a blocked state. When a task has completed its work for this period it calls task_delay(N) and immediately yields the CPU. The scheduler skips blocked tasks and only runs ready (RUNNING) tasks. This lets other tasks use the CPU during the wait period.
Key Rule: A task should only hold the CPU while it is actively doing work. As soon as it needs to wait — for a timer, a resource, or an event — it must move to the BLOCKED state.
Two-State Task Model
- RUNNING (Ready): Task is eligible to be scheduled on the CPU.
- BLOCKED: Task is waiting for a delay to expire; scheduler skips it.
2. Task Control Block (TCB) Structure
A Task Control Block is a small data structure that stores everything the scheduler needs to know about a task. Previously our TCB only had a stack pointer (psp_value). Now we add two fields to support blocking:
C
#define TASK_RUNNING 0x00U
#define TASK_BLOCKED 0x01U
#define MAX_TASKS 5U /* 4 user tasks + 1 idle task */
typedef struct {
uint32_t psp_value; /* saved stack pointer for this task */
uint32_t block_count; /* wake-up tick — set by task_delay() */
uint8_t current_state; /* TASK_RUNNING or TASK_BLOCKED */
void (*task_handler)(void); /* pointer to task function */
} TCB_t;
/* Global array of task control blocks */
TCB_t user_tasks[MAX_TASKS];
/* Global tick counter — incremented every SysTick ISR */
volatile uint32_t g_tick_count = 0;
3. Implementing task_delay()
task_delay(N) does three things atomically (with interrupts disabled):
- Records the wake-up tick:
block_count = g_tick_count + N. - Sets the current task’s state to
TASK_BLOCKED. - Triggers a PendSV exception to immediately switch to the next ready task.
C – task_delay.c
#include "scheduler.h"
/* Called by a task to block itself for 'tick_count' SysTick periods.
Each period = 1 ms (SysTick configured for 1 ms). */
void task_delay(uint32_t tick_count)
{
/* Disable interrupts while modifying shared scheduler state */
DISABLE_INTERRUPTS();
if (current_task != 0) { /* idle task (index 0) never blocks */
user_tasks[current_task].block_count = g_tick_count + tick_count;
user_tasks[current_task].current_state = TASK_BLOCKED;
schedule(); /* trigger PendSV immediately */
}
ENABLE_INTERRUPTS();
}
Important: The idle task (index 0) must never call task_delay(). If all user tasks are blocked, the idle task must still be available to run. Blocking it would deadlock the system.
Helper Macros for Interrupt Enable/Disable
C – scheduler.h
/* Cortex-M PRIMASK register controls global interrupt masking */
#define DISABLE_INTERRUPTS() __asm volatile ("CPSID i" : : : "memory")
#define ENABLE_INTERRUPTS() __asm volatile ("CPSIE i" : : : "memory")
4. The Idle Task
If all user tasks are blocked simultaneously, the scheduler has no ready task to run. The idle task solves this. It always has state TASK_RUNNING and is assigned index 0 in the TCB array.
Idle Task Contract:
- Runs when all user tasks are blocked.
- Must never block itself.
- Can put the CPU to sleep (
WFI) to save power. - Can perform background housekeeping (e.g., stack overflow checks).
C – idle_task.c
/* Idle task — runs only when all user tasks are blocked.
Uses WFI (Wait For Interrupt) to save power. */
void idle_task(void)
{
while (1) {
/* Put the CPU to sleep until the next interrupt (SysTick) */
__asm volatile("WFI");
}
}
5. Scheduler Decision Logic
The update_next_task() function is called from the SysTick handler. It does two things:
- Unblocks any task whose
block_counthas been reached. - Selects the next RUNNING task in round-robin order (wrapping around).
C – scheduler.c
static uint8_t current_task = 1; /* start with task 1 */
/* Step 1: Check blocked tasks — wake up any that are ready */
void unblock_tasks(void)
{
for (int i = 1; i < MAX_TASKS; i++) { /* skip idle (i=0) */
if (user_tasks[i].current_state == TASK_BLOCKED) {
if (user_tasks[i].block_count == g_tick_count) {
user_tasks[i].current_state = TASK_RUNNING;
}
}
}
}
/* Step 2: Round-robin — find next RUNNING task */
void update_next_task(void)
{
int state = TASK_BLOCKED;
int next = current_task;
for (int i = 0; i < (MAX_TASKS - 1); i++) {
next++;
if (next == MAX_TASKS) next = 1; /* wrap, skip idle */
state = user_tasks[next].current_state;
if (state == TASK_RUNNING) break;
}
if (state != TASK_RUNNING) {
next = 0; /* all user tasks blocked — run idle */
}
current_task = next;
}
/* SysTick ISR */
void SysTick_Handler(void)
{
g_tick_count++;
unblock_tasks();
update_next_task();
/* Pend a PendSV to do the actual context switch */
pend_pendsv();
}
Scheduler State Snapshots
The diagrams below show current_task progression as tasks get blocked:
Scheduler alternates between Task-2 and Task-4.
6. Complete Working Example
Four LED tasks blinking at different rates using task_delay(). All tasks block themselves, and the idle task runs when all four are blocked simultaneously.
C
/* Four LED tasks with different blink periods */
/* LED GPIO bits are on GPIOD pins 12, 13, 14, 15 (STM32F4-DISC1) */
#define LED_GREEN_PIN 12U
#define LED_ORANGE_PIN 13U
#define LED_RED_PIN 14U
#define LED_BLUE_PIN 15U
void task1_handler(void) /* Green LED — 1 Hz */
{
while (1) {
led_toggle(LED_GREEN_PIN);
task_delay(1000); /* block for 1000 ms */
}
}
void task2_handler(void) /* Orange LED — 2 Hz */
{
while (1) {
led_toggle(LED_ORANGE_PIN);
task_delay(500);
}
}
void task3_handler(void) /* Red LED — 4 Hz */
{
while (1) {
led_toggle(LED_RED_PIN);
task_delay(250);
}
}
void task4_handler(void) /* Blue LED — 10 Hz */
{
while (1) {
led_toggle(LED_BLUE_PIN);
task_delay(100);
}
}
C
#define STACK_SIZE 1024U /* bytes per task stack */
/* Private stack memory for each task */
static uint8_t task_stacks[MAX_TASKS][STACK_SIZE];
void scheduler_init(void)
{
/* Task 0 — idle */
user_tasks[0].current_state = TASK_RUNNING;
user_tasks[0].psp_value = init_stack(0, idle_task);
/* Task 1–4 — user tasks */
void (*handlers[4])(void) = {
task1_handler, task2_handler, task3_handler, task4_handler
};
for (int i = 1; i < MAX_TASKS; i++) {
user_tasks[i].current_state = TASK_RUNNING;
user_tasks[i].psp_value = init_stack(i, handlers[i - 1]);
}
}
C
int main(void)
{
enable_processor_faults(); /* enable UsageFault, BusFault, MemManage */
set_stack_msp(); /* set MSP to scheduler stack base */
scheduler_init(); /* initialise all TCBs and stacks */
systick_init(TICK_HZ); /* 1 ms tick */
switch_to_psp(); /* switch from MSP to PSP */
task1_handler(); /* start execution — enters while(1) */
/* never reached */
for(;;);
}
7. Programming Exercises
Exercise 1 — Add a Fifth Task
Add a fifth user task that prints a tick count over SWO every 2 seconds. Verify that the idle task still runs between all sleep periods using a debugger breakpoint.
C – Skeleton
/* TODO: increase MAX_TASKS to 6 */
void task5_handler(void)
{
while(1) {
/* print tick count via ITM/SWO */
ITM_SendValue(0, g_tick_count);
task_delay(2000); /* block for 2 seconds */
}
}
Exercise 2 — Implement a Countdown Timer
Write a function get_remaining_ticks(uint8_t task_id) that returns how many ticks a blocked task still has to wait. Return 0 if the task is already running.
C – Skeleton
uint32_t get_remaining_ticks(uint8_t task_id)
{
if (user_tasks[task_id].current_state == TASK_RUNNING)
return 0;
/* TODO: compute remaining ticks */
return user_tasks[task_id].block_count - g_tick_count;
}
Exercise 3 — Stack Overflow Detection in Idle Task
Use the idle task to check every task’s stack usage. If the stack pointer of any task goes below a “watermark” address, call a fault handler.
8. Frequently Asked Questions
Why does task_delay() trigger PendSV immediately instead of waiting for the next SysTick?
Triggering PendSV immediately yields the CPU right away. If we waited for the next SysTick, the blocked task would continue executing (burning time it said it doesn’t need) until the next 1 ms tick fires. Immediate yield makes blocking tasks as efficient as possible.
What happens if block_count overflows (wraps around)?
With a 32-bit counter at 1 ms per tick, overflow takes ~49.7 days. For longer-running systems the comparison user_tasks[i].block_count == g_tick_count should be changed to g_tick_count >= user_tasks[i].block_count using unsigned arithmetic, which handles the wrap-around correctly.
Can I call task_delay() from an interrupt handler?
No. task_delay() operates on current_task, which refers to the task that was running before the interrupt. Calling it from an ISR would block the wrong task and corrupt scheduling state. If you need delay-like behavior from an ISR, set a flag and let the task handle the delay in its own context.
How is this different from a real RTOS like FreeRTOS?
This is a simplified educational scheduler. FreeRTOS adds priority levels, semaphores, mutexes, message queues, dynamic task creation/deletion, and a much more sophisticated scheduler. But the core ideas — TCB, blocked/ready states, context switch via PendSV — are identical. Understanding this bare-metal version makes FreeRTOS internals intuitive.
Why is DISABLE_INTERRUPTS() needed in task_delay()?
Without interrupt disabling, a SysTick could fire between writing block_count and setting current_state = TASK_BLOCKED. The scheduler would then see an inconsistent TCB — the count is set but state hasn’t changed yet — and the task might run one extra tick or never unblock correctly.
Quick Knowledge Check
1. What state does the Idle Task always maintain?
- a) TASK_BLOCKED
- b) TASK_RUNNING
- c) TASK_SLEEPING
- d) TASK_WAITING
Check Answer
2. Which field in the TCB determines when a blocked task becomes RUNNING again?
- a) psp_value
- b) task_handler
- c) block_count
- d) current_state
Check Answer
3. task_delay(500) blocks a task for how long if SysTick fires every 1 ms?
- a) 5 ms
- b) 50 ms
- c) 500 ms (0.5 seconds)
- d) 5000 ms
Check Answer
EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments