Embedded C track

Lesson 18 · Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)

NVIC Interrupts & EXTI

EXTI and clearing pending flags

Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)6 min readFree lesson

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

  1. Project → New µVision Project… → name it stm32_exti.
  2. Device: STM32F103C8.
  3. Manage Run-Time Environment → CMSIS:CORE, Device:Startup.
  4. After adding main.c, copy and paste.
  5. Apply STM32F10X_MD to C/C++ → Define.
  6. Debug → Use Simulator, -pSTM32F103C8.
  7. 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 enters EXTI0_IRQHandler (check with a breakpoint).
  • The handler clears EXTI->PR and toggles PC13. After PORTA &= ~0x0001, it goes to |= 0x0001 and 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.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/stm32_exti/main.cC
#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*/
    }
}
src/stm32_exti/pc_test.cC
#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;
}

Assignment

Problem 1 is pure logic (PC gcc), Problem 2 is MDK (STM32F103C8, μVision simulator).


Problem 1 — EXTI Line → EXTICR Index/Shift Calculation

  • File name: homework_01/main.c
  • Key concepts: EXTICR[n/4], shift (n%4)*4
  • Platform: Common (PC gcc)

Requirements

  • Calculate and print which EXTICR array index and bit shift to use for pin numbers (EXTI lines) 0, 3, 4, 7, 13.
  • Index = n / 4, shift = (n % 4) * 4.
  • The format EXTIn -> EXTICR[i], shift s must match the expected output character by character.

Expected output

EXTI0 -> EXTICR[0], shift 0
EXTI3 -> EXTICR[0], shift 12
EXTI4 -> EXTICR[1], shift 0
EXTI7 -> EXTICR[1], shift 12
EXTI13 -> EXTICR[3], shift 4

Hint

  • idx = n / 4; shift = (n % 4) * 4;.
  • EXTI13 is 13/4=3, (13%4)*4 = 1*4 = 4.

Problem 2 — EXTI1(PA1) Falling Edge Interrupt

  • File name: homework_02/main.c
  • Key concepts: falling edge (FTSR), pull-up, EXTI1 line
  • Platform: MDK (STM32F103C8)

Requirements

  • Set PA1 as input + internal pull-up (button from PA1 to GND, press for falling edge), catch falling edge with EXTI1.
  • Toggle PC13 LED on each interrupt.

Expected Operation (Simulator)

  • When PORTA &= ~0x0002 (PA1=0, falling edge) is generated in the Command window, EXTI1_IRQHandler is called and PC13 toggles.
  • EXTI FTSR bit1=1, IMR bit1=1, EXTI1 Enabled in NVIC.

Hint

  • Mapping: AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI1; (EXTI1 also EXTICR[0], 0=PA).
  • Trigger: EXTI->FTSR |= EXTI_FTSR_TR1; Mask: EXTI->IMR |= EXTI_IMR_MR1;.
  • NVIC_EnableIRQ(EXTI1_IRQn);, handler is EXTI1_IRQHandler, clear EXTI->PR = EXTI_PR_PR1;.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>

/*
 *Answer 1 — EXTI line → EXTICR index/shift calculation
 *
 *Key Points:
 *- AFIO->EXTICR is an array of 4, and each register holds 4 lines with 4 bits each.
 *- Line n: index = n/4, shift = (n%4)*4.
 *
 *Common mistake:
 *- Swapping %4 and /4. Index is division, shift is remainder × 4.
 */

int main(void)
{
    int lines[5] = { 0, 3, 4, 7, 13 };
    int i;

    for (i = 0; i < 5; i++) {
        int n = lines[i];
        int idx = n / 4;
        int shift = (n % 4) * 4;
        printf("EXTI%d -> EXTICR[%d], shift %d\n", n, idx, shift);
    }
    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Correct answer 2 (MDK, STM32F103C8) — EXTI1(PA1) falling edge interrupt
 *
 *Key Points:
 *- Connect the button to PA1~GND with pull-up (ODR=1) → normally 1, 0 when pressed (falling edge).
 *- Set falling edge with FTSR. Since it's line 1, use 1 for IMR/PR/IRQn all.
 *
 *Common mistake:
 *- Line number mismatch (using TR0/PR0/EXTI0_IRQn for EXTI1).
 *- Pending clear missing (EXTI->PR = EXTI_PR_PR1) → infinite re-entry.
 *
 *Expected behavior (simulator): Enter EXTI1_IRQHandler on each PA1 falling edge, toggle PC13.
 */

#define BTN_PIN   1u     /* PA1 / EXTI1 */
#define LED_PIN   13u    /* PC13 */

void EXTI1_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR1) {
        EXTI->PR = EXTI_PR_PR1;          /*Clear pending*/
        GPIOC->ODR ^= (1u << LED_PIN);   /*Toggle LED*/
    }
}

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;

    /*PA1: input + pull-up (button PA1~GND, falling when pressed)*/
    GPIOA->CRL &= ~(0xFu << (BTN_PIN * 4u));
    GPIOA->CRL |=  (0x8u << (BTN_PIN * 4u));
    GPIOA->ODR |=  (1u << BTN_PIN);                   /*ODR=1 → Pull-up*/

    GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));
    GPIOC->CRH |=  (0x2u << ((LED_PIN - 8u) * 4u));

    AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI1;           /* EXTI1 ← PA1 */

    EXTI->FTSR |= EXTI_FTSR_TR1;                      /*Falling edge*/
    EXTI->IMR  |= EXTI_IMR_MR1;                        /*Unmask*/

    NVIC_EnableIRQ(EXTI1_IRQn);

    while (1) {
    }
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now