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).
The previous tutorials for this series:
The Github for the current tutorial with full files and code:
IDE:
Board:
Pressing the blue user button PA0 on the STM32F3Discovery toggles between two LED modes:
TIM2 drives the timing as in the previous tutorial.
The button press is handled by an EXTI interrupt, so no polling, no delay loop.
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:
Each line can be configured to trigger on:
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.).
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
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).
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.
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.
$ 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"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 ![]()
Add new comment