All guides
TECHNICAL GUIDESTM32L4R5INTERRUPTS2026

Interrupts on the STM32L4R5
NVIC, EXTI & ISR handling

The full external-interrupt signal chain — GPIO edge to SYSCFG mux to EXTI to NVIC to your ISR — with exact RM0432 register names, CMSIS macros, a compilable bare-metal button example and the HAL equivalent.

00 The interrupt signal path

On the STM32L4R5 (Cortex-M4F core, RM0432 reference manual) a GPIO pin cannot interrupt the CPU directly. The edge travels through four blocks. Understanding this chain is the whole game — every bug is a missing step in it.

GPIO pin edge → SYSCFG_EXTICR (pin→line mux) → EXTI (edge detect + mask + pending) → NVIC (enable + priority) → Cortex-M4 core → your *_IRQHandler
GPIOThe pin must be an input (MODER = 00) with a defined level (pull-up/down or external). Clock: RCC->AHB2ENR.
SYSCFG_EXTICRSelects which port (A..I) drives EXTI line y. All PxY pins share one line number y; only one port at a time. Clock: RCC->APB2ENR.
EXTIDetects rising/falling edges (RTSR1/FTSR1), masks the line to the NVIC (IMR1) or to an event (EMR1), and latches a pending flag (PR1).
NVICNested Vectored Interrupt Controller inside the Cortex-M4: per-IRQ enable bit and an 8-bit priority (top 4 bits implemented → 16 levels).

Two independent switches must both be on for an ISR to fire: the EXTI mask (IMR1) and the NVIC enable (ISER). Miss either and nothing happens; the pending flag may still latch in PR1, so once you finally unmask, a stale edge can fire immediately.

This section

  • Edge path: GPIO → SYSCFG mux → EXTI → NVIC → core → ISR.
  • Two enables required: EXTI IMR1 bit and NVIC ISER bit.
  • Three clocks to enable: the GPIO port (AHB2), SYSCFG (APB2), and — implicitly — the core is always clocked.

01 NVIC: enable, priority & grouping

The NVIC is memory-mapped inside the Cortex-M4 System Control Space. You rarely poke its registers directly — CMSIS (core_cm4.h, pulled in by stm32l4r5xx.h) gives you inline helpers. But knowing the registers explains exactly what those helpers write.

The CMSIS helpers you actually call

c — enabling an IRQ line
/* Order matters: set priority BEFORE enabling, so the first
 * interrupt is already at the priority you intended.          */
NVIC_SetPriority(EXTI0_IRQn, 5);   /* 0 = highest, 15 = lowest */
NVIC_EnableIRQ(EXTI0_IRQn);        /* unmask in the NVIC       */

/* Mirror operations */
NVIC_DisableIRQ(EXTI0_IRQn);
NVIC_ClearPendingIRQ(EXTI0_IRQn);  /* clears the NVIC latch, NOT EXTI->PR1 */
uint32_t p = NVIC_GetPriority(EXTI0_IRQn);

What those calls write, register-level

c — NVIC register equivalents (IRQn 6 = EXTI0)
#define IRQ  EXTI0_IRQn                       /* = 6 */

/* Enable: 32 IRQs per 32-bit word */
NVIC->ISER[IRQ >> 5] = (1u << (IRQ & 0x1F));   /* ISER[0], bit 6 */

/* Priority: one 8-bit IPR byte per IRQ; STM32 uses the TOP 4 bits,
 * so the value is left-shifted by (8 - __NVIC_PRIO_BITS) = 4.        */
NVIC->IPR[IRQ] = (uint8_t)(5u << (8 - __NVIC_PRIO_BITS)); /* 5<<4 = 0x50 */

/* Disable / clear-pending use the mirror arrays */
NVIC->ICER[IRQ >> 5] = (1u << (IRQ & 0x1F));   /* Clear-Enable  */
NVIC->ICPR[IRQ >> 5] = (1u << (IRQ & 0x1F));   /* Clear-Pending */

__NVIC_PRIO_BITS is 4U on the STM32L4R5. Because only the four most-significant bits are implemented, raw IPR writes must be pre-shifted; NVIC_SetPriority() does that shift for you, so you pass the plain 0..15 value.

NVIC registerCMSIS accessFunction
ISER[]NVIC->ISER[n]Set-Enable (write 1 to enable)
ICER[]NVIC->ICER[n]Clear-Enable (write 1 to disable)
ISPR[]NVIC->ISPR[n]Set-Pending (force pending)
ICPR[]NVIC->ICPR[n]Clear-Pending
IABR[]NVIC->IABR[n]Active bit (read-only)
IPR[]NVIC->IPR[irq]8-bit priority per IRQ (top 4 bits used)

Priority grouping (pre-emption vs sub-priority)

The 4 priority bits are split by the PRIGROUP field in SCB->AIRCR[10:8] into a pre-emption (group) part and a sub-priority part. Only pre-emption priority decides whether one ISR can interrupt another; sub-priority only orders two pending IRQs of the same group. Higher pre-emption (lower number) wins.

CMSIS / HAL groupPRIGROUPPre-empt bitsSub bitsPre-empt levelsSub levels
NVIC_PRIORITYGROUP_00b111 (7)04116
NVIC_PRIORITYGROUP_10b110 (6)1328
NVIC_PRIORITYGROUP_20b101 (5)2244
NVIC_PRIORITYGROUP_30b100 (4)3182
NVIC_PRIORITYGROUP_40b011 (3)40161

Key fact: HAL_Init() selects NVIC_PRIORITYGROUP_4 — all four bits are pre-emption, zero sub-priority. Under HAL the sub-priority argument you pass is silently ignored.

c — grouping: raw CMSIS vs HAL
/* Raw CMSIS: choose 2 group bits + 2 sub bits (PRIGROUP = 5) */
NVIC_SetPriorityGrouping(5u);
uint32_t prio = NVIC_EncodePriority(NVIC_GetPriorityGrouping(),
                                    2,   /* pre-emption level 0..3 */
                                    1);  /* sub-priority      0..3 */
NVIC_SetPriority(EXTI0_IRQn, prio);

/* HAL: HAL_Init() already forced group 4, so 'sub' does nothing */
HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0);  /* (IRQn, preempt, sub) */
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

02 Vector table & ISR naming

The Cortex-M4 fetches the ISR address from a table at the base of flash (0x0800_0000, pointed to by SCB->VTOR). Entry N holds the address for IRQ N. ST's startup_stm32l4r5xx.s fills that table with weak symbols aliased to Default_Handler (an infinite loop). You override one by defining a plain C function with the exact same name.

asm — startup_stm32l4r5xx.s (GCC), excerpt
g_pfnVectors:
  .word  _estack
  .word  Reset_Handler
  /* ... core exceptions ... */
  .word  EXTI0_IRQHandler        /* IRQ  6 */
  .word  EXTI1_IRQHandler        /* IRQ  7 */
  /* ... */
  .word  EXTI9_5_IRQHandler      /* IRQ 23 */
  /* ... */
  .word  EXTI15_10_IRQHandler    /* IRQ 40 */

  /* Each handler is a WEAK alias of Default_Handler: */
  .weak      EXTI0_IRQHandler
  .thumb_set EXTI0_IRQHandler, Default_Handler

Define a non-weak (strong) symbol of the same name in your C and the linker uses yours instead. A typo (EXTI_0_IRQHandler, wrong case, missing underscore) does not error — it silently leaves the weak Default_Handler in the table, and your interrupt appears to "do nothing" while the core spins in the default loop.

GPIO pin(s)EXTI lineEXTICR regIRQn (value)ISR symbol name
Px0EXTI0EXTICR[0]EXTI0_IRQn (6)EXTI0_IRQHandler
Px1EXTI1EXTICR[0]EXTI1_IRQn (7)EXTI1_IRQHandler
Px2EXTI2EXTICR[0]EXTI2_IRQn (8)EXTI2_IRQHandler
Px3EXTI3EXTICR[0]EXTI3_IRQn (9)EXTI3_IRQHandler
Px4EXTI4EXTICR[1]EXTI4_IRQn (10)EXTI4_IRQHandler
Px5 – Px9EXTI5 – EXTI9EXTICR[1..2]EXTI9_5_IRQn (23)EXTI9_5_IRQHandler
Px10 – Px15EXTI10 – EXTI15EXTICR[2..3]EXTI15_10_IRQn (40)EXTI15_10_IRQHandler

Lines 0–4 each own a dedicated vector; lines 5–9 share EXTI9_5 and lines 10–15 share EXTI15_10. Shared handlers must demultiplex via PR1 (see section 06). IRQn values above come straight from stm32l4r5xx.h.

03 EXTI: edges, line mux & pending

The EXTI controller owns the edge detection. On the STM32L4R5, configurable GPIO lines are 0–15; higher line numbers are internal sources (PVD, RTC, USB wakeup, COMP, …). Lines 0–31 use the *1 registers; lines 32+ use the *2 registers.

Register (lines 0–31)MeaningBehaviour
EXTI_IMR1Interrupt mask1 = line delivers an IRQ to the NVIC
EXTI_EMR1Event mask1 = line generates a CPU event/wakeup, no ISR
EXTI_RTSR1Rising trigger select1 = detect 0→1 transition
EXTI_FTSR1Falling trigger select1 = detect 1→0 transition
EXTI_SWIER1Software interrupt eventwrite 1 = force the line pending (self-test)
EXTI_PR1Pending register1 = selected edge occurred; write 1 to clear

The matching bit macros in stm32l4r5xx.h are EXTI_IMR1_IMx, EXTI_RTSR1_RTx, EXTI_FTSR1_FTx, EXTI_SWIER1_SWIx and EXTI_PR1_PIFx (for line x). Set both RTSR1 and FTSR1 for either-edge triggering.

Muxing the pin to the line: SYSCFG_EXTICR

Line number y can be driven by PAy, PBy, … but only one at a time. The 4-bit selector lives in SYSCFG->EXTICR[k], where k = y / 4 and the field starts at bit 4 * (y % 4).

EXTICR valuePortEXTICR valuePort
0x0PA0x5PF
0x1PB0x6PG
0x2PC0x7PH
0x3PD0x8PI
0x4PE(availability depends on package pin-out)

Mapping which register holds which line: EXTICR[0] → lines 0–3, EXTICR[1] → 4–7, EXTICR[2] → 8–11, EXTICR[3] → 12–15. The header also provides ready-made value macros such as SYSCFG_EXTICR4_EXTI13_PC (= 0x20) for the common cases.

Interrupt vs event. Unmask via IMR1 to run an ISR. Unmask via EMR1 instead and the edge sets a CPU event (wakes a __WFE() / pulses a peripheral trigger) but never vectors to a handler. Using EMR1 when you wanted a callback is a classic silent failure.

04 Bare-metal button example (PA0)

A complete, compilable register-level program (CMSIS device header only, no HAL). Wiring: a push-button from PA0 to GND (internal pull-up, so idle = high, press = falling edge), toggling the green LED LD1 (PC7) on the NUCLEO-L4R5ZI. PA0 → EXTI0 → dedicated EXTI0_IRQn.

c — main.c (register/LL level, fully self-contained)
#include "stm32l4r5xx.h"   /* CMSIS: EXTI, GPIOx, RCC, SYSCFG, NVIC_*, IRQn */

/* Shared with main loop -> MUST be volatile or the compiler may cache it */
volatile uint32_t g_presses = 0;

/* Vector-table symbol: name MUST match startup_stm32l4r5xx.s EXACTLY */
void EXTI0_IRQHandler(void)
{
    if (EXTI->PR1 & EXTI_PR1_PIF0) {   /* did line 0 fire?         */
        EXTI->PR1 = EXTI_PR1_PIF0;     /* clear pending: write 1   */
        GPIOC->ODR ^= GPIO_ODR_OD7;    /* toggle LD1               */
        g_presses++;
    }
}

int main(void)
{
    /* 1) Clocks: GPIOA + GPIOC on AHB2, SYSCFG on APB2 */
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN | RCC_AHB2ENR_GPIOCEN;
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;   /* WITHOUT this, EXTICR stays PA */
    (void)RCC->APB2ENR;                      /* read-back: let the clock settle */

    /* 2) PA0 = input (MODER 00) with pull-up (PUPDR 01) */
    GPIOA->MODER &= ~GPIO_MODER_MODE0;
    GPIOA->PUPDR = (GPIOA->PUPDR & ~GPIO_PUPDR_PUPD0)
                 | (1u << GPIO_PUPDR_PUPD0_Pos);

    /* 3) PC7 = general-purpose output (MODER 01), push-pull default */
    GPIOC->MODER = (GPIOC->MODER & ~GPIO_MODER_MODE7)
                 | (1u << GPIO_MODER_MODE7_Pos);

    /* 4) Route EXTI0 to port A (EXTICR1 field EXTI0 = 0x0 = PA) */
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;

    /* 5) EXTI line 0: falling edge only, clear stale, then unmask */
    EXTI->RTSR1 &= ~EXTI_RTSR1_RT0;   /* no rising edge   */
    EXTI->FTSR1 |=  EXTI_FTSR1_FT0;   /* falling edge     */
    EXTI->PR1    =  EXTI_PR1_PIF0;    /* clear stale flag BEFORE unmask */
    EXTI->IMR1  |=  EXTI_IMR1_IM0;    /* unmask -> reaches NVIC */

    /* 6) NVIC: priority (0..15) THEN enable */
    NVIC_SetPriority(EXTI0_IRQn, 5);
    NVIC_EnableIRQ(EXTI0_IRQn);

    while (1) {
        __WFI();   /* sleep until any interrupt wakes the core */
    }
}

Every write above maps 1:1 to the signal path in section 00. Drop the identical logic in with a different pin by changing the MODE/PUPD/EXTICR/IM/FT index and the IRQn/handler name from the table in section 02.

05 The HAL path & callbacks

STM32Cube HAL hides the registers behind three moving parts: HAL_GPIO_Init() (with an IT mode) programs SYSCFG + EXTI; the *_IRQHandler in stm32l4xx_it.c calls HAL_GPIO_EXTI_IRQHandler() which clears PR1 and dispatches; and you override the weak HAL_GPIO_EXTI_Callback(). Same result as section 04 — but you must not forget to wire all three.

c — main.c : GPIO + NVIC init
void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef g = {0};

    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_SYSCFG_CLK_ENABLE();   /* required for EXTI line muxing */

    /* PA0 -> external interrupt, falling edge, pull-up */
    g.Pin  = GPIO_PIN_0;
    g.Mode = GPIO_MODE_IT_FALLING;   /* IT = interrupt (EVT = event, no ISR) */
    g.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &g);

    /* PC7 -> push-pull output (LED) */
    g.Pin   = GPIO_PIN_7;
    g.Mode  = GPIO_MODE_OUTPUT_PP;
    g.Pull  = GPIO_NOPULL;
    g.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOC, &g);

    /* NVIC (HAL_Init selected NVIC_PRIORITYGROUP_4 -> sub-priority ignored) */
    HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
c — stm32l4xx_it.c : the vector-table ISR
void EXTI0_IRQHandler(void)
{
    /* Clears EXTI->PR1 for pin 0 AND calls HAL_GPIO_EXTI_Callback() */
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
c — your file : override the weak callback
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0) {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_7);   /* toggle LD1 */
    }
}
HAL_GPIO_Init (IT mode)Programs SYSCFG_EXTICR, RTSR1/FTSR1 and unmasks IMR1. It does not touch the NVIC.
HAL_NVIC_* You still call these yourself — CubeMX puts them right after HAL_GPIO_Init.
HAL_GPIO_EXTI_IRQHandlerThe only thing your *_IRQHandler body should contain; it clears the pending flag for you.
HAL_GPIO_EXTI_CallbackWeak, shared by all pins — branch on GPIO_Pin to tell lines apart.

06 Pending, clearing & shared handlers

There are two separate pending latches. Confusing them causes the two most common EXTI bugs: an interrupt that fires forever, and a shared handler that reacts to the wrong pin.

LatchSet whenCleared byIf you forget
EXTI->PR1 bitthe selected edge occurswriting 1 to that bitISR re-enters immediately → interrupt storm
NVIC pending (ISPR)the EXTI request assertsentering the ISR (or NVIC_ClearPendingIRQ)usually auto-handled; only matters after disabling

The rule: clear the EXTI source (PR1) inside the handler. The NVIC latch clears itself on ISR entry, but the EXTI flag is the persistent source — leave it set and the core re-vectors the moment the handler returns.

Shared handlers must demultiplex

Lines 10–15 all vector to EXTI15_10_IRQHandler. Inside it you must test each PR1 bit you care about and clear only the ones you handled. Example: the NUCLEO-L4R5ZI user button B1 is on PC13 → EXTI13 → EXTI15_10_IRQn.

c — PC13 button on a shared line
/* Mux EXTI13 -> port C. Line 13 lives in EXTICR[3], field bits [7:4]. */
SYSCFG->EXTICR[3] = (SYSCFG->EXTICR[3] & ~SYSCFG_EXTICR4_EXTI13)
                 | SYSCFG_EXTICR4_EXTI13_PC;   /* 0x20 selects PC */
EXTI->FTSR1 |= EXTI_FTSR1_FT13;
EXTI->PR1   =  EXTI_PR1_PIF13;                  /* clear stale */
EXTI->IMR1  |= EXTI_IMR1_IM13;
NVIC_SetPriority(EXTI15_10_IRQn, 6);
NVIC_EnableIRQ(EXTI15_10_IRQn);

/* One handler serves lines 10..15 -> you MUST check which line fired */
void EXTI15_10_IRQHandler(void)
{
    if (EXTI->PR1 & EXTI_PR1_PIF13) {
        EXTI->PR1 = EXTI_PR1_PIF13;   /* clear ONLY the line handled */
        /* ... service the button ... */
    }
    /* other lines (10,11,12,14,15) would each test their own PIF bit */
}
c — force a line pending in software (self-test)
EXTI->SWIER1 = EXTI_SWIER1_SWI0;   /* sets PR1 bit 0 -> runs EXTI0_IRQHandler */

07 Gotchas & common mistakes

Almost every "my interrupt doesn't fire" (or "fires forever") report on the ST forums is one of these. Scan the table first.

MistakeSymptomFix
SYSCFG clock not enabledEXTICR writes have no effect; line stays mapped to PA — a different port's pin appears "dead" or PA fires unexpectedlyRCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; before writing EXTICR
Not clearing EXTI->PR1 in the ISRHandler re-enters endlessly; app appears frozen (interrupt storm)Write 1 to the pending bit: EXTI->PR1 = EXTI_PR1_PIFx;
Handler name typo / wrong caseInterrupt "does nothing"; core silently spins in Default_HandlerCopy the exact symbol from section 02; it is case-sensitive
Two pins, same line numberPA0 and PB0 can't both use EXTI0 — only the last EXTICR port winsUse different pin numbers; one EXTI line = one port at a time
Wrong priority directionISR never pre-empts as expectedLower number = higher priority; only 0..15 are meaningful (4 bits)
Raw IPR write not shiftedPriority looks off by 16×NVIC->IPR[n] = prio << 4; — or just use NVIC_SetPriority()
Expecting sub-priority under HALTwo ISRs at same preempt level don't order as hopedHAL uses group 4 (0 sub bits); use a different NVIC_SetPriorityGrouping if you need sub-priority
Used EMR1 instead of IMR1CPU wakes / peripheral triggers but no ISR runsSet IMR1 for an interrupt; EMR1 is for events only
GPIO port clock offPin config writes are ignored; pin stays analog/inputEnable RCC->AHB2ENR bit for the port first
Shared flag not volatileMain loop never sees the ISR's update (optimized away)Declare cross-ISR variables volatile
Switch bounceOne press → many interrupts / double-toggleDebounce: ignore edges within ~10–20 ms, or read the pin after a short delay
RTOS ISR calls *FromISR at too-high priorityHard fault / assert inside FreeRTOSKeep numeric priority ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY (i.e. logically lower)

Checklist for a working GPIO interrupt

  • Enable GPIO port clock (AHB2) and SYSCFG clock (APB2).
  • Pin = input, with pull-up/down or an external reference.
  • SYSCFG->EXTICR[y/4] selects the correct port for line y.
  • Set RTSR1 and/or FTSR1, clear stale PR1, unmask IMR1.
  • NVIC_SetPriority() then NVIC_EnableIRQ() with the right IRQn.
  • Define the exact *_IRQHandler; clear PR1 first thing inside it.