So far, buttons have been read using the polling method, where the main loop continuously checks them. However, if the CPU only watches the button, it cannot do other tasks and may miss short inputs. Interrupts reverse this. Normally, the main code does its work, but when an event occurs (pin change, timer expiration, etc.), the CPU immediately stops what it is doing, jumps to the handler (ISR), and then returns.
In Cortex-M, the entity that manages interrupts is the NVIC (Nested Vectored Interrupt Controller), and the peripheral that converts changes on external pins into interrupts is the EXTI (External Interrupt/Event Controller). In this chapter, we take a button press on PA0 as an EXTI0 interrupt to toggle the LED.
Learning Objectives
- Explain the differences, advantages, and disadvantages of polling and interrupts.
- Understand the role of the NVIC and
NVIC_EnableIRQ(). - Convert the pin edge (Up/Down) into an interrupt using EXTI.
- Map the pin to an EXTI line using AFIO->EXTICR.
- Write the ISR and correctly clear the pending flag (PR).
Target Environment
- Compiler: Keil MDK-ARM (Arm Compiler 6).
- Device: STM32F103C8 (μVision simulator).
- Verification: μVision simulator (EXTI/NVIC registers, force PA0 input, observe PC13). ISR logic is PC
gcc(pc_test.c).
Key Concepts
1) Polling vs Interrupt
| Method | Operation | Advantages | Disadvantages |
|---|---|---|---|
| Polling | Continuously check in a loop | Simple, flow clear | CPU waste, may miss short events |
| Interrupt | Automatically called on event | Immediate response, CPU available | Synchronization caution needed |
2) NVIC
NVIC is an interrupt controller built into the Cortex-M core, managing the Activation/Priority/Pending of each interrupt. CMSIS provides functions.
NVIC_EnableIRQ(EXTI0_IRQn); /*Interrupt Activation*/
NVIC_SetPriority(EXTI0_IRQn, 2); /*Priority (smaller number means higher)*/
3) EXTI — Pin changes as interrupts
EXTI has 16 lines (EXTI0~15), and detects Up/Down edge on each line to trigger interrupts. Main registers:
| Register | Function |
|---|---|
IMR |
Interrupt mask (allow interrupt on the line) |
RTSR |
Rising edge trigger |
FTSR |
Falling edge trigger |
PR |
Pending (occurred) flag — write 1 to clear |
4) AFIO->EXTICR — Pin→Line mapping
EXTI line n can be connected to one of the pin n of several ports. Which port is selected by AFIO->EXTICR[]. The setting of line n is 4 bits at (n%4)*4 of EXTICR[n/4] (0=PA, 1=PB, 2=PC…).
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0; /*EXTI0 ← PA0 (Field 0)*/
EXTI mapping requires the AFIO clock (
RCC_APB2ENR_AFIOEN).
5) ISR and pending clear
The interrupt handler name must match the startup vector table (e.g., EXTI0_IRQHandler). You must clear the PR inside the handler. Otherwise, the same interrupt will trigger again as soon as you exit the handler, causing an infinite loop.
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR0) {
EXTI->PR = EXTI_PR_PR0; /*You need to use 1 to clear it*/
/*... processing ...*/
}
}
Creating a μVision Project
Project → New µVision Project…→ name itstm32_exti.- Device: STM32F103C8.
Manage Run-Time Environment→ CMSIS:CORE, Device:Startup.- After adding
main.c, copy and paste. - Apply
STM32F10X_MDtoC/C++ → Define. Debug → Use Simulator,-pSTM32F103C8.- Rebuild(F7) → Debug(Ctrl+F5).
Examples
Example 1 — main.c: Toggle LED with EXTI0
#include "stm32f10x.h"
/*
*Lesson 18 Example — LED toggling with EXTI0 (PA0) external interrupt.
*Target: STM32F103C8, Compiler: Keil MDK-ARM.
*
*Connect the button between PA0 and VCC and enable the internal pull-down (normally 0, 1 when pressed).
*The 'rising edge' the moment it is pressed is detected by EXTI line 0, triggering an interrupt.
*The main loop does nothing, and the toggle is handled by the interrupt handler.
*/
#define BTN_PIN 0u /* PA0 / EXTI0 */
#define LED_PIN 13u /* PC13 */
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR0) { /*Line 0 pending?*/
EXTI->PR = EXTI_PR_PR0; /*Clear the pending by writing 1*/
GPIOC->ODR ^= (1u << LED_PIN); /*Toggle LED*/
}
}
int main(void)
{
/*Clocks: GPIOA, GPIOC, and AFIO needed for EXTI mapping*/
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
/*PA0: input + pull-down (button between PA0 and VCC, rising edge when pressed)*/
GPIOA->CRL &= ~(0xFu << (BTN_PIN * 4u));
GPIOA->CRL |= (0x8u << (BTN_PIN * 4u));
GPIOA->ODR &= ~(1u << BTN_PIN); /*ODR = 0 → pulldown*/
/*PC13: push-pull output 2MHz*/
GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));
GPIOC->CRH |= (0x2u << ((LED_PIN - 8u) * 4u));
/*Connect EXTI0 line to PA0 (EXTICR1 EXTI0 field = 0 → Port A)*/
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
/*Line 0: rising edge trigger + unmask interrupt*/
EXTI->RTSR |= EXTI_RTSR_TR0;
EXTI->IMR |= EXTI_IMR_MR0;
/*Enable EXTI0 interrupt in NVIC*/
NVIC_EnableIRQ(EXTI0_IRQn);
while (1) {
/*Main is idle — button handling is done by the interrupt*/
}
}
Example 2 — pc_test.c: Check ISR handling logic
#include <stdio.h>
#include <stdint.h>
/*
*Lecture 18 PC Verification — Mimicking EXTI Interrupt Handling Logic.
*Every time an edge occurs, the ISR toggles the LED and increments the counter.
*(In actual hardware, the pending flag must be cleared for the next interrupt to occur.)
*/
int main(void)
{
int events[8] = { 1, 0, 1, 1, 0, 1, 0, 1 }; /*1 = Edge Occurrence*/
uint32_t count = 0;
int led = 0;
int i;
for (i = 0; i < 8; i++) {
if (events[i]) { /*Interrupt Occurrence*/
count++;
led = !led; /*ISR Operation: LED Toggle*/
}
printf("t=%d event=%d -> count=%lu LED=%s\n",
i, events[i], (unsigned long)count, led ? "ON" : "OFF");
}
return 0;
}
Checking with Simulator
- During debugging, if you execute
PORTA |= 0x0001(PA0=1, rising edge) in the Command window, the EXTI0 interrupt triggers and entersEXTI0_IRQHandler(check with a breakpoint). - The handler clears
EXTI->PRand toggles PC13. AfterPORTA &= ~0x0001, it goes to|= 0x0001and toggles again. - Check in the NVIC window if EXTI0 is Enabled, and if IMR/RTSR bit0 in the EXTI register is 1.
- Expected behavior: Each rising edge of PA0 toggles PC13 (without polling).
Verification on a PC
gcc pc_test.c -o pc_test && ./pc_test
Expected output (stdout):
t=0 event=1 -> count=1 LED=ON
t=1 event=0 -> count=1 LED=ON
t=2 event=1 -> count=2 LED=OFF
t=3 event=1 -> count=3 LED=ON
t=4 event=0 -> count=3 LED=ON
t=5 event=1 -> count=4 LED=OFF
t=6 event=0 -> count=4 LED=OFF
t=7 event=1 -> count=5 LED=ON
Common Mistakes
Q. The interrupt triggers once but I can never exit the handler.
A. You did not clear EXTI->PR. PR must be cleared by writing 1 (EXTI->PR = EXTI_PR_PR0;). If you skip clearing, it will re-enter the handler immediately after exiting.
Q. The handler is never called.
A. Check three things: ① RCC_APB2ENR_AFIOEN (AFIO clock), ② EXTI->IMR unmask, ③ NVIC_EnableIRQ. Also, make sure the handler name is exactly EXTI0_IRQHandler.
Q. It is PA0, but how do I set EXTICR?
A. Line n is the 4-bit position (n%4)*4 of EXTICR[n/4]. EXTI0 is the lower 4 bits of EXTICR[0]; if the value is 0, it is PA0.
Q. Can time-consuming tasks be done inside an ISR?
A. No. Keep the ISR short. The standard pattern is to just set a flag for long tasks and handle them in the main loop (used in episode 22 with the ring buffer).
Summary
- Interrupts are automatically called when an event occurs, making efficient use of the CPU.
- NVIC manages interrupts in general and enables them via
NVIC_EnableIRQ. - EXTI converts a pin edge into an interrupt and controls IMR/RTSR/FTSR/PR.
- Pin-to-line mapping is done with AFIO->EXTICR (AFIO clock required).
- Always clear the pending (PR) in the ISR, and keep the handler short.
Try it yourself
Solve the assignment below. The reference solution is shown below.