What is EXC_RETURN Decode Explained-Embedded C Programming Training Online

ARM Cortex-M Programming

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_RETURNMSP vs PSPCONTROL RegisterException ExitRTOSTail-Chaining

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

Bits
Field name
Value / meaning
[31:28]
EXC_RETURN indicator
Always 0xF — marks this as an exception return trigger, not a code address
[27:5]
Reserved
All ones (0xEFFFFF) — must not be modified
[4]
Stack frame type
1 = basic frame (no FPU registers saved, 8 words = 32 bytes)
0 = 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.
[3]
Return mode
1 = return to Thread mode
0 = return to Handler mode (i.e. another handler was interrupted — nesting)
[2]
Return stack
1 = unstack using PSP; set CONTROL[1] (SPSEL) = 1
0 = unstack using MSP; set CONTROL[1] (SPSEL) = 0
[1]
Reserved
Always 0
[0]
Reserved
Always 1

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.

Deriving the three common values
Bit[4]
Bit[3]
Bit[2]
Binary (bits 4:0)
Full value
Meaning
1
0
0
1_0001b
0xFFFFFFF1
Handler mode, MSP
1
1
0
1_1001b
0xFFFFFFF9
Thread mode, MSP
1
1
1
1_1101b
0xFFFFFFFD
Thread mode, PSP

Unstacking 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]

Exception return trigger
(BX LR with EXC_RETURN in LR)
EXC_RETURN[2] = 0
OR
EXC_RETURN[2] = 1
Unstack using MSP
Pop R0,R1,R2,R3,R12,LR,PC,xPSR from [MSP]
MSP += 32
Unstack using PSP
Pop R0,R1,R2,R3,R12,LR,PC,xPSR from [PSP]
PSP += 32
CONTROL[1] (SPSEL) ← 0
MSP selected for Thread mode
CONTROL[1] (SPSEL) ← 1
PSP selected for Thread mode
Resume program execution at restored PC

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)

Value
Return to
Unstack from
Execution uses after return
Typical scenario
0xFFFFFFF1
Handler mode
MSP
MSP
Exception within an exception (nested ISR exiting to outer ISR)
0xFFFFFFF9
Thread mode
MSP
MSP
Bare-metal firmware: interrupted code in Thread mode using MSP (no RTOS)
0xFFFFFFFD
Thread mode
PSP
PSP
RTOS: interrupted code was a task using PSP (most common in RTOS firmware)
0xFFFFFFE1
Handler mode
MSP
MSP
Cortex-M4F: nested ISR, FPU registers were live
0xFFFFFFE9
Thread mode
MSP
MSP
Cortex-M4F: Thread mode, MSP, FPU registers were live
0xFFFFFFED
Thread mode
PSP
PSP
Cortex-M4F + RTOS: task using PSP, FPU registers were live

All 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

Bit
Name
0 (reset)
1
[1] SPSEL
Stack pointer selection (Thread mode only)
MSP active
PSP active
[0] nPRIV
Privilege level (Thread mode only)
Privileged
Unprivileged

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:

  1. Save the current task’s remaining registers (R4–R11) onto its PSP stack.
  2. Save the current PSP value into the task’s TCB (Task Control Block).
  3. Select the next task: load its saved PSP from its TCB.
  4. Restore the next task’s R4–R11 from its PSP stack.
  5. Return with 0xFFFFFFFD in 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

Current task PSP stack (after software save)
Next task PSP stack (before software restore)
R11 ← top (software saved)
R11 ← top (software saved)
R10 … R4 (software)
R10 … R4 (software)
xPSR (hardware)
xPSR (hardware)
PC = task entry point
PC = task resume point
LR, R12, R3–R0 (hardware)
LR, R12, R3–R0 (hardware)

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

Why are bits [1] and [0] of EXC_RETURN fixed at 0 and 1?

These bits are reserved and fixed to ensure that EXC_RETURN values are odd (bit[0]=1) and word-aligned (bit[1]=0). This is consistent with the Thumb instruction set convention where bit[0] of a branch target is the T-bit (always 1 for Thumb). The fixed pattern also prevents any legitimate code address from accidentally matching the EXC_RETURN range.

Can I modify EXC_RETURN in LR before returning?

Yes, within limits. You can change bit[2] to switch between MSP and PSP unstacking. This is exactly what an RTOS context switch does — it modifies LR to ensure the next task’s frame is popped from the correct PSP. However, bits [31:5] and bits [1:0] must not be changed. Writing an invalid EXC_RETURN value causes a HardFault.

What generates the EXC_RETURN value in LR?

Hardware generates it automatically on exception entry. The value is determined by: (a) which stack the pre-exception code was using (PSP or MSP → sets bit[2]), (b) which mode the pre-exception code was in (Thread or Handler → sets bit[3]), (c) whether the FPU had live registers (sets bit[4] to 0 on Cortex-M4F with FPU enabled). The programmer never constructs EXC_RETURN manually in normal application code.

Does CONTROL[1] need to be written before returning with 0xFFFFFFFD?

No. When EXC_RETURN bit[2] = 1 is detected, the processor automatically sets CONTROL[1] = 1 during the exception exit sequence. You do not need to write CONTROL explicitly. The CONTROL register is updated as part of the unstacking process.

What happens if EXC_RETURN is 0xFFFFFFF9 but the interrupted code was using PSP?

The processor will unstack from MSP instead of PSP. The PSP frame pushed during entry (which IS the correct frame) will be abandoned, and incorrect data will be popped into R0–R3, R12, LR, PC, xPSR. The corrupted PC will almost certainly cause a HardFault or jump to an unintended location. Never modify EXC_RETURN bit[2] unless you know precisely which stack the interrupted code was using.

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

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

2 Comments

Leave a Reply

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