What is Exception Entry & Exit Mechanism-Embedded C Training for Freshers

Need Help or More Information?

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

Example 11 — STM32F407 Discovery

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:

  1. State save (push): It pushes 8 registers onto the currently active stack (PSP if Thread mode was using PSP, else MSP).
  2. 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.

Thread mode and Handler mode exception flow
Thread Mode (PSP active)
… executing …
SP = PSP
IRQ fires!
IRQ fires!
BX LR triggers pop
Handler Mode (MSP always)
ISR executes
SP = MSP
LR = EXC_RETURN
BX LR triggers pop
(hardware saves 8 regs to PSP stack)
(hardware restores 8 regs from PSP stack)
============================================================

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)

PSP+28 xPSR T-bit=1, IPSR ← last pushed (top of frame)
PSP+24 PC return address next instruction to resume
PSP+20 LR caller LR link register before IRQ
PSP+16 R12 scratch
PSP+12 R3 arg3/scratch
PSP+8 R2 arg2/scratch
PSP+4 R1 arg1/scratch
PSP+0 R0 arg0/scratch ← PSP now points here
Why these 8?
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.
R4–R11 are callee-saved. If the ISR uses them, the compiler automatically generates PUSH/POP instructions inside the ISR prologue/epilogue to save them. The hardware does NOT save R4–R11.

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]RegisterAccess via CNotes
[0]R0pFrame[0]First function argument, also return value slot
[1]R1pFrame[1]Second argument
[2]R2pFrame[2]Third argument
[3]R3pFrame[3]Fourth argument
[4]R12pFrame[4]Intra-procedure call scratch register
[5]LRpFrame[5]Return address of interrupted function
[6]PCpFrame[6]Address of next instruction to execute on return
[7]xPSRpFrame[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 */
Key rule: EXC_RETURN bit[2] tells the processor which stack to pop the stack frame FROM on return. It mirrors the CONTROL.SPSEL value that was active in Thread mode before the exception was taken.
============================================================

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.

BitNameValue=0Value=1Who Can Write
2FPCAFP context inactiveFP context active (lazy stacking)Hardware (auto)
1SPSELMSP used in Thread modePSP used in Thread modePrivileged Thread mode only
0nPRIVPrivileged Thread modeUnprivileged Thread modePrivileged code only
Important: CONTROL.SPSEL is IGNORED in Handler mode. Handler mode ALWAYS uses MSP regardless of SPSEL. Writing SPSEL only takes effect when running in Thread mode.

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)

AddressBitsFunction
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)

AddressBitsFunction
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

BitsFieldDescription
[31]NNegative flag
[30]ZZero flag
[29]CCarry flag
[28]VOverflow flag
[27]QSaturation flag (DSP)
[26:25]ICI/ITInterrupt-Continuable Instruction / IF-THEN state
[24]TThumb state bit — MUST be 1 on Cortex-M4
[23:20]—Reserved
[19:16]GEGreater-than-or-Equal flags (SIMD)
[15:10]ICI/ITContinuation of IT bits
[8:0]ISR_NUMBER0=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

Exception entry and exit timeline (Thread mode on PSP)
Thread Mode (PSP=0x20008000)
MSR PSP, #0x20008000
MSR CONTROL, #0x02 → PSP is now active SP
printf(“before…”)
generate_interrupt()
*ISER0 |= (1<<3)
*STIR = 3
[hardware stacks frame]
PSP: 0x20008000
→ 0x20007FE0
[hardware pops frame]
PSP: 0x20007FE0
→ 0x20008000
printf(“after…”)
Time →
IRQ3 fires
exception return
Handler Mode (MSP)
Handler mode starts
LR = 0xFFFFFFFD
SP = MSP
printf(“ISR running…”)
BX LR (EXC_RETURN)

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)

StepActionNotes
1Acknowledge interruptNVIC asserts exception request to CPU core
2Complete current instruction (or not — depends on instruction)LDM/STM may be interrupted mid-execution
3Push 8 registers to stackUses PSP if CONTROL.SPSEL=1, else MSP. Stack must be 8-byte aligned.
4Switch to Handler modeIPSR[8:0] loaded with exception number
5Force SP to MSPRegardless of CONTROL.SPSEL
6Load EXC_RETURN into LR0xFFFFFFFD if was using PSP, 0xFFFFFFF9 if was using MSP
7Fetch exception vectorPC = word at (VTOR + exception_number × 4)
8Execute ISR first instructionPre-fetch/branch prediction may have already done this

Exit Sequence (when BX LR with EXC_RETURN in LR executes)

StepActionNotes
1Detect EXC_RETURN in LRUpper bits = 0xFFFFFFF identifies it as EXC_RETURN
2Check NVIC for higher priority pendingIf yes, tail-chain to next ISR instead of returning to Thread
3Pop 8 registers from stackUses PSP if EXC_RETURN bit[2]=1, else MSP
4Restore XPSRCondition flags and T-bit restored
5Restore PCExecution resumes at stacked PC
6Return to Thread modeIPSR cleared to 0
7Restore SPSEL from EXC_RETURNSP 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.

Normal return versus tail-chaining
Normal return: ISR_A → Thread → ISR_B
ISR_A
pop 8
Thread
push 8
ISR_B
Cycles: ~12 pop + 12 push = 24 wasted cycles
Tail-chained: ISR_A → ISR_B (IRQ_B pending during ISR_A)
ISR_A
no pop/push
ISR_B
Cycles: ~6 (just fetch new vector)
Frame already on stack — reused for ISR_B
============================================================

9. Interview Questions & Answers

Q1. What 8 registers does Cortex-M4 automatically save on exception entry?
R0, R1, R2, R3, R12, LR (R14), PC (R15), and xPSR. These are the caller-saved registers per AAPCS. R4–R11 are callee-saved and pushed manually by the compiler inside the ISR if needed.
Q2. What is EXC_RETURN and what are its three possible values?
EXC_RETURN is a special value (upper bits = 0xFFFFFFF) loaded into LR by the processor on exception entry. It encodes the return context:
• 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?
The hardware pushes the 8-register stack frame onto the PSP (Process Stack Pointer). This is because the interrupted Thread mode code owns the PSP, and the frame belongs to that thread’s context. The ISR then runs using MSP. On return, EXC_RETURN=0xFFFFFFFD tells the processor to pop from PSP.
Q4. Can an ISR ever use PSP instead of MSP?
No. Handler mode always uses MSP regardless of CONTROL.SPSEL. SPSEL only affects Thread mode. This is a fundamental ARM Cortex-M design choice to ensure ISRs always have a predictable, dedicated stack (MSP = scheduler stack in RTOS scenarios).
Q5. What is the purpose of the T-bit in the stacked xPSR?
The T-bit (bit 24 of xPSR) indicates Thumb mode. It is saved so that when the exception returns, the processor restores the correct ISA state. On Cortex-M4, T-bit must always be 1 (all code is Thumb-2). If the restored xPSR has T-bit=0, a UsageFault (INVSTATE) is triggered immediately after return.
Q6. Why does CONTROL.SPSEL not take effect immediately after MSR CONTROL?
There can be pipeline effects. After writing to CONTROL, an ISB (Instruction Synchronization Barrier) should be executed to flush the pipeline and ensure the new SPSEL value is in effect before any subsequent instruction that depends on the SP value. Without ISB, the processor may still fetch subsequent instructions using the old SP.
Q7. In a naked ISR, why do we read MSP to get the stack frame pointer?
When an exception fires while Thread mode was using PSP, the hardware pushes the frame to PSP then switches SP to MSP for the ISR. At the start of the naked ISR, MSP points directly to the pushed stack frame (R0 at top). Reading MSP therefore gives the base address of the exception frame, allowing C code to read/modify the stacked register values.
Q8. What is tail chaining and when does it occur?
Tail chaining occurs when one ISR completes and there is another pending exception ready to run. Instead of popping the stack frame back to Thread mode and immediately pushing it again for the next ISR, the processor keeps the frame on the stack and just fetches the new ISR vector. This saves ~12 cycles and reduces interrupt latency for back-to-back interrupts.
Q9. What is late arrival in Cortex-M exception handling?
Late arrival occurs when a higher-priority exception becomes pending during the stack-push phase of entering a lower-priority exception. The processor is already doing the hardware save, so it completes the save and then switches to the higher-priority handler instead, using the already-saved frame. When the high-priority ISR finishes, it tail-chains to the lower-priority ISR.
Q10. How do you determine whether code is currently executing in Thread mode or Handler mode?
Read the IPSR (Interrupt Program Status Register) using 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?
The ARM ABI requires the stack to be 8-byte aligned at any public interface (function call or exception entry). Cortex-M4 ensures this automatically at exception entry by examining bit 9 of the saved xPSR — if an extra word was pushed as an aligner pad, this bit is set so it can be removed on return. This is needed because LDRD/STRD and VFP instructions require 8-byte-aligned accesses.
Q12. How is the exception stack frame different when the FPU is active?
With the FPU, an Extended Stack Frame is pushed: the basic 8 registers plus S0–S15 (16 FP registers), FPSCR, and a reserved word = 26 total words (104 bytes). Cortex-M4 supports lazy FP context saving where the FP registers are only pushed if the ISR actually uses FP instructions, reducing overhead when the ISR doesn’t need FP. The EXC_RETURN bit[4]=0 indicates an extended frame was used.
container

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

Leave a Reply

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