What is Exception Entry Exit Sequence-Embedded C Course Online

Exception Entry Exit Sequence

When two interrupts compete with identical priorities, hardware breaks the tie using the IRQ number. Once the NVIC selects a winner, a precise 7-step entry sequence fires automatically before a single line of your ISR runs. Understanding every step — and the EXC_RETURN value left in LR — is the key to writing safe, nestable interrupt handlers.

Exception EntryEXC_RETURNPending BitStackingActive BitNVIC

IRQ Number Tie-Break

The priority resolution tree has three levels. The NVIC works through them in order whenever two or more interrupts are simultaneously pending:

NVIC Arbitration Decision Tree

Step 1: Compare pre-empt priority values
Lower value wins → can nest
Step 2: Same pre-empt? Compare sub-priority values
Lower value runs first (no nesting)
Step 3: Same pre-empt AND sub-priority? Hardware tie-break
Lowest IRQ number runs first

The IRQ number tie-break is deterministic and requires no programmer action. It is defined in the ARM Architecture Reference Manual.

Example: TIM2 vs I2C1 — equal priorities
Peripheral
IRQ Number (STM32F4)
Pre-empt Priority
Sub-Priority
Result
TIM2
IRQ28
5
0
Wins (lower IRQ#)
I2C1_EV
IRQ31
5
0
Waits

With identical pre-empt and sub-priority, TIM2 (IRQ28) is served before I2C1_EV (IRQ31) because 28 < 31.

Priority Configuration Exercise

A practical way to study how the NVIC arbitrates between priorities is to pend interrupts manually using the NVIC Set Pending Register (NVIC_ISPR) — no peripheral hardware is needed. The exercise below pends TIM2 and I2C1_EV simultaneously and observes which ISR fires first.

Setup: STM32F4 IRQ numbers

Peripheral
IRQ#
NVIC_ISPR register
Bit to set
TIM2 global
28
NVIC_ISPR0 (0xE000E200)
Bit 28
I2C1 event
31
NVIC_ISPR0 (0xE000E200)
Bit 31

Both IRQs are in NVIC_ISPR0 because both are below IRQ32.

Experiment A: Same Priority

#include "stm32f4xx.h"
#include <stdio.h>

void TIM2_IRQHandler(void)
{
    printf("TIM2 ISR running\r\n");
    /* Clear the pending bit — for a manually-pended IRQ just return */
}

void I2C1_EV_IRQHandler(void)
{
    printf("I2C1_EV ISR running\r\n");
}

int main(void)
{
    /* Configure both ISRs at the same priority */
    NVIC_SetPriorityGrouping(4U);           /* all bits = pre-empt, STM32F4 */
    NVIC_SetPriority(TIM2_IRQn,     5);     /* priority 5 → 0x50 in IPR */
    NVIC_SetPriority(I2C1_EV_IRQn,  5);     /* same priority */

    /* Enable both IRQs */
    NVIC_EnableIRQ(TIM2_IRQn);
    NVIC_EnableIRQ(I2C1_EV_IRQn);

    /* Pend both at the same time */
    NVIC->ISPR[0] = (1U << 28) | (1U << 31);

    /* IRQ28 (TIM2) fires first because 28 < 31 (tie-break by IRQ number) */

    while (1) {}
}

Expected output (observe via SWO/UART):

TIM2 ISR running
I2C1_EV ISR running

Experiment B: Different Priority

    /* Give I2C1 a higher priority (lower value) */
    NVIC_SetPriority(TIM2_IRQn,    5);   /* lower urgency */
    NVIC_SetPriority(I2C1_EV_IRQn, 3);  /* higher urgency */

    /* Pend both */
    NVIC->ISPR[0] = (1U << 28) | (1U << 31);

    /* I2C1 fires first despite having a higher IRQ number */
I2C1_EV ISR running
TIM2 ISR running

Key insight: When priorities differ, urgency (pre-empt priority value) always wins over IRQ number. The IRQ number tie-break only applies when both pre-empt priority AND sub-priority are identical.

Pending Interrupt Behavior

Every IRQ has two status bits in the NVIC: a pending bit (in NVIC_ISPR/ICPR) and an active bit (in NVIC_IABR). Understanding how these two bits change through an interrupt’s lifecycle is essential for writing correct ISRs and avoiding missed-interrupt bugs.

IRQ Lifecycle — Pending and Active Bits

State
Pending Bit
Active Bit
CPU action
Peripheral asserts IRQ line
1 (set)
0
None yet — waiting
NVIC accepts → entry sequence starts
0 (auto-cleared)
1 (set)
Stacking + vector fetch
ISR executing
0
1
ISR code running
IRQ re-asserts while ISR runs
1 (set again)
1
Noted — will re-enter after exit
ISR returns (EXC_RETURN loaded)
0 (or 1 if re-pended)
0 (cleared)
Unstacking → Thread mode

The pending bit is cleared automatically by hardware during entry (step 4 of entry sequence). Software never needs to clear it — only the peripheral’s status flag needs clearing.

Common bug: Forgetting to clear the peripheral’s own interrupt flag (e.g. USART SR.RXNE, TIM SR.UIF). The NVIC pending bit is auto-cleared on entry, but if the peripheral flag is still set, the peripheral immediately re-asserts the IRQ line, which sets the NVIC pending bit again, causing the ISR to re-enter the moment it returns — an infinite loop of ISR calls.

Exception Entry Sequence (7 Steps)

When the NVIC decides to take an exception, the processor executes the following sequence automatically — entirely in hardware, before the first instruction of the ISR runs:

7-Step Exception Entry Sequence

1
Pending bit set
The peripheral asserts its interrupt line. The NVIC records this in the pending bit (NVIC_ISPR). This bit stays set until the NVIC decides to accept the exception.
2
Stacking + Vector fetch
Hardware pushes 8 registers onto the stack (R0, R1, R2, R3, R12, LR, PC, xPSR) — 32 bytes total. Simultaneously, the processor fetches the handler address from the vector table (base + exception_number × 4). Both operations happen in parallel on the AHB bus.
3
Entry into handler, Active bit set
The fetched handler address is loaded into PC. The NVIC sets the active bit for this exception in NVIC_IABR. The first ISR instruction now executes.
4
Pending bit auto-cleared
Hardware automatically clears the NVIC pending bit as the handler begins. This is done automatically — no software action needed. If the peripheral re-asserts its IRQ line during the ISR, the pending bit will be set again.
5
Mode → Handler mode
The processor transitions from Thread mode to Handler mode. IPSR (bits [8:0] of xPSR) now holds the exception number. Handler mode always uses the Main Stack Pointer (MSP).
6
Handler code executing
Your ISR function body runs. R0–R3 and R12 were saved by hardware, so the ISR can use them freely. Callee-saved registers (R4–R11) must be preserved by the ISR if used.
7
MSP used for stack operations
All stack operations inside the handler (local variables, nested calls, hardware stacking of nested exceptions) use the MSP, regardless of whether the interrupted code was using the PSP. The PSP value is preserved untouched.

Steps 1–4 happen in hardware automatically. The programmer only controls step 6 (ISR body).

Visualising the Stack During Entry

MSP movement during exception entry (Thread→Handler)

Offset
Register saved
Notes
SP+28
xPSR
bit[9] = stack alignment
SP+24
PC (return addr)
next instruction after interrupt point
SP+20
LR
caller’s return address
SP+16
R12
scratch / intra-procedure call register
SP+12
R3
4th argument / scratch
SP+8
R2
3rd argument / scratch
SP+4
R1
2nd argument / scratch
SP+0 ← new MSP
R0
1st argument / return value

MSP decrements by 32 bytes (8 words). If the pre-interrupt SP was not 8-byte aligned, hardware adds a padding word and sets xPSR bit[9].

Exception Exit and EXC_RETURN

The ARM Cortex-M exception return mechanism does not use a normal function return address. Instead, it uses a special 32-bit value called EXC_RETURN that encodes the target mode and stack pointer to restore.

Three Key Facts About EXC_RETURN
  • Generated on entry: When the processor enters an exception handler, hardware writes an EXC_RETURN value into the Link Register (LR). The old LR value (the caller’s return address) is saved onto the stack as part of the hardware exception frame.
  • Stored in LR: While the ISR runs, LR holds the EXC_RETURN value, not a normal function return address. If your ISR calls a sub-function, the compiler automatically saves/restores LR around the call (via PUSH/POP or BL).
  • Triggers exit: When any instruction loads EXC_RETURN into the PC (BX LR, POP {PC}, or LDMIA with PC in the register list), the processor recognises the magic pattern and executes the full exception exit (unstacking, mode switch, IPSR clear).

Exception Exit Steps

What happens when EXC_RETURN is loaded into PC

1
Processor detects EXC_RETURN pattern (bits [31:4] = 0xFFFFFFF). Normal instructions cannot produce an address in this range, so the pattern is unambiguous.
2
EXC_RETURN bits [3:0] determine which stack to unstack from (MSP or PSP) and which mode to return to (Thread or Handler). See the values table below.
3
Hardware pops the 8-register frame (R0, R1, R2, R3, R12, LR, PC, xPSR) from the selected stack. The saved PC is loaded into the program counter — execution resumes at the interrupted instruction.
4
IPSR (exception number in xPSR) is cleared to 0. Processor mode switches from Handler mode back to Thread mode (or remains in Handler mode for a nested exception exit).
5
Active bit for the exiting exception is cleared in NVIC_IABR. If the pending bit was set again during the ISR, a new exception entry begins immediately.

EXC_RETURN Values Table

Hardware generates one of three EXC_RETURN values depending on which stack was active before the exception and whether the FPU (Cortex-M4F) has live registers:

EXC_RETURN — lower 4 bits decode

EXC_RETURN value
Return to
Stack used for unstacking
FPU frame saved?
0xFFFFFFF1
Handler mode
MSP
No
0xFFFFFFF9
Thread mode
MSP
No
0xFFFFFFFD
Thread mode
PSP
No
0xFFFFFFE1
Handler mode
MSP
Yes (Cortex-M4F)
0xFFFFFFE9
Thread mode
MSP
Yes (Cortex-M4F)
0xFFFFFFED
Thread mode
PSP
Yes (Cortex-M4F)

The most common value in bare-metal firmware is 0xFFFFFFF9 (interrupted code was in Thread mode using MSP). In an RTOS with PSP per task, 0xFFFFFFFD is used for task context ISRs.

EXC_RETURN bit-field decode

Bits [31:4]
Bit [3]
Bit [2]
Bit [1]
Bit [0]
0xFFFFFFF (magic)
0=FPU frame, 1=no FPU
0=Handler, 1=Thread
always 0
0=MSP, 1=PSP

Bit[2]=1 means return to Thread mode. Bit[0]=1 means use PSP for unstacking. Together these two bits select the four main exit scenarios.

Practical Code

Reading EXC_RETURN from Inside an ISR

/* Print EXC_RETURN value from within an ISR.
 * The compiler saves LR to the stack on ISR entry (if the ISR
 * calls any sub-function). We read it via inline asm. */
void TIM2_IRQHandler(void)
{
    uint32_t exc_return;
    __asm volatile ("MOV %0, LR" : "=r" (exc_return));

    if (exc_return == 0xFFFFFFF9U) {
        /* Interrupted code was in Thread mode, using MSP */
    } else if (exc_return == 0xFFFFFFFDU) {
        /* Interrupted code was in Thread mode, using PSP (RTOS task) */
    } else if (exc_return == 0xFFFFFFF1U) {
        /* Nested exception — interrupted another handler */
    }

    /* Clear TIM2 update interrupt flag before returning */
    TIM2->SR &= ~TIM_SR_UIF;
}

Observing Active Bits

/* Check whether an IRQ is currently active (its ISR is running) */
int irq_is_active(IRQn_Type irq)
{
    if (irq < 0) return 0;   /* system exceptions not in IABR */
    uint32_t reg = (uint32_t)irq / 32U;
    uint32_t bit = (uint32_t)irq % 32U;
    return (int)((NVIC->IABR[reg] >> bit) & 1U);
}

/* Check pending bit */
int irq_is_pending(IRQn_Type irq)
{
    return (int)((NVIC->ISPR[(uint32_t)irq / 32U] >> ((uint32_t)irq % 32U)) & 1U);
}

Minimal Bare-Metal ISR Template

/* A complete bare-metal ISR for TIM2 on STM32F4.
 * Hardware stacks R0-R3, R12, LR, PC, xPSR on entry.
 * Hardware unstacks them on exit via EXC_RETURN.
 * No __attribute__((interrupt)) needed on Cortex-M. */

volatile uint32_t tim2_tick_count = 0;

void TIM2_IRQHandler(void)
{
    /* 1. Clear the peripheral interrupt flag FIRST.
     *    If you clear it last, there is a window where the flag is
     *    still set when the ISR returns, causing an immediate re-entry. */
    TIM2->SR = ~TIM_SR_UIF;   /* RC_W0 field: write 0 to clear */

    /* 2. Do the work */
    tim2_tick_count++;

    /* 3. Return: compiler emits BX LR.
     *    LR contains EXC_RETURN (e.g. 0xFFFFFFF9).
     *    Hardware unstacks and returns to interrupted code. */
}

Nested Exception — Reading Active Exception Number

/* IPSR (bits[8:0] of xPSR) holds the current exception number.
 * 0 = Thread mode, 1 = Reset, 2 = NMI, 3 = HardFault,
 * 4 = MemManage, 5 = BusFault, 6 = UsageFault,
 * 11 = SVC, 14 = PendSV, 15 = SysTick, 16+ = IRQ0+ */
uint32_t get_active_exception(void)
{
    return __get_xPSR() & 0x1FFU;   /* CMSIS */
}

/* Equivalent without CMSIS */
uint32_t get_active_exception_raw(void)
{
    uint32_t ipsr;
    __asm volatile ("MRS %0, IPSR" : "=r" (ipsr));
    return ipsr & 0x1FFU;
}

Frequently Asked Questions

Does hardware always clear the NVIC pending bit on exception entry?

Yes, for level-triggered peripherals the NVIC pending bit is cleared automatically on exception entry (step 4). However, the peripheral’s own interrupt flag is not touched by the NVIC — you must clear it yourself inside the ISR. If the peripheral flag remains set, the peripheral immediately re-asserts the IRQ line, which sets the NVIC pending bit again, leading to an infinite ISR re-entry.

Why is LR overwritten with EXC_RETURN on exception entry?

Normally LR holds a function’s return address. In Handler mode there is no concept of a “caller” in the normal sense — the handler was entered by hardware, not a BL instruction. The old LR value (from the interrupted code) is already saved in the hardware exception frame on the stack. Overwriting LR with EXC_RETURN gives the ISR a clean way to trigger the full exception exit mechanism via BX LR.

Can I manually set EXC_RETURN to change the return mode?

Yes, but only between the valid EXC_RETURN values (0xFFFFFFF1, 0xFFFFFFF9, 0xFFFFFFFD, and their FPU variants). Modifying EXC_RETURN is occasionally done in RTOS context-switch code to switch a task from using MSP to PSP. In general application code this is not needed.

What happens if two IRQs have the same number? (Is that possible?)

No. Each peripheral is assigned a fixed, unique IRQ number by the SoC vendor. There are no duplicate IRQ numbers. The tie-break by IRQ number only applies to the selection order between two simultaneously pending IRQs with identical priority values — both have different IRQ numbers.

How long does the stacking phase take?

On Cortex-M3/M4 with zero wait-state SRAM, the exception entry latency is 12 clock cycles: 1 cycle for the pending decision, 8 cycles for the 8-register push (with bus optimisations), and some pipeline cycles for the vector fetch and branch. This latency is deterministic and specified in the ARM TRM. With flash wait states or cache misses the latency can be higher.

Is __attribute__((interrupt)) needed on Cortex-M?

No. Unlike older ARM architectures (ARM7TDMI), Cortex-M hardware saves and restores the caller-saved registers (R0–R3, R12, LR, PC, xPSR) automatically. A plain C function with the correct name in the vector table works as a complete ISR. The __attribute__((interrupt)) GCC attribute is for other ARM architectures and must not be used on Cortex-M — it generates incorrect prologue/epilogue code.

Suggested Diagrams for This Lecture

  • Timeline diagram: IRQ asserted → pending → entry → ISR → exit → Thread resumed
  • Stack frame layout: 8 registers pushed, SP movement, new SP position
  • EXC_RETURN bit-field breakdown: bits [31:4] = magic, bit[3] = FPU, bit[2] = Thread, bit[0] = PSP
  • Nested ISR stack diagram: two consecutive frames on MSP
  • Priority arbitration flowchart: pre-empt → sub-priority → IRQ number

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 *