Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Exception Entry & Exit Mechanism
Deep dive into how Cortex-M4 saves and restores CPU state automatically, the stack frame layout, EXC_RETURN magic values, and the PSP/MSP split between Thread and Handler modes.
1. The Big Picture — What Happens on an Exception
When an exception (interrupt, fault, SVC, PendSV, SysTick…) fires, the Cortex-M4 hardware performs two distinct actions automatically before the first instruction of the ISR runs:
- State save (push): It pushes 8 registers onto the currently active stack (PSP if Thread mode was using PSP, else MSP).
- Mode change: It switches to Handler mode, forces MSP as the active SP, loads the ISR vector from the vector table into PC, and places a special EXC_RETURN value into LR.
When the ISR executes BX LR (or any return instruction), the processor reads the EXC_RETURN value that was stored in LR, uses it to decide which stack to restore from, pops the 8 registers back, and returns to exactly where it was interrupted.
2. The Automatic Stack Frame (Exception Stack Frame)
The Cortex-M4 pushes exactly 8 registers in a specific order. This is called Stack Frame 1 (SF1) — the hardware-managed frame. If the FPU is active, an additional 17 words (S0–S15, FPSCR, aligner) may be pushed (Extended Frame), but we focus on the basic frame here.
Stack grows downward (lower addresses)
The ARM AAPCS calling convention defines R0–R3, R12, LR, PC, xPSR as caller-saved registers. The hardware saves them so the ISR (which acts like a called function) can freely use them and return to the interrupted code without corruption.
Stack Frame Word-by-Word (array indexing)
When you capture the stack pointer in your handler (e.g., MRS R0, MSP), R0 points to pBaseStackFrame[0] = R0. The indices are:
| Index [n] | Register | Access via C | Notes |
|---|---|---|---|
| [0] | R0 | pFrame[0] | First function argument, also return value slot |
| [1] | R1 | pFrame[1] | Second argument |
| [2] | R2 | pFrame[2] | Third argument |
| [3] | R3 | pFrame[3] | Fourth argument |
| [4] | R12 | pFrame[4] | Intra-procedure call scratch register |
| [5] | LR | pFrame[5] | Return address of interrupted function |
| [6] | PC | pFrame[6] | Address of next instruction to execute on return |
| [7] | xPSR | pFrame[7] | Condition flags + T-bit + ISR number at time of interrupt |
3. EXC_RETURN — The Magic Link Register Values
When the processor enters an exception handler, it loads LR with a special value called EXC_RETURN. The upper 28 bits are all 1s (0xFFFFFFF?), and the lower 4 bits encode the return context. When a BX/POP/LDM with PC reaches the processor while in Handler mode with an EXC_RETURN value in the target register, it performs exception return instead of a normal branch.
| EXC_RETURN Value | Return To | Stack Used on Return | When Occurs |
|---|---|---|---|
0xFFFFFFF1 |
Handler mode | MSP | Exception within exception (nested interrupts) |
0xFFFFFFF9 |
Thread mode | MSP | Thread mode was using MSP (CONTROL.SPSEL=0) |
0xFFFFFFFD |
Thread mode | PSP | Thread mode was using PSP (CONTROL.SPSEL=1) |
EXC_RETURN Bit Field Decoding
/* EXC_RETURN = 0xFFFFFFFD — bit-by-bit */
Bits[31:4] = 0xFFFFFFF /* all ones — signals this is EXC_RETURN */
Bit [3] = 1 /* Return to Thread mode (0 = return to Handler) */
Bit [2] = 1 /* Restore SP from PSP (0 = restore from MSP) */
Bit [1] = 0 /* Reserved, always 0 */
Bit [0] = 1 /* Reserved, always 1 */
/* EXC_RETURN = 0xFFFFFFF9 */
Bit [3] = 1 /* Thread mode */
Bit [2] = 0 /* Restore from MSP */
/* EXC_RETURN = 0xFFFFFFF1 */
Bit [3] = 0 /* Handler mode (nested exception) */
Bit [2] = 0 /* Restore from MSP */
4. CONTROL Register — Selecting the Stack Pointer
The CONTROL register governs which stack pointer is active in Thread mode and the privilege level. It is a 3-bit register readable/writable only via MRS/MSR instructions.
| Bit | Name | Value=0 | Value=1 | Who Can Write |
|---|---|---|---|---|
| 2 | FPCA | FP context inactive | FP context active (lazy stacking) | Hardware (auto) |
| 1 | SPSEL | MSP used in Thread mode | PSP used in Thread mode | Privileged Thread mode only |
| 0 | nPRIV | Privileged Thread mode | Unprivileged Thread mode | Privileged code only |
Setting CONTROL = 0x2 (SPSEL=1, nPRIV=0)
#include <stdint.h>
void switch_thread_to_psp(uint32_t psp_top)
{
/* Step 1: Load PSP register with the top of task stack */
__asm volatile ("MSR PSP, %0" :: "r"(psp_top));
/* Step 2: Set CONTROL.SPSEL = 1 → Thread mode will use PSP
CONTROL = 0x02 means: privileged + PSP active */
uint32_t ctrl = 0x2;
__asm volatile ("MSR CONTROL, %0" :: "r"(ctrl));
/* Step 3: ISB — flush pipeline so CONTROL change takes effect */
__asm volatile ("ISB");
}
============================================================
5. Key Registers — Complete Reference
NVIC ISER0 (0xE000E100)
| Address | Bits | Function |
|---|---|---|
| 0xE000E100 | [31:0] | Write 1 to bit N → enable IRQ N (N=0..31). Write 0 = no effect. Read = enabled status. |
NVIC STIR — Software Trigger Interrupt Register (0xE000EF00)
| Address | Bits | Function |
|---|---|---|
| 0xE000EF00 | [8:0] | Write interrupt ID (0–239) to trigger that IRQ from software. Requires USERSETMPEND in CCR or privileged access. |
XPSR — Combined Program Status Register
| Bits | Field | Description |
|---|---|---|
| [31] | N | Negative flag |
| [30] | Z | Zero flag |
| [29] | C | Carry flag |
| [28] | V | Overflow flag |
| [27] | Q | Saturation flag (DSP) |
| [26:25] | ICI/IT | Interrupt-Continuable Instruction / IF-THEN state |
| [24] | T | Thumb state bit — MUST be 1 on Cortex-M4 |
| [23:20] | — | Reserved |
| [19:16] | GE | Greater-than-or-Equal flags (SIMD) |
| [15:10] | ICI/IT | Continuation of IT bits |
| [8:0] | ISR_NUMBER | 0=Thread mode; 1=Reset; 2=NMI; 3=HardFault; 4=MemManage; 5=BusFault; 6=UsageFault; 11=SVCall; 14=PendSV; 15=SysTick; 16+=IRQ(N-16) |
6. Complete Code Walkthrough — Exception Entry/Exit Trace
This example sets PSP to 0x20008000, switches Thread mode to use PSP (CONTROL=0x2), then fires IRQ3 via STIR. This lets us observe that the stack frame is saved to PSP while the ISR runs using MSP.
/* ================================================================
* EXCEPTION ENTRY / EXIT DEMONSTRATION
* STM32F407 — Registers programmed directly
* ================================================================ */
#include <stdio.h>
#include <stdint.h>
/* ── Generate IRQ3 (RTC_WKUP on STM32F407) from software ── */
void generate_interrupt(void)
{
uint32_t *pSTIR = (uint32_t*)0xE000EF00; /* SW Trigger Interrupt Register */
uint32_t *pISER0 = (uint32_t*)0xE000E100; /* Interrupt Set-Enable Register 0 */
/* Enable IRQ3 in NVIC — set bit 3 */
*pISER0 |= (1 << 3);
/* Write IRQ number 3 to STIR — triggers software interrupt */
*pSTIR = (3 & 0x1FF);
}
int main(void)
{
/* ── Step 1: Set PSP to a valid RAM address ──────────────────
0x20008000 is in the middle of STM32F407 SRAM (128KB total:
0x20000000–0x20020000). We use it as our thread stack top. */
uint32_t psp_value = 0x20008000;
__asm volatile ("MSR PSP, %0" :: "r"(psp_value));
/* ── Step 2: CONTROL = 0x02 ──────────────────────────────────
bit[1]=SPSEL=1: Thread mode SP is now PSP
bit[0]=nPRIV=0: Thread mode remains privileged */
uint32_t ctrl = 0x2;
__asm volatile ("MSR CONTROL, %0" :: "r"(ctrl));
/* Stack pointer is now PSP = 0x20008000
MSP still holds the main/scheduler stack */
printf("Thread mode: before interrupt (SP=PSP=0x20008000)\n");
/* ── Step 3: Fire IRQ3 ──────────────────────────────────────
Hardware will:
a) Push 8 regs to PSP (because Thread mode was using PSP)
PSP decrements by 32 bytes → PSP = 0x20007FE0
b) Switch to Handler mode, use MSP for ISR stack
c) Load LR with EXC_RETURN = 0xFFFFFFFD (bit2=1 → pop from PSP)
d) Set PC = vector table entry for IRQ3 */
generate_interrupt();
/* ── Step 4: After ISR returns ──────────────────────────────
Hardware pops 8 regs from PSP (restores PSP to 0x20008000)
Returns here */
printf("Thread mode: after interrupt (PSP restored)\n");
for(;;);
}
/* ── IRQ3 = RTC_WKUP ISR — executes in Handler mode ── */
void RTC_WKUP_IRQHandler(void)
{
/* At this point:
- Processor is in Handler mode
- SP = MSP (not PSP) — ISRs always use MSP
- LR = 0xFFFFFFFD (EXC_RETURN to Thread/PSP)
Reading IPSR here would show 19 (16 + IRQ3) */
printf("Handler mode: ISR running (SP=MSP)\n");
/* BX LR (implicit from compiler) triggers exception return:
- Reads EXC_RETURN=0xFFFFFFFD from LR
- Pops 8 regs from PSP
- Returns to Thread mode with PSP active */
}
Execution Timeline
Verifying PSP Change with Inline Assembly
/* Read and print PSP at various points */
uint32_t read_psp(void)
{
uint32_t psp;
__asm volatile ("MRS %0, PSP" : "=r"(psp));
return psp;
}
uint32_t read_msp(void)
{
uint32_t msp;
__asm volatile ("MRS %0, MSP" : "=r"(msp));
return msp;
}
uint32_t read_control(void)
{
uint32_t ctrl;
__asm volatile ("MRS %0, CONTROL" : "=r"(ctrl));
return ctrl;
}
uint32_t read_ipsr(void)
{
uint32_t ipsr;
__asm volatile ("MRS %0, IPSR" : "=r"(ipsr));
return ipsr; /* 0 = Thread mode, non-zero = Handler mode */
}
/* In ISR, reading MSP gives you base of exception stack frame */
void RTC_WKUP_IRQHandler(void)
{
uint32_t msp_val = read_msp();
uint32_t ipsr = read_ipsr();
printf("ISR: MSP=0x%08lX, IPSR=%lu\n", msp_val, ipsr);
/* IPSR will be 19 (16 + IRQ3=3) */
}
Extracting Stack Frame Inside ISR
/* Naked entry point — preserves exact register state */
__attribute__((naked)) void RTC_WKUP_IRQHandler(void)
{
/* In handler mode, SP = MSP which points to the stacked frame.
Pass MSP as argument to C function. */
__asm("MRS R0, MSP");
__asm("B IRQ3_Handler_c");
}
void IRQ3_Handler_c(uint32_t *frame)
{
printf("Stacked R0 = 0x%08lX\n", frame[0]);
printf("Stacked R1 = 0x%08lX\n", frame[1]);
printf("Stacked R2 = 0x%08lX\n", frame[2]);
printf("Stacked R3 = 0x%08lX\n", frame[3]);
printf("Stacked R12 = 0x%08lX\n", frame[4]);
printf("Stacked LR = 0x%08lX\n", frame[5]);
printf("Stacked PC = 0x%08lX\n", frame[6]); /* next instruction after SVC/interrupt */
printf("Stacked xPSR= 0x%08lX\n", frame[7]);
/* Bit 24 of stacked xPSR should be 1 (T-bit) */
if (frame[7] & (1 << 24))
printf("T-bit = 1 (Thumb mode, correct)\n");
}
============================================================
7. Exception Sequence — Detailed Hardware Steps
Entry Sequence (hardware performs automatically)
| Step | Action | Notes |
|---|---|---|
| 1 | Acknowledge interrupt | NVIC asserts exception request to CPU core |
| 2 | Complete current instruction (or not — depends on instruction) | LDM/STM may be interrupted mid-execution |
| 3 | Push 8 registers to stack | Uses PSP if CONTROL.SPSEL=1, else MSP. Stack must be 8-byte aligned. |
| 4 | Switch to Handler mode | IPSR[8:0] loaded with exception number |
| 5 | Force SP to MSP | Regardless of CONTROL.SPSEL |
| 6 | Load EXC_RETURN into LR | 0xFFFFFFFD if was using PSP, 0xFFFFFFF9 if was using MSP |
| 7 | Fetch exception vector | PC = word at (VTOR + exception_number × 4) |
| 8 | Execute ISR first instruction | Pre-fetch/branch prediction may have already done this |
Exit Sequence (when BX LR with EXC_RETURN in LR executes)
| Step | Action | Notes |
|---|---|---|
| 1 | Detect EXC_RETURN in LR | Upper bits = 0xFFFFFFF identifies it as EXC_RETURN |
| 2 | Check NVIC for higher priority pending | If yes, tail-chain to next ISR instead of returning to Thread |
| 3 | Pop 8 registers from stack | Uses PSP if EXC_RETURN bit[2]=1, else MSP |
| 4 | Restore XPSR | Condition flags and T-bit restored |
| 5 | Restore PC | Execution resumes at stacked PC |
| 6 | Return to Thread mode | IPSR cleared to 0 |
| 7 | Restore SPSEL from EXC_RETURN | SP selection restored to pre-exception state |
8. Tail Chaining — Zero-Overhead ISR-to-ISR Transitions
When a higher-priority exception becomes pending while a lower-priority ISR is running, or when two exceptions are pending simultaneously, the Cortex-M4 uses tail chaining to switch from one ISR to the next without performing a full pop+push cycle.
9. Interview Questions & Answers
Q1. What 8 registers does Cortex-M4 automatically save on exception entry?
Q2. What is EXC_RETURN and what are its three possible values?
• 0xFFFFFFF1 — return to Handler mode, use MSP
• 0xFFFFFFF9 — return to Thread mode, use MSP
• 0xFFFFFFFD — return to Thread mode, use PSP
Q3. If Thread mode is using PSP and an interrupt fires, which stack does the hardware push the stack frame onto?
Q4. Can an ISR ever use PSP instead of MSP?
Q5. What is the purpose of the T-bit in the stacked xPSR?
Q6. Why does CONTROL.SPSEL not take effect immediately after MSR CONTROL?
Q7. In a naked ISR, why do we read MSP to get the stack frame pointer?
Q8. What is tail chaining and when does it occur?
Q9. What is late arrival in Cortex-M exception handling?
Q10. How do you determine whether code is currently executing in Thread mode or Handler mode?
MRS Rn, IPSR. If IPSR[8:0] = 0, code is in Thread mode. Any non-zero value means Handler mode, and the value is the exception number (1=Reset, 2=NMI, 3=HardFault, etc.). IRQ N maps to exception number N+16.Q11. What does 8-byte stack alignment mean and why is it required?
Q12. How is the exception stack frame different when the FPU is active?
STM32F407 Deep Dive Series — Example 11: Exception Entry & Exit
ARM Cortex-M4 Technical Reference Manual · ARMv7-M Architecture Reference Manual
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

1 Comment