Electronics - STM32 - User button as EXTI with the STM32F3Discovery board

Santa is impressed by the rotating LEDs but now he wants to control them.

Let's add a push button with the EXTI (External Interrupt).

Shopping list

The previous tutorials for this series:

The Github for the current tutorial with full files and code:

IDE:

  • VSCode
  • DevContainer extension

Board:

What this tutorial does

Pressing the blue user button PA0 on the STM32F3Discovery toggles between two LED modes:

  • MODE_ROTATE: one LED rotates clockwise every 500ms
  • MODE_BLINK: all 8 LEDs blink together every 500ms

TIM2 drives the timing as in the previous tutorial.

The button press is handled by an EXTI interrupt, so no polling, no delay loop.

Three new concepts

EXTI - External Interrupt/Event Controller

On STM32, any GPIO pin can trigger an interrupt when its signal changes.

EXTI is the peripheral that manages these external interrupt lines.

There are 16 EXTI lines (0 to 15), one per pin number, so:

  • EXTI line 0 handles pin 0 of any GPIO port (PA0, PB0, PC0, etc.),
  • EXTI line 1 handles pin 1 of any GPIO port (PA1, PB1, PC1, etc.),
  • And so on.

Each line can be configured to trigger on:

  • Rising edge: Signal goes from 0 to 1 (button press on an active-high button)
  • Falling edge: Signal goes from 1 to 0 (button release, or active-low button)
  • Both edges

Furthermore we have to tell the MCU which GPIO port (A, B, C, etc.) to use with the pin (0, 1, 2, etc.) corresponding to each EXTI line (0, 1, 2, etc.). 

This is done via SYSCFG.

SYSCFG - System Configuration Controller

 

Since EXTI line 0 can be connected to PA0, PB0, PC0, PE0, etc., something must decide which port is actually routed to the line.

That is SYSCFG's job.

SYSCFG has 4 EXTICR registers (External Interrupt Configuration Registers).

There are 4 EXTICR registers, each covering 4 EXTI lines.

So 16 EXTI lines in total, one per pin number (0 to 15).

For our button on PA0:

SYSCFG->EXTICR[0] &= ~(0xFUL << 0);
SYSCFG->EXTICR[0] |=  (0x0UL << 0);  // 0x00 = GPIOA

Pull-down resistor

The user button on the STM32F3Discovery is wired between PA0 and VDD (3.3V).

By default, PA0 is floating (its value is undefined).

But we need to read 0 when the button is not pressed and 1 when pressed.

To achieve this goal we have to configure a pull-down resistor to hold PA0 to 0V.

And when the button is pressed, VDD overrides the pull-down and the pin reads 1.

Indeed the pull-down resistor connects PA0 to GND through a high-value resistor (internally ~40kΩ on STM32).

GPIOA->PUPDR &= ~(0x3UL << (BUTTON_PIN * 2));
GPIOA->PUPDR |=  (GPIO_PUPDR_PULLDOWN << (BUTTON_PIN * 2));

We multiply by 2 the value of BUTTON_PIN because we have 2 bits by pin.

Here BUTTON_PIN is equal to 0, so it won't change anything but if the value was 4 the final value would have been 8 (because 4 * 2 = 8).

And we insert the value 2 (10 in binary) in the right register bits by first clearing the 2 bits with the mask (~ operator), then setting 10 for pull-down (indeed, GPIO_PUPDR_PULLDOWN value is 10 in binary).

The EXTI initialization chain

Five steps are required, in order:

// 1. Enable GPIOA clock (button pin)
RCC->AHBENR |= RCC_AHBENR_IOPAEN;

// 2. Enable SYSCFG clock
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

// 3. Route EXTI line 0 to GPIOA via SYSCFG
SYSCFG->EXTICR[0] &= ~(0xFUL << 0);
SYSCFG->EXTICR[0] |=  (0x0UL << 0);

// 4. Configure EXTI line 0: unmask + rising edge trigger
EXTI->IMR  |=  (1UL << 0);   // unmask to allow interrupt
EXTI->RTSR |=  (1UL << 0);   // trigger on rising edge
EXTI->FTSR &= ~(1UL << 0);   // not on falling edge

// 5. Enable EXTI0 in NVIC
NVIC_EnableIRQ(EXTI0_IRQn);  // IRQ number 6 on STM32F303

Skipping any step means the interrupt never fires.

The interrupt handler

void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & (1UL << 0)) {
        EXTI->PR = (1UL << 0);  // Clear pending flag — mandatory

        if (mode == MODE_ROTATE) {
            mode = MODE_BLINK;
            leds_all_off();
        } else {
            mode = MODE_ROTATE;
            leds_all_off();
        }
    }
}

Two things worth noting:

Clearing the pending flag is mandatory just like TIM2's UIF flag.

Writing 1 to the corresponding bit in EXTI->PR clears it.

If not cleared, the interrupt fires again immediately.

EXTI->PR is cleared by writing 1, not 0.

This is the opposite of most registers.

It is a write-1-to-clear register, a common pattern in STM32 status registers.

Build and flash

$ cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake -DCMAKE_BUILD_TYPE=Debug -G Ninja
$ ninja -C build
$ openocd -f interface/stlink.cfg \
          -f target/stm32f3x.cfg \
          -c "program build/stm32f3discovery.bin verify reset exit 0x08000000"

Conclusion

Press the blue button on the board to toggle between rotation and blink mode.

The CPU never polls the button.

It sleeps with wfi and wakes up only when TIM2 or EXTI0 fires.

Two interrupts, one CPU, zero busy loops.

The elves are already testing it enlightened

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.