Task Blocking State & Task Control Block-Embedded Systems Training in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

← PREV_LECNEXT_LEC →

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.

Pages 261–270RTOS BasicsTCBBlocked StateIdle TaskFree Embedded C Course

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.

Problem: Busy-Wait Round Robin (CPU Wasted)
Task1
spin delay
→
Task2
spin delay
→
Task3
spin delay
→
Task4
spin delay
→
CPU 100%
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.
Task State Transitions
RUNNING
Scheduled by CPU
task_delay(N) →
← delay expired
BLOCKED
Waiting for tick count

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:

Field
uint32_t psp_value
Saved Process Stack Pointer — restored when task resumes
Field
uint32_t block_count
SysTick count at which the task becomes RUNNING again
Field
uint8_t current_state
TASK_RUNNING or TASK_BLOCKED
Field
void (*task_handler)(void)
Pointer to the task’s function

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;
Suggested images
📷 Suggested image: Memory layout diagram showing 5 TCB structs in SRAM with their fields annotated.

3. Implementing task_delay()

task_delay(N) does three things atomically (with interrupts disabled):

  1. Records the wake-up tick: block_count = g_tick_count + N.
  2. Sets the current task’s state to TASK_BLOCKED.
  3. 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");
    }
}
TCB Array Layout (Index 0 = Idle, 1–4 = User Tasks)
Index 0
IDLE
Always RUNNING
Index 1
TASK-1
RUNNING
Index 2
TASK-2
BLOCKED
Index 3
TASK-3
RUNNING
Index 4
TASK-4
BLOCKED

5. Scheduler Decision Logic

The update_next_task() function is called from the SysTick handler. It does two things:

  1. Unblocks any task whose block_count has been reached.
  2. 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:

Scenario A: Task-1 & Task-3 blocked
IDLE (0)
Always RUN
TASK-1 (1)
BLOCKED
TASK-2 (2)
RUNNING ✓
TASK-3 (3)
BLOCKED
TASK-4 (4)
RUNNING ✓

Scheduler alternates between Task-2 and Task-4.

Scenario B: All user tasks blocked → Idle runs
IDLE (0)
RUNNING ← selected
TASK-1 (1)
BLOCKED
TASK-2 (2)
BLOCKED
TASK-3 (3)
BLOCKED
TASK-4 (4)
BLOCKED

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
Correct answer: b) TASK_RUNNING. TASK_RUNNING. The idle task must always be schedulable.

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
Correct answer: c) block_count. block_count stores the tick value at which the task should wake up.

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
Correct answer: c) 500 ms (0.5 seconds). 500 ticks × 1 ms/tick = 500 ms.

EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4

← PREV_LECNEXT_LEC →

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *