RTOS Kernel Internals: Tasks, Scheduler & Context Switch
Look inside a real-time kernel. Learn what a task is (TCB + stack), task states, the tick and tickless idle, priority-based preemptive scheduling with ready lists, how a Cortex-M context switch works (SysTick, PendSV, PSP/MSP, EXC_RETURN), stack sizing per task, kernel-aware interrupt priorities, and the overheads you pay for an RTOS.
An RTOS makes one CPU look like many: each task believes it owns the processor. The illusion is created by the scheduler, which saves one task's registers and restores another's many times per second — in well under a microsecond.
Knowing the internals turns mysterious RTOS bugs into understandable ones: a task that never runs (priority starvation), random crashes (stack overflow into another task), ISRs that break the kernel (wrong priority for FreeRTOS API calls), or a high idle current (tick waking the CPU).
An analogy
A kernel is a juggler: each ball is a task. The juggler (scheduler) only ever holds one ball at a time (running), others are in the air (ready) or set aside on a table (blocked). The hand-off must be fast and precise, or balls drop.
Tasks and states
A task is a function with its own stack and a TCB. It is Running (on the CPU), Ready (could run, waiting for the CPU), Blocked (waiting for time, a queue, semaphore or event) or Suspended (explicitly paused). Blocking is what makes an RTOS efficient: a task waiting for data uses zero CPU.
The scheduler
Most RTOSes use fixed-priority preemptive scheduling: the highest-priority Ready task always runs; equal priorities share the CPU by time slicing each tick. The kernel keeps a ready list per priority and often a bitmap so finding the highest priority is a single CLZ instruction — O(1) regardless of task count.
- Reschedule points: tick, task blocks/unblocks, ISR gives a semaphore, priority change
- Idle task runs when nothing else can — ideal place for sleep modes
Context switch on Cortex-M
Tasks run in Thread mode on the PSP; the kernel and ISRs use the MSP. When the scheduler decides to switch, it pends PendSV. When no other ISR is active, PendSV saves R4–R11 to the current task's stack, stores the SP in its TCB, selects the next task, restores its registers and returns — hardware pops the rest. SVC starts the first task.
Kernel-aware interrupts, tickless idle and overheads
FreeRTOS masks interrupts up to configMAX_SYSCALL_INTERRUPT_PRIORITY during critical sections; ISRs that call FromISR APIs must have that priority or lower urgency (numerically ≥), while higher-priority ISRs run with zero kernel latency but must not call the kernel. Tickless idle stops the periodic tick during long idle periods and programs a low-power timer to wake exactly when the next task is due. Costs of an RTOS: ~5–10 KB Flash, TCB + stack per task (RAM), context-switch time (~1 µs), and non-determinism if priorities are designed badly.
Formulas
- Saved context (Cortex-M4, no FPU):
8 words by hardware + 8 words (R4–R11) by software = 64 B - With FPU context:
+18 words hardware (S0–S15, FPSCR, pad) + 16 words (S16–S31) = +136 B - Tick overhead:
Load = f_tick × C_tick / f_CPU (1 kHz × 200 cycles @ 168 MHz = 0.12 %) - Task stack size:
Stack ≥ context frame + deepest call chain + locals + ISR nesting (if on PSP) + margin - Tick resolution:
vTaskDelay(n) waits between (n − 1) and n ticks
Worked example: Tick overhead
Given: 1 kHz tick, 200 cycles per tick ISR, 168 MHz
Find: CPU load.
- 1000 × 200 / 168×10⁶
Answer: 0.12% — negligible for CPU, but it wakes the core 1000×/s (bad for low power)
Worked example: Stack sizing
Given: Context 64 B; Deepest calls 520 B incl. locals; printf-style logging 400 B
Find: Task stack with 30% margin.
- 64 + 520 + 400 = 984 B
- × 1.3 = 1279 B
Answer: ≈ 1.3 KB → allocate 1.5 KB (384 words)
Worked example: Delay granularity
Given: Tick 1 kHz, vTaskDelay(1)
Find: Actual delay.
- Task may block just before the next tick
Answer: Anywhere from ~0 to 1 ms — use vTaskDelayUntil or hardware timers for precise timing
PendSV context switch (Cortex-M4, no FPU — simplified FreeRTOS-style)
.syntax unified
.thumb
.global PendSV_Handler
.extern current_tcb, scheduler_select_next
PendSV_Handler:
mrs r0, psp @ r0 = current task's stack pointer
stmdb r0!, {r4-r11} @ push callee-saved regs (HW already pushed r0-r3,r12,lr,pc,xpsr)
ldr r1, =current_tcb
ldr r2, [r1]
str r0, [r2] @ save SP into current TCB (first field)
push {r1, lr}
bl scheduler_select_next @ C: current_tcb = highest-priority ready task
pop {r1, lr}
ldr r2, [r1]
ldr r0, [r2] @ load new task's saved SP
ldmia r0!, {r4-r11} @ restore its callee-saved regs
msr psp, r0
bx lr @ EXC_RETURN: hardware unstacks r0-r3,... on PSPHardware saves half the registers on exception entry; PendSV saves the other half, swaps stack pointers between TCBs and returns. PendSV has the lowest priority so switches happen only after all ISRs finish.
Detecting stack overflow and measuring headroom (FreeRTOS)
/* FreeRTOSConfig.h */
#define configCHECK_FOR_STACK_OVERFLOW 2 /* check guard pattern on switch */
void vApplicationStackOverflowHook(TaskHandle_t t, char *name)
{
log_fatal("stack overflow in %s", name); /* then reset safely */
NVIC_SystemReset();
}
void monitor_task(void *arg)
{
for (;;) {
UBaseType_t words = uxTaskGetStackHighWaterMark(sensor_task_handle);
log_info("sensor task free stack: %u bytes", (unsigned)(words * 4));
vTaskDelay(pdMS_TO_TICKS(5000));
}
}Stacks are painted with a pattern at creation; the high-water mark is the least free space ever observed. Size stacks from measurements under worst-case load, then add margin.
Common mistakes
- Wrong: Calling non-FromISR kernel APIs inside ISRs. — Right: Use xQueueSendFromISR etc. and portYIELD_FROM_ISR.
- Wrong: ISR priority above configMAX_SYSCALL_INTERRUPT_PRIORITY calling FreeRTOS. — Right: Kernel-calling ISRs must be at or below that urgency.
- Wrong: Busy-wait loops in tasks. — Right: Block on delays or events so lower priorities can run.
- Wrong: Guessing stack sizes. — Right: Measure high-water marks; enable overflow checking.
Applications
- IoT devices: Network, sensor and UI tasks on FreeRTOS/Zephyr.
- Automotive ECUs: OSEK/AUTOSAR OS with static task configuration.
- Medical devices: Certified kernels (SafeRTOS, embOS-Safe).
- Drones & robotics: Priority tasks for control, estimation and comms.
Key takeaways
- Task = TCB + stack; states Ready/Running/Blocked/Suspended.
- Highest-priority Ready task runs; blocking frees the CPU.
- PendSV saves R4–R11 and swaps PSP.
- Measure stacks; respect kernel interrupt priorities.
Going further
- O(1) scheduling: Priority bitmaps + CLZ instruction.
- MPU-protected tasks: FreeRTOS-MPU / Zephyr user mode isolate task memory.
- SMP RTOS: Multi-core scheduling on dual Cortex-M/R devices.