In the previous article, we implemented a button-controlled LED toggle using a polling method. While simple and effective for basic projects, polling is inefficient for real-time or low-power embedded systems.
In this article, we will improve the design by configuring the button press to trigger an external interrupt (EXTI) instead of continuously polling the button pin. This approach frees up CPU resources and provides faster response times.
The STM32F446RE Nucleo-64 board provides an onboard LED and user button:
Electrical Behavior:
Thus, when the button is pressed, it generates a falling edge on PC13, which we use to trigger the interrupt.
The objective is to toggle the LED immediately when the button is pressed, using an interrupt triggered on a falling edge.
The project is structured as follows:
/*
* main.c
*
* Created on: Apr 24, 2025
* Author: erraj
*/
#include "stm32f4xx_hal.h"
void SystemClock_Config(void);
void GPIO_Config(void);
void Error_Handler(void);
int main(void) {
/** Initialize HAL Driver*/
HAL_Init();
/** Configure System Clock */
SystemClock_Config();
/** Setup GPIO for LED */
GPIO_Config();
while (1) {
// Main loop does nothing - handled by EXTI
}
}
void GPIO_Config(void) {
__HAL_RCC_GPIOA_CLK_ENABLE(); // LED (PA5)
__HAL_RCC_GPIOC_CLK_ENABLE(); // Button (PC13)
__HAL_RCC_SYSCFG_CLK_ENABLE(); // For EXTI line mapping
GPIO_InitTypeDef GPIO_InitStruct = { 0 };
// Configure PA5 (LED) as Output
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// Configure PC13 (Button) as External Interrupt Input
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// Enable EXTI Line 15 to 10 IRQ (covers PC13)
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = { 0 };
RCC_ClkInitTypeDef RCC_ClkInitStruct = { 0 };
/** Step 1: Enable Power Control clock */
__HAL_RCC_PWR_CLK_ENABLE();
/** Step 2: Set voltage scaling to match 84 MHz operation */
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);
/** Step 3: Configure HSI as PLL source and setup PLL to 84 MHz */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
// PLL Config for SYSCLK = 84 MHz (VCO = 336 MHz)
RCC_OscInitStruct.PLL.PLLM = 16; // HSI = 16 MHz → /16 = 1 MHz
RCC_OscInitStruct.PLL.PLLN = 336; // *336 = 336 MHz
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4; // /4 = 84 MHz (SYSCLK)
RCC_OscInitStruct.PLL.PLLQ = 7; // USB clock = 336 / 7 = 48 MHz ✅
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler(); // Add your error response here
}
/** Step 4: Select PLL as system clock and configure bus clocks */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // 42 MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // 84 MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
Error_Handler(); // Add your error response here
}
}
// Callback function called by HAL when EXTI13 triggers
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_13) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Toggle LED
}
}
void Error_Handler(void) {
// Simple error handler: blink LED rapidly or stay in infinite loop
while (1) {
// Optional: Toggle LED to indicate error
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(100); // Fast blink indicates error
}
}
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file stm32f4xx_it.c
* @brief Interrupt Service Routines.
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f4xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN TD */
/* USER CODE END TD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/* External variables --------------------------------------------------------*/
/* USER CODE BEGIN EV */
/* USER CODE END EV */
/******************************************************************************/
/* Cortex-M4 Processor Interruption and Exception Handlers */
/******************************************************************************/
/**
* @brief This function handles Non maskable interrupt.
*/
void NMI_Handler(void)
{
/* USER CODE BEGIN NonMaskableInt_IRQn 0 */
/* USER CODE END NonMaskableInt_IRQn 0 */
/* USER CODE BEGIN NonMaskableInt_IRQn 1 */
while (1)
{
}
/* USER CODE END NonMaskableInt_IRQn 1 */
}
/**
* @brief This function handles Hard fault interrupt.
*/
void HardFault_Handler(void)
{
/* USER CODE BEGIN HardFault_IRQn 0 */
/* USER CODE END HardFault_IRQn 0 */
while (1)
{
/* USER CODE BEGIN W1_HardFault_IRQn 0 */
/* USER CODE END W1_HardFault_IRQn 0 */
}
}
/**
* @brief This function handles Memory management fault.
*/
void MemManage_Handler(void)
{
/* USER CODE BEGIN MemoryManagement_IRQn 0 */
/* USER CODE END MemoryManagement_IRQn 0 */
while (1)
{
/* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
/* USER CODE END W1_MemoryManagement_IRQn 0 */
}
}
/**
* @brief This function handles Pre-fetch fault, memory access fault.
*/
void BusFault_Handler(void)
{
/* USER CODE BEGIN BusFault_IRQn 0 */
/* USER CODE END BusFault_IRQn 0 */
while (1)
{
/* USER CODE BEGIN W1_BusFault_IRQn 0 */
/* USER CODE END W1_BusFault_IRQn 0 */
}
}
/**
* @brief This function handles Undefined instruction or illegal state.
*/
void UsageFault_Handler(void)
{
/* USER CODE BEGIN UsageFault_IRQn 0 */
/* USER CODE END UsageFault_IRQn 0 */
while (1)
{
/* USER CODE BEGIN W1_UsageFault_IRQn 0 */
/* USER CODE END W1_UsageFault_IRQn 0 */
}
}
/**
* @brief This function handles System service call via SWI instruction.
*/
void SVC_Handler(void)
{
/* USER CODE BEGIN SVCall_IRQn 0 */
/* USER CODE END SVCall_IRQn 0 */
/* USER CODE BEGIN SVCall_IRQn 1 */
/* USER CODE END SVCall_IRQn 1 */
}
/**
* @brief This function handles Debug monitor.
*/
void DebugMon_Handler(void)
{
/* USER CODE BEGIN DebugMonitor_IRQn 0 */
/* USER CODE END DebugMonitor_IRQn 0 */
/* USER CODE BEGIN DebugMonitor_IRQn 1 */
/* USER CODE END DebugMonitor_IRQn 1 */
}
/**
* @brief This function handles Pendable request for system service.
*/
void PendSV_Handler(void)
{
/* USER CODE BEGIN PendSV_IRQn 0 */
/* USER CODE END PendSV_IRQn 0 */
/* USER CODE BEGIN PendSV_IRQn 1 */
/* USER CODE END PendSV_IRQn 1 */
}
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
/******************************************************************************/
/* STM32F4xx Peripheral Interrupt Handlers */
/* Add here the Interrupt Handlers for the used peripherals. */
/* For the available peripheral interrupt handler names, */
/* please refer to the startup file (startup_stm32f4xx.s). */
/******************************************************************************/
/* USER CODE BEGIN 1 */
/**
* @brief This function handles EXTI line[15:10] interrupts.
*/
void EXTI15_10_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}
/* USER CODE END 1 */
The main function initializes the HAL drivers, configures the system clock, and setups the GPIO pins.
Unlike the polling method, the infinite loop remains empty because the event handling is now interrupt-driven.
int main(void) {
HAL_Init();
SystemClock_Config();
GPIO_Config();
while (1) {
// Main loop does nothing - everything handled by interrupt
}
}
In the super loop (while(1)
), there is no code for polling. The system will wait idle until an interrupt occurs.
The GPIO_Config()
function sets up the necessary hardware peripherals:
void GPIO_Config(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
// Configure PA5 as output
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// Configure PC13 as external interrupt input
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// Enable interrupt for EXTI line 15 to 10
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
Key points to note:
The SystemClock_Config()
function configures the system to run at 84 MHz using HSI and PLL.
void SystemClock_Config(void) {
// System clock setup
}
The file stm32f4xx_it.c
handles all core exceptions and peripheral interrupts.
The important interrupt handler implemented is:
void EXTI15_10_IRQHandler(void) {
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}
HAL_GPIO_EXTI_IRQHandler()
internally clears the interrupt flag and calls the user-defined callback HAL_GPIO_EXTI_Callback()
.The actual action (LED toggling) is implemented inside the user callback:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_13) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
In case of a system fault or clock configuration failure, the Error_Handler toggles the LED rapidly to indicate a fault:
void Error_Handler(void) {
while (1) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(100);
}
}
Peripheral | Pin | Mode | Notes |
---|---|---|---|
LED (LD2) | PA5 | Output, Push-Pull | Active High, toggled manually |
Button (B1) | PC13 | Input, External Interrupt | Falling edge triggers EXTI |
Compared to polling, interrupt-driven designs offer several advantages:
This interrupt-driven design is the recommended approach for real-time and embedded system projects.
This article demonstrated how to implement external interrupt handling on the STM32F446RE microcontroller to toggle an LED on a button press.
Instead of constantly polling the button status, the system now reacts immediately and efficiently to user actions using external interrupts.
This lays the foundation for developing more responsive and energy-efficient embedded applications.
In future articles, we will explore techniques like software debouncing to make button handling more reliable in real-world conditions.