RTOS Preemptive Priority Task Switching in C
This article explains the mechanics of preemptive priority-based context switching within a real-time operating system (RTOS) written in C. It covers how an RTOS detects the need for a switch, evaluates task priorities, preserves CPU state, and resumes execution of a higher-priority thread. By exploring the interaction between high-level C logic and low-level assembly routines, you will understand how microcontrollers transition between tasks deterministically.
Core Data Structures: The TCB and Stack
At the center of an RTOS scheduler is the Task Control Block (TCB).
Implemented as a C struct, the TCB stores the state of a
thread:
typedef struct TCB {
void *stack_ptr; // Current top of stack pointer
uint8_t priority; // Task priority level
struct TCB *next; // Link to next TCB in ready list
/* Additional task metadata */
} TCB_t;Each task possesses its own dedicated stack allocated in RAM. When a
task is executing, the CPU's stack pointer (SP) points somewhere within
that task's private stack area. When a task is idle or preempted, its
stack_ptr field inside its TCB holds the exact address of
its most recently saved CPU register frame.
The Trigger for Preemption
Preemption occurs whenever an event causes a task with a higher
priority than the currently running task to enter the READY
state. This happens primarily through two mechanisms:
- Hardware Timer Interrupt (SysTick): A periodic hardware interrupt fires at fixed intervals (e.g., every 1 ms). The interrupt service routine (ISR) increments the system tick, updates delay counters, and evaluates whether a sleeping high-priority task has awakened.
- Asynchronous Events / API Calls: An ISR or the running task itself posts a semaphore, sends a message to a queue, or yields. If this action unblocks a higher-priority task, a context switch is requested immediately.
The Scheduling Decision in C
When a rescheduling event is triggered, the scheduler identifies the
highest-priority ready thread. Real-time kernels often maintain an array
of ready lists indexed by priority or use a priority bitmap combined
with a hardware instruction like Count Leading Zeros (CLZ)
to achieve \(O(1)\) lookup time:
TCB_t *volatile current_tcb;
TCB_t *volatile next_tcb;
void schedule(void) {
uint8_t highest_priority = get_highest_priority();
next_tcb = ready_list[highest_priority];
if (next_tcb != current_tcb) {
trigger_context_switch();
}
}The function trigger_context_switch() does not
immediately swap registers in pure C. Instead, it asserts a software
interrupt (such as the PendSV interrupt on ARM Cortex-M
processors) to ensure the actual switch occurs at the lowest interrupt
priority, preventing nested interrupt conflicts.
The Mechanics of Context Switching
Because C cannot manipulate core CPU registers directly, the context switch is performed by an assembly wrapper that coordinates with the C scheduler. The switch follows a defined sequence:
- Saving Hardware Context: The CPU enters the
context-switch exception handler. Modern architectures (like ARM
Cortex-M) automatically push a subset of registers (such as
R0-R3,R12,LR,PC, andxPSR) onto the current task's stack. - Saving Software Context: The assembly handler
manually pushes the remaining general-purpose registers (such as
R4-R11) onto the current task's stack. - Updating the Outgoing TCB: The CPU stack pointer is
read and written into
current_tcb->stack_ptr. - Selecting the New Task: The pointer is updated:
current_tcb = next_tcb;. - Loading the Incoming TCB: The CPU stack pointer
register is updated with the address stored in
next_tcb->stack_ptr. - Restoring Software Context: The remaining registers
(
R4-R11) of the new task are popped from its stack. - Exception Return: The assembly handler executes an
exception return instruction. The CPU hardware automatically pops the
rest of the register frame (
R0-R3,R12,LR,PC,xPSR), restoring the Program Counter to the exact instruction where the new task was previously preempted.
The switch completes deterministically, allowing the preempting task to run until it blocks or is preempted by an even higher-priority task.