EXC_RETURN Decode Explained
EXC_RETURN is a 32-bit magic value written into LR on every exception entry. Its lower bits encode which stack to unstack from and which mode to return to. Two bits — bit[3] and bit[2] — determine everything the processor does during an exception exit, including how the CONTROL register is updated.
EXC_RETURN Bit-Field Decode
EXC_RETURN is not a real memory address. Bits [31:28] are always 0xF, which places it in the address range 0xF0000000–0xFFFFFFFF — a region that can never contain executable code on Cortex-M. The processor recognises this pattern and triggers the exception exit mechanism instead of a normal branch.
EXC_RETURN 32-bit field layout
0xF — marks this as an exception return trigger, not a code address0 = extended frame (FPU S0–S15 + FPSCR saved, 26 words = 104 bytes)
Always 1 on Cortex-M3 (no FPU). On Cortex-M4F: 0 if FPU was active.
0 = return to Handler mode (i.e. another handler was interrupted — nesting)
0 = unstack using MSP; set CONTROL[1] (SPSEL) = 0
Only bits [4], [3], and [2] carry meaningful information. Bit[3] and bit[2] together select the return scenario. Bit[4] selects the frame size for unstacking.
0xFFFFFFF10xFFFFFFF90xFFFFFFFDUnstacking Flowchart
When EXC_RETURN is loaded into the PC, the processor reads bit[2] to decide which stack pointer to use for the pop operation and how to update the CONTROL register:
Exception Return Decision — EXC_RETURN[2]
(BX LR with EXC_RETURN in LR)
Pop R0,R1,R2,R3,R12,LR,PC,xPSR from [MSP]
MSP += 32
Pop R0,R1,R2,R3,R12,LR,PC,xPSR from [PSP]
PSP += 32
MSP selected for Thread mode
PSP selected for Thread mode
The CONTROL register SPSEL bit is updated automatically by hardware during the exception exit — software does not need to write CONTROL explicitly.
Bit[3] is checked first: if bit[3] = 0, the processor returns to Handler mode and bit[2] is effectively irrelevant (Handler mode always uses MSP). If bit[3] = 1, the processor returns to Thread mode and bit[2] selects the stack.
All Possible EXC_RETURN Values
EXC_RETURN value reference (Cortex-M3/M4 without FPU active)
0xFFFFFFF10xFFFFFFF90xFFFFFFFD0xFFFFFFE10xFFFFFFE90xFFFFFFEDAll other values are reserved and produce unpredictable behaviour.
Link to the CONTROL Register
The CONTROL register has two bits that govern stack pointer selection and privilege level in Thread mode. EXC_RETURN bit[2] directly programs CONTROL[1] during exception exit:
CONTROL register bits
CONTROL[1] is written automatically by hardware during exception exit based on EXC_RETURN[2]. Software only writes CONTROL[1] explicitly when switching from MSP to PSP (e.g. during RTOS startup).
/* Reading CONTROL register — CMSIS */
uint32_t ctrl = __get_CONTROL();
uint32_t spsel = (ctrl >> 1) & 1U; /* 1 = PSP active in Thread mode */
uint32_t npriv = (ctrl >> 0) & 1U; /* 1 = unprivileged Thread mode */
/* Switching to PSP in Thread mode (RTOS startup) */
__set_PSP((uint32_t)task_stack_top);
__set_CONTROL(0x02U); /* SPSEL=1, nPRIV=0 */
__ISB(); /* instruction sync barrier — mandatory after CONTROL write */Always issue ISB after writing CONTROL. The pipeline may have already fetched the next instruction using the old CONTROL value. The ISB flushes the pipeline and ensures the new SPSEL value takes effect before any subsequent memory accesses.
RTOS Context-Switch Usage
The EXC_RETURN mechanism is the foundation of every RTOS context switch on Cortex-M. A context switch consists of:
- Save the current task’s remaining registers (R4–R11) onto its PSP stack.
- Save the current PSP value into the task’s TCB (Task Control Block).
- Select the next task: load its saved PSP from its TCB.
- Restore the next task’s R4–R11 from its PSP stack.
- Return with
0xFFFFFFFDin LR so the processor unstacks the hardware frame (R0–R3, R12, old LR, PC, xPSR) from the next task’s PSP.
PendSV-based context switch — stack layout
Yellow = pushed/popped by hardware via EXC_RETURN=0xFFFFFFFD. Blue/Green = pushed/popped by PendSV handler software (STMDB/LDMIA).
/* Minimal PendSV handler for context switch (GCC ARM, Cortex-M) */
__attribute__((naked)) void PendSV_Handler(void)
{
__asm volatile (
/* Save remaining registers of current task onto its PSP */
"MRS R0, PSP \n\t" /* R0 = current PSP */
"STMDB R0!, {R4-R11} \n\t" /* push R4-R11, update R0 (PSP) */
/* Save current PSP and switch to next task */
"BL os_switch_context \n\t" /* R0 = new task PSP on return */
/* Restore registers of next task from its PSP */
"LDMIA R0!, {R4-R11} \n\t" /* pop R4-R11, update R0 (PSP) */
"MSR PSP, R0 \n\t" /* update PSP to next task stack */
/* Return to next task using PSP unstack */
"MOV LR, #0xFFFFFFFD \n\t" /* EXC_RETURN: Thread/PSP */
"BX LR \n\t" /* trigger exception exit */
);
}
Tail-Chaining and Late-Arrival
Cortex-M implements two pipeline optimisations that interact with EXC_RETURN:
Tail-Chaining
When one ISR finishes and another interrupt is already pending at the same or lower priority, the processor skips the unstack+restack cycle. Instead of restoring Thread mode just to immediately re-enter Handler mode, it fetches the next vector and jumps directly. This saves up to 12 cycles (one full stacking sequence).
From the programmer’s perspective, both ISRs run correctly — the EXC_RETURN that ends the first ISR is consumed internally and never reaches the PC as a normal value.
Late Arrival
If a higher-priority interrupt arrives while the processor is still in the stacking phase of a lower-priority exception entry, the processor switches to the higher-priority vector. The already-pushed exception frame on the stack is reused for both exceptions.
When the higher-priority ISR returns with 0xFFFFFFF1 (Handler→Handler), the processor then takes the lower-priority exception, which was deferred by the late arrival, without any additional stacking.
Practical Code
Reading EXC_RETURN from a Running ISR
void USART1_IRQHandler(void)
{
uint32_t exc_return;
__asm volatile ("MOV %0, LR" : "=r" (exc_return));
/* Decode bit[2] — which stack was the interrupted code using? */
if (exc_return & (1U << 2)) {
/* PSP — interrupted code was an RTOS task */
} else {
/* MSP — interrupted code was bare-metal Thread mode or another handler */
}
/* Decode bit[3] — which mode are we returning to? */
if (exc_return & (1U << 3)) {
/* Returning to Thread mode */
} else {
/* Returning to Handler mode — we're inside a nested exception */
}
/* Normal ISR work ... */
USART1->SR; /* read SR to clear ORE (order matters on STM32F4) */
(void)USART1->DR; /* read DR to clear RXNE */
}Verifying EXC_RETURN in a Debugger
/* In GDB, halt inside an ISR and print LR:
*
* (gdb) p/x $lr
* $1 = 0xfffffff9
*
* Decode:
* bit[4] = 1 → basic frame (no FPU)
* bit[3] = 1 → return to Thread mode
* bit[2] = 0 → unstack from MSP
* → bare-metal firmware, Thread mode was using MSP
*
* For an RTOS with PSP tasks:
* (gdb) p/x $lr
* $2 = 0xfffffffd
* bit[2] = 1 → unstack from PSP
* → returning to an RTOS task
*/Detecting Nested Exception at Runtime
/* Read IPSR — if non-zero, we are in an exception handler.
* If IPSR > 0 AND we entered this ISR from another ISR,
* LR will be 0xFFFFFFF1 (Handler-to-Handler). */
uint32_t is_in_nested_exception(void)
{
uint32_t exc_return;
__asm volatile ("MOV %0, LR" : "=r" (exc_return));
/* bit[3]=0 means return to Handler mode → we are nested */
return ((exc_return & (1U << 3)) == 0) ? 1U : 0U;
}
Frequently Asked Questions
Suggested Diagrams for This Lecture
- EXC_RETURN 32-bit field: colour-coded with bits [31:28]=magic, [4]=frame type, [3]=mode, [2]=stack
- Flowchart: EXC_RETURN[2]=0 → MSP path vs EXC_RETURN[2]=1 → PSP path, both converging at “resume”
- RTOS PSP stack layout: software-saved R4–R11 on top of hardware-saved R0–R3/R12/LR/PC/xPSR
- Tail-chaining timeline: ISR-A ends → ISR-B starts without unstack/restack gap

2 Comments