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):
| Offset | Register | Notes |
|---|---|---|
| 0x00 | R0 | Argument register |
| 0x04 | R1 | Volatile |
| 0x08 | R2 | Volatile |
| 0x0C | R3 | Volatile |
| 0x10 | R12 | Scratch |
| 0x14 | LR | Link register |
| 0x18 | PC | Resume address |
| 0x1C | xPSR | Program 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:
- Save callee-saved registers
- On exception entry, hardware already pushed
R0–R3, R12, LR, PC, xPSR. - PendSV saves the remaining
R4–R11onto the PSP. - Now the outgoing task’s stack contains a full snapshot of its CPU state.
- On exception entry, hardware already pushed
- 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.
- The saved PSP is passed into
- Restore callee-saved registers
- PendSV pops
R4–R11from the new thread’s stack. - PSP is updated to point to the new thread’s stack frame.
- PendSV pops
- Return from exception
BX lrexits 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
0xFFFFFFFDmeans: return to Thread mode, use PSP, pop registers. - If wrong, the CPU may try to restore from MSP or crash with a HardFault.
- Value
- 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.