Task State, Stacking & Unstacking on ARM Cortex-M3/M4-Embedded C Training Institute in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

ARM Cortex-M Programming
Free Embedded C Programming Course

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.

▶ ARM Cortex-M3 / M4📚 Intermediate Level🚀 Free Course✎ Hands-On C Code

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:

ARM Cortex-M4 Processor Core
General Purpose Registers (caller-saved)
r0
r1
r2
r3
r12
General Purpose Registers (callee-saved)
r4
r5
r6
r7
r8
r9
r10
r11
Special Registers
SP (r13)
LR (r14)
PC (r15)
MSP
PSP
CONTROL
Status Register
xPSR (APSR + IPSR + EPSR)
ALU — Arithmetic Logic Unit
▲ HARDWARE SAVED
r0, r1, r2, r3, r12, LR, PC, xPSR pushed automatically by CPU on exception entry.
▼ MANUALLY SAVED
r4–r11 must be pushed/popped by the PendSV handler in assembly.
💾 PSP SAVED IN TCB
The updated PSP value after pushing r4–r11 is stored in the task’s TCB entry.

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 ExecutionSaved ByWhere Stored
r0 – r3, r12Function arguments, return value, scratch registers. Frequently change with every function call.Hardware (automatic)Task PSP stack — exception frame
r4 – r11Callee-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
xPSRCondition flags (N/Z/C/V), current exception number, Thumb state bit (must be 1).Hardware (automatic)Task PSP stack — exception frame
PSP valueThe process stack pointer of this task — points to the top of the saved context.PendSV + TCBTask 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.

T1 — Outgoing Task (Context Save)
1
SysTick fires — T1 is interrupted mid-execution. Hardware pushes r0–r3, r12, LR, PC, xPSR onto T1’s PSP stack automatically.
2
PendSV pends — SysTick handler sets PENDSVSET bit in SCB->ICSR and returns. PendSV fires next.
3
Read PSP of T1 — PendSV reads the current PSP value (MRS r0, PSP). This points to the top of T1’s stack.
4
Push r4–r11 — PendSV pushes the 8 callee-saved registers onto T1’s PSP stack (STMDB r0!, {r4–r11}).
5
Save PSP to TCB — PendSV stores the updated PSP value into T1’s Task Control Block entry in RAM.
Context
Switch
↓
PendSV
Handler
↓
≈1–3 µs
T2 — Incoming Task (Context Restore)
6
Load PSP from TCB — PendSV reads T2’s saved PSP value from T2’s TCB entry.
7
Pop r4–r11 — PendSV pops the 8 callee-saved registers from T2’s stack (LDMIA r0!, {r4–r11}), restoring T2’s private variables.
8
Write PSP — PendSV writes the updated PSP back to the PSP register (MSR PSP, r0), so it now points to T2’s exception frame.
9
BX LR (EXC_RETURN) — PendSV returns with BX LR. The CPU sees EXC_RETURN = 0xFFFFFFFD and pops r0–r3, r12, LR, PC, xPSR from T2’s stack.
10
T2 resumes — PC is restored to T2’s last instruction. T2 continues running as if it was never interrupted.

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.

  1. Exception recognised The processor finishes the current instruction (or abandons it cleanly) and decides to take the exception.
  2. 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.
  3. 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).
  4. 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.
  5. 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

T1 Stack — Before SysTick
PSP
▲ SP (Task running here)
PSP-4
… free stack space …
PSP-N
local variables / call frames
►
T1 Stack — After Hardware Stacking
PSP-0
▲ New PSP (PendSV starts here)
PSP-4
r0 HW
PSP-8
r1 HW
PSP-12
r2 HW
PSP-16
r3 HW
PSP-20
r12 HW
PSP-24
LR HW
PSP-28
PC (resume addr) HW
PSP-32
xPSR HW
↓
… (original local vars below) …
Stack grows downward ↓ | 32 bytes pushed by hardware

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.

  1. CPU detects EXC_RETURN The BX LR at the end of the handler passes 0xFFFFFFFD to the PC. The processor recognises the EXC_RETURN pattern and begins exception return.
  2. 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.
  3. Eight registers popped — in reverse order The CPU pops xPSR, PC, LR, r12, r3, r2, r1, r0 from the selected stack. SP increments by 32 bytes.
  4. 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.
  5. Mode switches back to Thread mode The processor exits Handler mode and returns to Thread mode using PSP (because EXC_RETURN = 0xFFFFFFFD specifies this).
T2 Stack — Before PendSV Returns
PSP+0
▲ PSP (set by PendSV)
PSP+4
r0 will pop
PSP+8
r1 will pop
PSP+12
r2 will pop
PSP+16
r3 will pop
PSP+20
r12 will pop
PSP+24
LR will pop
PSP+28
PC ← T2 resumes here will pop
PSP+32
xPSR will pop
►
T2 Stack — After Unstacking
PSP+32
▲ New PSP (T2 running)
↓
… free stack space …
low
T2 local variables / call frames
T2 is now running:
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:

Complete suspended task stack (top = lower address)
SP+0
▲ PSP saved in TCB
SP+4
r4 MANUAL
SP+8
r5 MANUAL
SP+12
r6 MANUAL
SP+16
r7 MANUAL
SP+20
r8 MANUAL
SP+24
r9 MANUAL
SP+28
r10 MANUAL
SP+32
r11 MANUAL
SP+36
r0 HW
SP+40
r1 HW
SP+44
r2 HW
SP+48
r3 HW
SP+52
r12 HW
SP+56
LR HW
SP+60
PC (resume address) HW
SP+64
xPSR HW
↓
… task’s own local variables …
MANUAL FRAME (r4–r11)
32 bytes
Pushed by PendSV assembly before saving PSP to TCB
HARDWARE FRAME (r0–r3, r12, LR, PC, xPSR)
32 bytes
Pushed by CPU on exception entry, popped on EXC_RETURN
TOTAL CONTEXT
64 bytes = 16 registers
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    lr

Stack 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?

  1. The C source code of the task function
  2. The complete set of CPU register values at the moment the task is interrupted
  3. The task’s local variables stored in global RAM
  4. 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?

  1. r4, r5, r6, r7, r8, r9, r10, r11
  2. r0, r1, r2, r3, r12, LR, PC, xPSR
  3. All 16 general purpose registers
  4. 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?

  1. The top of the hardware exception frame (r0 slot)
  2. The top of the manually saved frame (r4 slot)
  3. The bottom of the task stack
  4. 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)?

  1. A call to __enable_irq()
  2. A POP {pc} instruction in the handler
  3. A BX LR when LR holds an EXC_RETURN value (e.g. 0xFFFFFFFD)
  4. 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?

  1. Double Byte — stores 16-bit values
  2. Decrement Before — decrements the address register before storing each word
  3. Data Block — stores a fixed-size data block
  4. 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)?

  1. 32 bytes (8 registers × 4 bytes)
  2. 48 bytes (12 registers × 4 bytes)
  3. 64 bytes (16 registers × 4 bytes)
  4. 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.

Image 1 — ARM Cortex-M Register File “Labelled diagram of r0-r15, xPSR grouped by: yellow=hardware saved, purple=manually saved, green=PSP in TCB”

Alt: “ARM Cortex-M4 register file task execution context diagram embedded C”
Image 2 — Stacking Diagram “Memory diagram showing task stack before and after exception entry, with 8-register frame pushed downward, SP arrow moving down”

Alt: “ARM Cortex-M exception stacking hardware auto push r0 r3 r12 LR PC xPSR”
Image 3 — Full Context Frame “Complete 64-byte suspended task stack showing manual frame (r4-r11) below hardware frame (r0-r3, r12, LR, PC, xPSR)”

Alt: “ARM Cortex-M complete task context switch stack frame layout 64 bytes”

Frequently Asked Questions

Why doesn’t the hardware save r4–r11 automatically?

It is a deliberate ARM ABI design choice. The AAPCS (ARM Architecture Procedure Call Standard) divides registers into two groups: caller-saved (r0–r3, r12) and callee-saved (r4–r11).

A callee-saved register is one that a called function must preserve — if a C function uses r4, it must push r4 at the start and pop it before returning. This means that at any call boundary, r4–r11 already hold whatever value the caller placed there and the callee has promised to restore.

The hardware saves only the caller-saved group because those are the registers that an ISR is allowed to freely destroy. If the CPU saved all 16 registers, exception entry would take twice as long, which would harm interrupt latency.

What happens to the FPU registers during a context switch?

If the Cortex-M4 FPU is enabled and a task uses floating-point instructions, the hardware optionally saves s0–s15 and FPSCR as an extended exception frame (an extra 68 bytes on top of the standard 32 bytes). This is controlled by the FPCCR (Floating-Point Context Control Register).

The FPU also has callee-saved registers (s16–s31) which must be manually pushed by the PendSV handler if the task uses them. For a simple scheduler without FPU use, compile with -mfloat-abi=soft to avoid this complexity entirely. For production FPU support, check the FPCCR.LSPACT bit in PendSV to determine whether s0–s15 were auto-saved, then push s16–s31 manually.

Does saving and restoring context every 1 ms waste a lot of CPU time?

The overhead is very small. The STMDB/LDMIA instructions for r4–r11 execute in roughly 9–10 clock cycles each on Cortex-M4. Combined with the hardware stacking (about 12 cycles on a single-cycle SRAM system) and the function call overhead in the C helpers, a complete context switch takes approximately 100–200 ns at 168 MHz.

With a 1 ms time slice, the switch overhead is under 0.02% of CPU time for a 4-task scheduler. For production RTOS designs, FreeRTOS and Zephyr measure similarly — context switch overhead is rarely a concern unless you are scheduling hundreds of tasks per millisecond.

Can a task save its own context voluntarily (cooperative yield)?

Yes. A task can trigger a context switch at any time by pending PendSV itself: SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;. Since PendSV is set to the lowest priority, it fires after the current Thread-mode code is “interrupted” at the next safe boundary.

This is the basis of cooperative scheduling and of RTOS blocking calls like vTaskDelay() in FreeRTOS. When a task calls delay or waits on a semaphore, the RTOS marks the task as blocked, selects the next ready task, and pends PendSV — giving up the CPU voluntarily without waiting for SysTick to fire.

What is the difference between STMDB and PUSH on ARM?

PUSH {r4-r11} is exactly equivalent to STMDB SP!, {r4-r11} — they assemble to the same machine code. PUSH uses SP implicitly and is a convenient alias.

In PendSV we write STMDB r0!, {r4-r11} instead of PUSH because we are pushing onto the task’s PSP stack (address in r0), not onto the handler’s MSP stack. PUSH always uses SP (which in the handler is MSP), so it would write to the wrong stack. Using STMDB r0! lets us specify any base address — in this case the task’s PSP.

Why do we need ISB (Instruction Synchronisation Barrier) after MRS/MSR?

The Cortex-M4 has a pipeline that can speculatively fetch and partially execute instructions ahead of the current one. When you write a system register with MSR (e.g. change the PSP), the pipeline may have already fetched subsequent instructions using the old SP value.

The ISB instruction flushes the pipeline and forces the CPU to re-fetch everything after that point. This guarantees that any instruction executed after the ISB sees the new, correct PSP value. Without ISB, you risk a race where the CPU uses a stale SP value for the next instruction — a very subtle and hard-to-reproduce bug.

How does the CPU know which stack to unstack from on exception return?

The EXC_RETURN value stored in LR encodes this information. Specifically, bit 2 of EXC_RETURN tells the CPU which stack to use for unstacking:

0xFFFFFFF1 — return to Handler mode, use MSP (another handler was preempted)
0xFFFFFFF9 — return to Thread mode, use MSP
0xFFFFFFFD — return to Thread mode, use PSP (standard for RTOS tasks)

In a proper RTOS all tasks run in Thread mode with PSP, so PendSV always returns with 0xFFFFFFFD. The CPU pops the 8 exception frame registers from the PSP and continues the task from the restored PC. LR is restored to the task’s original link register value — not 0xFFFFFFFD — so the task’s own function calls work correctly.

EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *