Embedded

Context switching in an RTOS

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: A context switch is the mechanism by which an RTOS freezes one task's complete CPU state and reloads another's — on Arm Cortex-M it is deliberately deferred to the lowest-priority PendSV exception so interrupts stay responsive, and each switch costs roughly 80–200 CPU cycles depending on core and FPU state.

    Ask any embedded firmware engineer to explain how a real-time operating system runs ten tasks on one CPU core and the honest answer is: it doesn't run them simultaneously — it switches between them so fast that each appears to own the processor. That sleight of hand is the context switch, and understanding its exact cycle-by-cycle behaviour separates engineers who merely use FreeRTOS or Zephyr from those who can debug a priority inversion at 3 a.m. This article walks through what the "context" actually is, why Cortex-M hands the job to PendSV, how the tick timer drives preemption, and what it all costs.

    What exactly is a "context"?

    A task's context is everything the CPU needs to resume it as if it had never been interrupted: the general-purpose registers (R0–R12), the stack pointer, the link register (R14), the program counter (R15), and the program status register (xPSR). On an Arm Cortex-M core, hardware already saves eight of these — R0–R3, R12, LR, PC, xPSR — automatically onto the current stack when any exception is taken. This hardware-stacked frame is why the software context switcher only has to manually push the remaining eight registers (R4–R11).

    Key fact: Each RTOS task keeps its own stack, and the stack pointer lives in the Task Control Block (TCB). Saving a context is therefore mostly about pushing R4–R11 onto the outgoing task's stack and recording the final SP into its TCB; restoring is the mirror image.

    Context switch: who saves what Task A (running) R0–R3, R12, LR, PC, xPSRHW auto-stacked R4–R11SW pushed by PendSVSP saved to TCB_A PendSV Task B (next) SP loaded from TCB_Bstack pointer restored pop R4–R11SW, inside PendSVHW unstacks the rest, B resumes CourseTron · coursetron.com
    Half the frame is stacked by hardware; the scheduler's job is only to move the remaining registers and swap stack pointers.

    Why PendSV? The clever part

    You could, in principle, context-switch directly inside the SysTick timer interrupt. In practice that is a trap. If a higher-priority peripheral interrupt — say a UART or CAN handler — is pending while SysTick runs, performing a long context switch there would delay that interrupt and wreck determinism. Arm's solution is PendSV (Pendable Supervisor Call), an exception that firmware configures at the lowest priority.

    The pattern used by FreeRTOS, Zephyr, and ThreadX is identical: the SysTick ISR or a blocking API decides a switch is needed and simply sets the PendSV pending bit (writing to ICSR). PendSV then fires only after every higher-priority ISR has drained. This guarantees the actual register shuffling happens at a moment when no urgent interrupt is waiting — the switch never steals latency from real I/O.

    Key fact: On Cortex-M, EXC_RETURN's bit 4 tells the core whether an FPU context was stacked. If any task uses the hardware FPU, the switch must also preserve S16–S31 — adding ~17 extra registers and noticeably lengthening the switch.

    The tick timer: the heartbeat of preemption

    Time-sliced, preemptive scheduling needs a periodic interrupt. That is the tick timer, almost always SysTick on Cortex-M, configured for 1 ms (1000 Hz) or 10 ms periods. On every tick the kernel increments its tick count, decrements delay timers for sleeping tasks (vTaskDelay), and checks whether a higher-priority task has become ready. If so, it pends PendSV. See our deeper walkthrough in FreeRTOS tasks and scheduling for how ready lists are organised per priority.

    The tick frequency is a genuine design trade-off. A 1 ms tick gives fine timing granularity but means 1000 interrupts per second, each costing scheduler overhead even when nothing changes. This is why "tickless idle" modes exist — the kernel stops the periodic tick during long idle periods and reprograms the timer to wake only when the next task is actually due, saving substantial power on battery devices. Our note on tickless idle and low-power RTOS design covers the maths.

    SysTick tick leads to a PendSV switchTick 3: Task High becomes ready, PendSV is pended Task Low running Task High runningPendSV Orange lines below the bars: SysTick ticks, 1 ms apart CourseTron · coursetron.com
    The marker rides along until a tick marks a higher-priority task ready; PendSV then switches the CPU over.

    Scheduler overhead: counting the cost

    Overhead is the time spent not doing useful work — the tick ISR plus the PendSV switch itself. The table below gives realistic figures for common cores; these are the kinds of numbers you confirm with a logic analyser toggling a GPIO around the switch, or with a cycle-accurate simulator such as those in Arm's Fast Models or a vendor instruction-set simulator.

    CoreSwitch (no FPU)Switch (with FPU)At 100 MHz
    Cortex-M0+~84 cyclesn/a~0.84 µs
    Cortex-M4F~95 cycles~170 cycles~1.7 µs
    Cortex-M7~110 cycles~190 cycles~1.9 µs

    At a 1 kHz tick, a 1.7 µs switch plus a ~1 µs tick ISR consumes well under 0.3% of CPU time — negligible for most designs. The danger is not the per-switch cost but switch frequency: a badly designed system where two tasks ping-pong via a semaphore thousands of times per millisecond can burn real percentages. This is why profiling tools (Percepio Tracealyzer, SEGGER SystemView) visualise switch density, not just totals.

    A worked example of the switch

    Imagine a 115200-baud UART driver task blocked on a queue. A byte arrives, the UART ISR posts to the queue and sets PendSV pending. The ISR returns; PendSV fires; the scheduler pushes R4–R11 of the idle task, writes its SP to its TCB, loads the UART task's SP from its TCB, pops its R4–R11, and executes BX LR with EXC_RETURN. Hardware unstacks the rest and the UART task resumes exactly after its blocking xQueueReceive — with the received byte already in a register. The whole handover is under 2 µs, imperceptible against a 86 µs byte time.

    Worked example: UART byte wakes a blocked taskNot to scale: one byte at 115200 baud lasts about 86 µs Idle task ISR PendSV UART task resumes 123 Byte arrives: ISR posts to the queue and pends PendSVPendSV swaps R4–R11 and SP through the two TCBs (under 2 µs)Hardware unstacks, task continues after xQueueReceive CourseTron · coursetron.com
    Timeline of the UART example: the ISR pends PendSV, which hands the CPU to the UART task in under 2 µs.

    Want to write and debug this kind of code on real silicon? CourseTron's Embedded & RTOS track takes you from bare-metal startup to a preemptive scheduler you build yourself. For hands-on VLSI and chip-design training that complements embedded work, our partner ChipXpert VLSI training is a great next step.

    Common pitfalls engineers hit

    • Setting PendSV at high priority — destroys interrupt latency; always lowest.
    • Forgetting FPU lazy stacking — mixing FPU and non-FPU tasks without enabling lazy stacking corrupts float state.
    • Calling blocking APIs from an ISR — use the ...FromISR variants, which only pend a switch rather than performing one.
    • Undersized task stacks — the manually saved registers plus local variables can overflow a stack sized only for the hardware frame.

    FAQ

    Is a context switch the same as an interrupt?

    No. An interrupt is a hardware event that preempts the CPU; a context switch is the software act of saving one task and restoring another. An interrupt may trigger a switch, but handling an ISR that returns to the same task is not a context switch.

    Why not switch inside SysTick directly?

    Because SysTick runs at a relatively high exception priority. Doing the full register save/restore there would block any pending higher-priority peripheral interrupt. Deferring to low-priority PendSV keeps real I/O latency bounded.

    How many cycles does a FreeRTOS context switch take?

    Typically 80–200 CPU cycles on Cortex-M, depending on core, memory wait states, and whether FPU registers must be saved. Measure your own build with a GPIO toggle and a logic analyser rather than trusting a datasheet number.

    What is tickless idle?

    A power-saving mode where the kernel suppresses the periodic tick during idle and reprograms the timer to interrupt only when the next task is due, eliminating thousands of needless wake-ups on battery-powered devices.

    Advertisement