MicrOS Internals: How Stack Frames and PendSV Drive Context Switching


One of the most magical parts of an RTOS is when the CPU suddenly stops running one function and starts running another — as if by sleight of hand. In MicrOS, this magic is powered by the PendSV exception and carefully constructed stack frames for each thread.

This post goes under the hood and shows how threads are “bootstrapped” and how PendSV safely swaps them, cycle after cycle.


Hardware’s Role: The Automatic Exception Frame

On Cortex-M, any exception (IRQ, SysTick, PendSV) triggers automatic stacking. The hardware pushes these 8 words onto the current stack pointer (PSP if in thread mode, MSP if in handler mode):

OffsetRegisterNotes
0x00R0Argument register
0x04R1Volatile
0x08R2Volatile
0x0CR3Volatile
0x10R12Scratch
0x14LRLink register
0x18PCResume address
0x1CxPSRProgram status word (must have Thumb bit set)

This is called the exception stack frame. When the exception returns, the CPU automatically pops these values back, restoring execution exactly where it left off.


Creating a Task: Building the Initial Frame

When MicrOS creates a thread, it doesn’t just give it a stack. It pre-loads the stack with a fake exception frame so that when the scheduler runs it for the first time, the CPU “returns” directly into the task function.

For example, in micros_thread_create():

High memory
+--------------------+
|    free stack      |
|        ...         |
+--------------------+
| R11 (dummy)        |
| R10                |
| R9                 |
| R8                 |
| R7                 |
| R6                 |
| R5                 |
| R4                 |  <- SW-saved area
+--------------------+
| R0 = arg           |
| R1 (dummy)         |
| R2 (dummy)         |
| R3 (dummy)         |
| R12 (dummy)        |
| LR = 0xFFFFFFFD    |  <- tells CPU: return to thread, use PSP
| PC = entry()       |  <- task entry point
| xPSR = 0x01000000  |  <- Thumb bit set
+--------------------+
Low memory

The very first exception return the CPU executes lands inside your task function, with the argument already loaded into R0.


Switching Context in PendSV

Now, how do we swap between two such prepared contexts? Enter PendSV_Handler.

MicrOS’s PendSV does:

  1. Save callee-saved registers
    • On exception entry, hardware already pushed R0–R3, R12, LR, PC, xPSR.
    • PendSV saves the remaining R4–R11 onto the PSP.
    • Now the outgoing task’s stack contains a full snapshot of its CPU state.
  2. Call the scheduler in C
    • The saved PSP is passed into micros_context_switch(old_sp).
    • That function updates the TCB (thread->sp = old_sp) and chooses the next runnable thread.
    • It returns the PSP of the new thread.
  3. Restore callee-saved registers
    • PendSV pops R4–R11 from the new thread’s stack.
    • PSP is updated to point to the new thread’s stack frame.
  4. Return from exception
    • BX lr exits PendSV.
    • Hardware automatically pops the rest of the frame (R0–R3, R12, LR, PC, xPSR) from the PSP.
    • The CPU continues execution in the new thread.

Visualizing the Context Switch

Let’s see the stack layouts for Thread A (outgoing) and Thread B (incoming):

Thread A stack before switch (PSP):
   +--------------------+
   | R11                |
   | R10                |
   | ...                |
   | R4                 |  <- saved by PendSV
   +--------------------+
   | R0                 |
   | R1                 |
   | R2                 |
   | R3                 |
   | R12                |
   | LR                 |
   | PC                 |
   | xPSR               |  <- stacked by HW
   +--------------------+
   PSP -> bottom

Thread B stack ready to run (PSP):
   +--------------------+
   | R11                |
   | R10                |
   | ...                |
   | R4                 |  <- prebuilt by create()
   +--------------------+
   | R0 = arg           |
   | R1..R3 dummy       |
   | R12 dummy          |
   | LR = 0xFFFFFFFD    |
   | PC = entry()       |
   | xPSR = 0x01000000  |
   +--------------------+
   PSP -> bottom

PendSV saves A’s R4–R11, calls the scheduler, loads B’s PSP, restores B’s R4–R11, and returns. Hardware pops the rest of B’s frame — and suddenly Thread B is running.


The Subtleties: LR and PSP

Two registers are especially important:

  • LR (EXC_RETURN)
    • Value 0xFFFFFFFD means: return to Thread mode, use PSP, pop registers.
    • If wrong, the CPU may try to restore from MSP or crash with a HardFault.
  • PSP (Process Stack Pointer)
    • Always points to the top of the current thread’s frame.
    • Updated in the TCB during micros_context_switch().

A mismatch here is the #1 reason for “lockup” errors in QEMU.


Why This Works

The beauty of Cortex-M is that the CPU does half the work. Hardware saves and restores volatile registers automatically; the RTOS only needs to manage the callee-saved ones (R4–R11) and PSP bookkeeping.

This is why MicrOS can implement multitasking with just a few dozen lines of C and a small naked handler.


Takeaway

In MicrOS today:

  • Each task is born with a prebuilt exception frame.
  • PendSV saves the old frame, picks a new one, and restores it.
  • LR and PSP hold the key to correct execution flow.

Understanding this mechanism makes context switching less of a black box and more of a predictable, elegant process.


👉 Next post: wiring SysTick + PendSV together for preemptive round-robin scheduling in MicrOS.

Grzegorz Grzęda
Grzegorz Grzęda

At G2Labs, Grzegorz Grzęda develops IoT and embedded platforms while building MicrOS in public and sharing hands-on tutorials.

Articles: 10

Newsletter Updates

Enter your email address below and subscribe to our newsletter