Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Task State, Stacking & Unstacking on ARM Cortex-M3/M4
Exactly what registers form a task’s execution context, how the hardware automatically saves and restores them on exception entry and exit, and the precise step-by-step procedure of a context switch.
1 — What Is a Task’s Execution Context (State)?
Imagine you are reading a book, then someone pauses you mid-sentence and asks another person to continue reading. For the second person to pick up exactly where you left off, they would need to know your page number, the word you were on, and every thought you were holding in your head. In an embedded processor, that information is the execution context.
For a task running on an ARM Cortex-M processor, the execution context is the complete snapshot of the CPU register file at the moment the task is interrupted. If you save all those registers somewhere and reload them later, the task resumes without any idea it was ever paused.
Formal definition: The execution context (or state) of a task is the set of CPU register values — general-purpose registers, status register, and key special registers — that completely describe what the processor was doing on behalf of that task at a given instant.
🔢
General Purpose Registers
r0 — r12
Hold operands, intermediate results, function arguments and return values during task execution. Every task has a unique set of values in these registers.
🏠
Special Registers
PC, LR, SP
The Program Counter (where we are), Link Register (where to return), and Stack Pointer (which stack frame is active). Losing these means losing the task’s position entirely.
🏗
Status Register
xPSR (APSR / IPSR / EPSR)
Holds condition flags (N, Z, C, V, Q), exception number, and the Thumb state bit. A comparison instruction sets these flags; the next branch instruction reads them.
2 — Which CPU Registers Form the Task State
The ARM Cortex-M4 processor core contains the following register groups. All of them together represent the full execution context of a task:
Complete Cortex-M4 register file — coloured by who saves which group during a context switch
The 16-Register Task Context at a Glance
| Register(s) | Role during Task Execution | Saved By | Where Stored |
|---|---|---|---|
r0 – r3, r12 | Function arguments, return value, scratch registers. Frequently change with every function call. | Hardware (automatic) | Task PSP stack — exception frame |
r4 – r11 | Callee-saved variables. Hold loop counters, pointers, long-lived values across function calls. | PendSV handler (manual) | Task PSP stack — below exception frame |
LR (r14) | Return address from the last function call, or EXC_RETURN on exception entry. | Hardware (automatic) | Task PSP stack — exception frame |
PC (r15) | Address of the next instruction to execute — exactly where the task will resume. | Hardware (automatic) | Task PSP stack — exception frame |
xPSR | Condition flags (N/Z/C/V), current exception number, Thumb state bit (must be 1). | Hardware (automatic) | Task PSP stack — exception frame |
PSP value | The process stack pointer of this task — points to the top of the saved context. | PendSV + TCB | Task Control Block (TCB) in RAM |
What about the FPU registers (s0–s31)? If your task uses floating-point operations, the Cortex-M4 FPU adds an extended frame (s0–s15 + FPSCR) to the hardware save. For simplicity this tutorial uses -mfloat-abi=softfp or compiles without FPU, keeping the frame at the standard 8 registers. In production RTOS code, check the FPCCR register to determine whether FPU context saving is needed.
3 — Step-by-Step Context Switch Procedure
A context switch has two halves: saving the outgoing task (T1) and restoring the incoming task (T2). Here is exactly what happens in order, from the moment SysTick fires to the moment T2’s first instruction runs.
MRS r0, PSP). This points to the top of T1’s stack.STMDB r0!, {r4–r11}).Switch
Handler
LDMIA r0!, {r4–r11}), restoring T2’s private variables.MSR PSP, r0), so it now points to T2’s exception frame.BX LR. The CPU sees EXC_RETURN = 0xFFFFFFFD and pops r0–r3, r12, LR, PC, xPSR from T2’s stack.Complete T1→T2 context switch: 5 steps to save, 5 steps to restore. All happen inside PendSV_Handler.
Why split between hardware and software? The hardware auto-save (steps 1 & 9) happens with zero PendSV code — the CPU does it on exception entry/exit. This means the PendSV handler only needs to deal with 8 registers (r4–r11) instead of all 16, making context switches faster and the assembly code simpler.
4 — Hardware Stacking: What Happens on Exception Entry
Every time the Cortex-M processor takes any exception (SysTick, PendSV, IRQ, SVC, etc.), it performs an automatic stacking operation before running the handler. This is the hardware’s contribution to context saving — you get it for free.
- Exception recognised The processor finishes the current instruction (or abandons it cleanly) and decides to take the exception.
- Stack pointer selected If Thread mode was active with SPSEL=1, the PSP is used as the stack for the frame. If Thread mode used MSP, or we were already in Handler mode, the MSP is used.
- Eight registers pushed — in order The CPU pushes:
r0, r1, r2, r3, r12, LR, PC, xPSR. The stack grows downward; SP decrements by 32 bytes (8 × 4 bytes). - LR loaded with EXC_RETURN LR is overwritten with the EXC_RETURN value (e.g. 0xFFFFFFFD). This encodes the information needed to undo the stacking when the handler returns.
- PC loaded with handler address The CPU jumps to the exception handler (SysTick_Handler, PendSV_Handler, etc.). The handler now runs in Handler mode using MSP.
Stack State During Exception Handler
Hardware stacking pushes 8 registers in a fixed order. PSP decrements by 32 bytes. The handler runs on MSP (not PSP). The original PSP value is unchanged — it still points to the top of the pushed frame.
Stack alignment: If the pre-exception SP was not 8-byte aligned, the hardware adds a padding word and sets bit 9 of the pushed xPSR to record this. On unstacking it removes the padding automatically. Always align your stack tops to 8 bytes to avoid this edge case.
5 — Hardware Unstacking: What Happens on Exception Exit
When an exception handler executes a BX LR instruction and LR holds an EXC_RETURN value (a value with bits[31:5] all set to 1, e.g. 0xFFFFFFFD), the processor does not jump to a function — it performs exception return, which includes automatic unstacking.
- CPU detects EXC_RETURN The
BX LRat the end of the handler passes 0xFFFFFFFD to the PC. The processor recognises the EXC_RETURN pattern and begins exception return. - Stack pointer selected from EXC_RETURN Bit 2 of EXC_RETURN says which stack to unstack from. 0xFFFFFFFD (bit 2 = 1) → use PSP. 0xFFFFFFF9 (bit 2 = 0) → use MSP.
- Eight registers popped — in reverse order The CPU pops
xPSR, PC, LR, r12, r3, r2, r1, r0from the selected stack. SP increments by 32 bytes. - Execution resumes at restored PC The PC register now holds the address that was saved when the task was interrupted. The task continues from that exact instruction with all its register values intact.
- Mode switches back to Thread mode The processor exits Handler mode and returns to Thread mode using PSP (because EXC_RETURN = 0xFFFFFFFD specifies this).
r0–r3, r12, LR, PC, xPSR all restored.
PC points to T2’s interrupted instruction.
T2 has no idea it was ever paused.
Hardware unstacking on EXC_RETURN: pops 8 registers from T2’s PSP stack, advances SP by 32 bytes, resumes T2 at the saved PC.
6 — The Full Stack Frame Picture
After PendSV has pushed r4–r11 and the hardware has pushed the exception frame, the complete layout of a suspended task’s stack looks like this:
Pushed by PendSV assembly before saving PSP to TCB
Pushed by CPU on exception entry, popped on EXC_RETURN
Complete CPU state for this task
Complete 16-register (64-byte) context of a suspended task. PSP in TCB points to the top of the manual frame (r4).
7 — Implementing Context Save & Restore
Now that you understand exactly what must be saved and where, here is the complete PendSV context switch implementation alongside a C-side helper to verify the saved frame.
PendSV Handler (ASM)
.syntax unified
.thumb
/* C functions to get/set PSP values stored in the TCB array */
.extern get_current_psp @ uint32_t get_current_psp(void)
.extern save_current_psp @ void save_current_psp(uint32_t psp)
.extern get_next_psp @ uint32_t get_next_psp(void)
.extern advance_task @ void advance_task(void)
.global PendSV_Handler
.type PendSV_Handler, %function
PendSV_Handler:
@ ════════════════════════════════════════════════════════
@ PHASE 1: SAVE OUTGOING TASK CONTEXT
@ At this point hardware has ALREADY pushed:
@ xPSR, PC, LR, r12, r3, r2, r1, r0 onto PSP
@ We only need to push r4-r11 manually.
@ ════════════════════════════════════════════════════════
mrs r0, psp @ r0 = current PSP value of outgoing task
isb @ instruction sync barrier (good practice)
stmdb r0!, {r4-r11} @ push r4-r11 BELOW the hardware frame
@ STMDB = store multiple, decrement before
@ r0 is decremented as registers are stored
@ r11 is stored first (highest addr), r4 last (lowest)
@ r0 now points to the TOP of the complete saved frame (where r4 lives)
@ Save this value into the TCB
push {lr} @ preserve EXC_RETURN — bl will overwrite LR
bl save_current_psp @ save_current_psp(r0) — stores r0 in TCB
@ Advance the scheduler round-robin index
bl advance_task @ current_task = (current_task + 1) % TASK_COUNT
pop {lr} @ restore EXC_RETURN
@ ════════════════════════════════════════════════════════
@ PHASE 2: RESTORE INCOMING TASK CONTEXT
@ ════════════════════════════════════════════════════════
push {lr}
bl get_next_psp @ r0 = next task's saved PSP (top of its context)
pop {lr}
ldmia r0!, {r4-r11} @ pop r4-r11 from incoming task's stack
@ LDMIA = load multiple, increment after
@ r4 loaded first (lowest addr), r11 last
@ r0 is incremented — now points to hardware frame
msr psp, r0 @ update PSP: CPU will unstack hardware frame from here
isb
@ EXC_RETURN in LR = 0xFFFFFFFD:
@ → Return to Thread mode
@ → Use PSP
@ → No FP extension
@ CPU will now automatically pop r0,r1,r2,r3,r12,LR,PC,xPSR from PSP
bx lrStack Inspection (C)
#include <stdint.h>
#include <stdio.h>
#include "scheduler.h"
/*
* Layout of a fully saved task stack frame.
* The PSP stored in the TCB points to the START of this struct
* (i.e. to the r4 field).
*
* Stack grows downward, so r4 is at the lowest address.
*/
typedef struct {
/* Manually pushed by PendSV (lower addresses = pushed last) */
uint32_t r4, r5, r6, r7, r8, r9, r10, r11;
/* Auto-pushed by hardware on exception entry (higher addresses) */
uint32_t r0, r1, r2, r3, r12;
uint32_t lr; /* LR at time of preemption (task's return address) */
uint32_t pc; /* PC — where the task will resume */
uint32_t xpsr; /* Program status — Thumb bit must be 1 */
} task_frame_t;
/* Print the saved register values for a given task */
void dump_task_frame(uint32_t saved_psp, uint8_t task_id)
{
task_frame_t *frame = (task_frame_t *)saved_psp;
printf("\n=== Task %d saved context ===\n", task_id);
/* Manually saved */
printf(" r4 = 0x%08lX\n", frame->r4);
printf(" r5 = 0x%08lX\n", frame->r5);
printf(" r6 = 0x%08lX\n", frame->r6);
printf(" r7 = 0x%08lX\n", frame->r7);
printf(" r8 = 0x%08lX\n", frame->r8);
printf(" r9 = 0x%08lX\n", frame->r9);
printf(" r10 = 0x%08lX\n", frame->r10);
printf(" r11 = 0x%08lX\n", frame->r11);
/* Hardware saved */
printf(" r0 = 0x%08lX\n", frame->r0);
printf(" r1 = 0x%08lX\n", frame->r1);
printf(" r2 = 0x%08lX\n", frame->r2);
printf(" r3 = 0x%08lX\n", frame->r3);
printf(" r12 = 0x%08lX\n", frame->r12);
printf(" LR = 0x%08lX\n", frame->lr);
printf(" PC = 0x%08lX <-- task resumes here\n", frame->pc);
printf(" PSR = 0x%08lX (Thumb bit: %s)\n",
frame->xpsr,
(frame->xpsr & (1U << 24)) ? "SET (OK)" : "CLEAR (FATAL!)");
}
/* Call from a debug task or fault handler to inspect all tasks */
void dump_all_tasks(void)
{
for (int i = 0; i < TASK_COUNT; i++) {
dump_task_frame(get_task_psp(i), i + 1);
}
}Output & Verification
Expected Output from dump_task_frame()
// Called after first SysTick fires — Task 1 has been suspended once
=== Task 1 saved context ===
r4 = 0x00000004 <– init value from fake frame
r5 = 0x00000005
r6 = 0x00000006
r7 = 0x00000007
r8 = 0x00000008
r9 = 0x00000009
r10 = 0x0000000A
r11 = 0x0000000B
r0 = 0x00000000
r1 = 0x00000000
r2 = 0x00000000
r3 = 0x00000000
r12 = 0x00000000
LR = 0xFFFFFFFD <– EXC_RETURN
PC = 0x080001A4 <– task1_handler entry addr
PSR = 0x01000000 (Thumb bit: SET (OK))
What to verify:
- PC should point to a valid flash address inside your task function
- xPSR bit 24 must be 1 (Thumb bit SET) — if CLEAR you will get UsageFault on resume
- LR = 0xFFFFFFFD confirms Thread mode + PSP return path
- After real execution (not fake frame), r4–r11 will show actual task variable values
🧠 Quick Knowledge Check
1. What exactly is the “execution context” of a task on ARM Cortex-M?
- The C source code of the task function
- The complete set of CPU register values at the moment the task is interrupted
- The task’s local variables stored in global RAM
- The NVIC priority configuration for the task
Check Answer
Correct answer: B — The complete set of CPU register values at the moment the task is interrupted
The execution context is the complete CPU register snapshot: r0-r12, LR, PC, xPSR, and the PSP value. Saving and restoring these is all that is needed to suspend and resume a task.
2. Which registers does Cortex-M hardware push automatically on exception entry?
- r4, r5, r6, r7, r8, r9, r10, r11
- r0, r1, r2, r3, r12, LR, PC, xPSR
- All 16 general purpose registers
- Only PC and xPSR
Check Answer
Correct answer: B — r0, r1, r2, r3, r12, LR, PC, xPSR
The hardware exception frame contains r0, r1, r2, r3, r12, LR, PC, xPSR — 8 registers, 32 bytes. r4-r11 must be saved manually by the PendSV handler.
3. After PendSV pushes r4–r11 and saves the PSP to the TCB, what does the saved PSP value point to?
- The top of the hardware exception frame (r0 slot)
- The top of the manually saved frame (r4 slot)
- The bottom of the task stack
- The task entry function address
Check Answer
Correct answer: B — The top of the manually saved frame (r4 slot)
After STMDB r0!, {r4-r11}, r0 points to the r4 slot — the very top of the complete saved context. The hardware frame lives above it (at higher addresses).
4. What triggers hardware unstacking (automatic register restore)?
- A call to
__enable_irq() - A
POP {pc}instruction in the handler - A
BX LRwhen LR holds an EXC_RETURN value (e.g. 0xFFFFFFFD) - Writing to the SCB->ICSR register
Check Answer
Correct answer: C — A BX LR when LR holds an EXC_RETURN value (e.g. 0xFFFFFFFD)
When BX LR is executed and LR contains an EXC_RETURN value (bits[31:5] all set), the CPU performs exception return which includes automatic unstacking from the selected stack.
5. In the STMDB instruction used to save r4–r11, what does “DB” stand for and what does it mean?
- Double Byte — stores 16-bit values
- Decrement Before — decrements the address register before storing each word
- Data Block — stores a fixed-size data block
- Debug Breakpoint — inserts a debug marker
Check Answer
Correct answer: B — Decrement Before — decrements the address register before storing each word
STMDB = Store Multiple Decrement Before. The address register (r0) is decremented before each store. This is the standard push operation — it makes the stack grow downward.
6. How many bytes does a complete context save consume on the task stack (no FPU)?
- 32 bytes (8 registers × 4 bytes)
- 48 bytes (12 registers × 4 bytes)
- 64 bytes (16 registers × 4 bytes)
- 128 bytes (32 registers × 4 bytes)
Check Answer
Correct answer: C — 64 bytes (16 registers × 4 bytes)
8 hardware registers (32 bytes) + 8 manually saved registers (32 bytes) = 16 registers × 4 bytes = 64 bytes total per context switch.
Alt: “ARM Cortex-M4 register file task execution context diagram embedded C”
Alt: “ARM Cortex-M exception stacking hardware auto push r0 r3 r12 LR PC xPSR”
Alt: “ARM Cortex-M complete task context switch stack frame layout 64 bytes”
Frequently Asked Questions
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments