STM32 EXTI a livello di registro:
SYSCFG, mappatura interrupt GPIO e collegamento NVIC
Niente di peggio di un pulsante che non sveglia il microcontrollore — o di un interrupt che scatta su entrambi i fronti quando ne volevi solo uno. La maggior parte dei tutorial HAL sorvola sul doppio passaggio GPIO→EXTI attraverso SYSCFG, lasciandoti una configurazione magica e incomprensibile. Risolviamo la cosa.
L'EXTI (External Interrupt/Event Controller) è il periferico che porta i segnali esterni al NVIC. Su STM32 supporta fino a 23 linee interrupt/evento. Le linee 0–15 sono collegate ai pin GPIO: ogni linea può essere mappata su un solo pin alla volta. Le linee 16–22 sono fissate a periferiche interne (RTC, PVD, USB, ecc.).
Questo articolo copre le linee 0–15 — gli interrupt GPIO configurabili — a livello di registro su STM32F4. Gli stessi principi valgono per STM32G0, G4, L4, H7 e la maggior parte delle famiglie Cortex-M STM32, anche se il numero di linee EXTI e la disposizione dei registri SYSCFG possono variare leggermente.
Architettura: il percorso in due stadi
Un interrupt esterno GPIO su STM32 attraversa esattamente due stadi periferici prima di raggiungere il NVIC:
- SYSCFG — seleziona quale porta GPIO (A, B, C, ...) alimenta ogni linea EXTI. Si configura nei registri
SYSCFG_EXTICR1..4, quattro bit per linea. - EXTI — imposta la sensibilità di fronte (salita, discesa o entrambi), maschera l'interrupt e segnala il pending flag. Controllato tramite
EXTI_IMR,EXTI_RTSR,EXTI_FTSReEXTI_PR.
Il NVIC deve semplicemente avere il canale IRQ corrispondente abilitato. Su STM32F4, le linee EXTI 0–4 hanno ciascuna un vettore di interrupt dedicato. Le linee 5–9 condividono EXTI9_5_IRQn e le linee 10–15 condividono EXTI15_10_IRQn.
Molti sviluppatori configurano solo i registri EXTI e il NVIC, chiedendosi poi perché l'interrupt non scatti mai. Il pezzo mancante è SYSCFG_EXTICR. Senza di esso, la linea EXTI non ha alcuna porta mappata e rimane disconnessa da qualsiasi GPIO — l'interrupt semplicemente non si attiva mai.
Configurazione a livello di registro passo per passo
Configuriamo un pulsante su PC13 (pulsante utente STM32F4DISCOVERY) per generare un interrupt sul fronte di discesa (pressione). PC13 significa: porta C, pin 13 → linea EXTI 13.
1. Abilitare i clock dei periferici
Sia SYSCFG che la porta GPIO devono avere il clock attivo. Su STM32F4, SYSCFG si abilita tramite RCC->APB2ENR, non RCC->AHB1ENR:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; /* abilita clock GPIOC */ RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; /* abilita clock SYSCFG */
2. Configurare il pin GPIO come input
Il GPIO deve essere in modalità input (MODER = 0b00) con pull-up per un pulsante che ha un pull-up esterno. Sulla scheda DISCOVERY, PC13 ha un pull-up interno abilitato dal bootloader, ma lo impostiamo esplicitamente:
GPIOC->MODER &= ~(3U << 26); /* PC13 = 0b00 = input */ GPIOC->PUPDR &= ~(3U << 26); GPIOC->PUPDR |= (1U << 26); /* pull-up */
3. Collegare la porta GPIO all'EXTI via SYSCFG
La linea EXTI 13 deve sapere quale porta fornisce il segnale. Questo avviene in SYSCFG_EXTICR4, che gestisce le linee 12–15. Ogni linea usa 4 bit; la linea 13 occupa i bit 4–7 (il secondo nibble):
/* EXTI13 source = Porta C (0b0010) */ SYSCFG->EXTICR[3] &= ~(0xFU << 4); /* cancella mappatura precedente */ SYSCFG->EXTICR[3] |= (2U << 4); /* PCx */
Il codice porta è: PA=0, PB=1, PC=2, PD=3, PE=4, PF=5, PG=6, PH=7, PI=8. L'indice dell'array è mappato come EXTICR[0] → linee 0–3, [1] → 4–7, [2] → 8–11, [3] → 12–15.
4. Configurare la sensibilità di fronte EXTI
Vogliamo solo il fronte di discesa (la pressione del pulsante va da alto a basso):
EXTI->IMR |= (1U << 13); /* attiva interrupt sulla linea 13 */ EXTI->FTSR |= (1U << 13); /* trigger fronte di discesa */ EXTI->RTSR &= ~(1U << 13); /* fronte di salita disabilitato */
Per entrambi i fronti, imposta sia RTSR che FTSR. Per solo evento (nessun interrupt alla CPU), usa EXTI->EMR invece di IMR — utile per il wakeup da basso consumo senza svegliare il core.
5. Abilitare l'interrupt nel NVIC
Il pin 13 appartiene al gruppo EXTI15_10_IRQn:
NVIC_EnableIRQ(EXTI15_10_IRQn); NVIC_SetPriority(EXTI15_10_IRQn, 5); /* priorità 5 */
6. Scrivere la ISR
La ISR deve cancellare il bit pending in EXTI->PR, altrimenti l'interrupt si riattiva immediatamente all'uscita:
void EXTI15_10_IRQHandler(void)
{
if (EXTI->PR & (1U << 13)) {
/* Pulsante PC13 premuto — gestisci */
EXTI->PR = (1U << 13); /* clear scrivendo 1 */
}
}
Scrivere 1 in EXTI_PR cancella il pending flag. Scrivere 0 non ha effetto. Questo registro è write-1-to-clear (W1C), quindi non usare mai |= su di esso: cancelleresti tutti i pending flag che sono a 1, inclusi quelli di altre linee arrivate tra la lettura e la scrittura. Usa un assegnamento diretto con solo il bit che intendi cancellare.
Esempio pratico: doppio fronte con debounce software
A volte servono entrambi gli eventi di pressione e rilascio — per esempio, per rilevare per quanto tempo un pulsante è stato tenuto premuto. Configuriamo PC13 per entrambi i fronti:
void button_interrupt_init(void)
{
/* Abilitazione clock */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
__DSB(); /* assicura clock attivo */
/* PC13 input con pull-up */
GPIOC->MODER &= ~(3U << 26);
GPIOC->PUPDR &= ~(3U << 26);
GPIOC->PUPDR |= (1U << 26);
/* EXTI13 source = Porta C */
SYSCFG->EXTICR[3] &= ~(0xFU << 4);
SYSCFG->EXTICR[3] |= (2U << 4);
/* Entrambi i fronti */
EXTI->IMR |= (1U << 13);
EXTI->RTSR |= (1U << 13); /* salita — rilascio pulsante */
EXTI->FTSR |= (1U << 13); /* discesa — pressione pulsante */
NVIC_EnableIRQ(EXTI15_10_IRQn);
NVIC_SetPriority(EXTI15_10_IRQn, 5);
}
volatile uint32_t last_edge_time;
volatile uint8_t button_pressed;
void EXTI15_10_IRQHandler(void)
{
if (EXTI->PR & (1U << 13)) {
EXTI->PR = (1U << 13);
uint32_t now = DWT->CYCCNT; /* cycle counter per temporizzazione */
uint32_t delta = now - last_edge_time;
if (delta > (16000000U / 1000 * 50)) { /* > 50 ms di debounce */
last_edge_time = now;
if (GPIOC->IDR & (1U << 13)) {
/* Pin alto — pulsante rilasciato */
button_pressed = 0;
} else {
/* Pin basso — pulsante premuto */
button_pressed = 1;
}
}
}
}
Il debounce legge il registro dati GPIO (GPIOC_IDR) dopo il fronte — sapendo che un fronte è avvenuto, possiamo determinare direttamente lo stato effettivo. Il cycle counter DWT fornisce temporizzazioni con risoluzione microsecondi senza bisogno di un timer dedicato.
Wakeup dalla modalità Stop via EXTI
Le linee EXTI sono il metodo principale per risvegliare lo STM32 dalla modalità Stop. Il percorso di wakeup è leggermente diverso da quello dell'interrupt:
- La linea EXTI deve essere configurata come evento, non solo come interrupt. Imposta il bit corrispondente in
EXTI->EMR(Event Mask Register) oltre aIMR. - Il GPIO deve rimanere alimentato — verifica che la porta sia nel dominio
VDD, nonVPB(backup domain) a meno che non si usi il wakeup via RTC. - Dopo il risveglio, la ISR viene eseguita normalmente. Il PLL e HSI/HSE potrebbero necessitare di riconfigurazione a seconda della variante Stop (Stop 0, Stop 1, Stop 2 sugli STM32 più recenti).
/* Abilita sia interrupt che evento */ EXTI->IMR |= (1U << 13); /* interrupt — per la ISR dopo il risveglio */ EXTI->EMR |= (1U << 13); /* evento — sveglia la CPU da Stop */ EXTI->FTSR |= (1U << 13); /* fronte di discesa */ /* Entra in modalità Stop */ PWR->CR |= PWR_CR_LPDS; /* low-power deep sleep */ SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; __WFI(); /* entra in Stop, si sveglia su evento EXTI */
Senza EXTI_EMR, il fronte di discesa viene memorizzato ma non genera l'evento di wakeup necessario per uscire dalla modalità Stop. L'interrupt scatterà dopo che il core si sarà risvegliato per un altro motivo, vanificando lo scopo.
Linee IRQ condivise: EXTI5_9 e EXTI15_10
Quando più pin condividono un handler IRQ (linee 5–9 o 10–15), la ISR deve controllare quale linea EXTI ha generato l'interrupt leggendo EXTI->PR:
void EXTI15_10_IRQHandler(void)
{
uint32_t pending = EXTI->PR;
if (pending & (1U << 13)) {
EXTI->PR = (1U << 13);
/* gestisci PC13 */
}
if (pending & (1U << 10)) {
EXTI->PR = (1U << 10);
/* gestisci PA10 */
}
}
Leggi sempre PR all'inizio, cancella il bit specifico e dispatch. Non cancellare mai tutti i bit con EXTI->PR = 0xFFFF — potresti perdere una linea che si è attivata subito dopo il controllo del pending.
Checklist pratica
| Passo | Registro | Cosa verificare |
|---|---|---|
| Clock GPIO | RCC->AHB1ENR | Bit impostato per la porta |
| Clock SYSCFG | RCC->APB2ENR | Bit SYSCFGEN impostato |
| Modalità GPIO | GPIOx->MODER | 0b00 (input) |
| Mappatura porta | SYSCFG->EXTICR[n] | Nibble di 4 bit corrisponde al codice porta |
| Maschera interrupt | EXTI->IMR | Bit impostato per la linea EXTI |
| Trigger fronte | EXTI->RTSR/FTSR | Almeno un fronte abilitato |
| NVIC abilitato | NVIC_ISER | Numero IRQ abilitato |
| PR cancellato nella ISR | EXTI->PR | Scrivi 1 sul bit (mai |=) |
| Evento wakeup | EXTI->EMR | Deve essere impostato per wakeup da Stop |
Come lo affronterei su un progetto cliente
Su un firmware di produzione, non dissemino mai la configurazione EXTI in tutto il codebase. Uso una piccola struct exti_config() e una singola funzione di init che prende porta, pin, flag di fronte e priorità NVIC. Questo rende la configurazione verificabile in un unico punto ed evita la sessione di debugging "perché il mio interrupt non scatta" sei mesi dopo, quando qualcuno aggiunge un secondo sensore sulla stessa linea EXTI.
Per il debounce, preferisco l'approccio con cycle counter DWT mostrato sopra rispetto al campionamento basato su timer: nessuna prenotazione di periferica, overhead zero quando non ci sono fronti, e funziona direttamente dalla ISR. L'unico prerequisito è abilitare il DWT in CoreDebug->DEMCR:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
Aggiungo anche un'asserzione statica che la linea EXTI e il NVIC IRQn corrispondano. Niente becca un PC13 mappato su EXTI0_IRQn più velocemente di un controllo a tempo di compilazione.
Fonti
- STM32F4 Reference Manual (RM0090), sezioni 8.2 (SYSCFG) e 13 (EXTI)
- STM32F4xx HAL Driver manual — capitolo periferico EXTI
- ARM Cortex-M3/M4 Generic User Guide (DUI0552) — NVIC e modello eccezioni
- ST Application Note AN4088 — configurazione EXTI e casi d'uso

Domande o correzioni? Scrivimi — rispondo a tutti i messaggi.