要了解按键中断, 就要对芯片整体的中断系统有准确的把握
中断系统
在Cortex-M内核中, 中断属于异常(Exception)的子集, 所以描述中断时常用异常进入(Exception entry)异常退出(Exception exit)来描述中断进入和退出.
整个中断系统由如下两个控制器负责实现
- NVIC(Nested Vectored Interrupt Controller ) 嵌套向量中断控制器, 负责给所有的中断排队, 分优先级, 然后按照优先级跳转到对应的中断处理函数中执行中断逻辑
- EXTI(Extended interrupts and events controller ) 外部中断/事件控制器, 属于外设里的传感器, 负责监测引脚点评变化, 一旦引脚变化产生, 就将对应信号传递到NVIC进行中断处理.
中断源总共分为三类:
- 第一类, 内核异常(Internal Exceptions)
由SysTick滴答定时器, Reset, HardFault触发, 直接通往NVIC处理 - 第二类, 通信与定时外设(Peripherals)
由SPI, USART串口, I2C, 定时器, ADC等通信信号触发, 这些信号控制器有自己独立的逻辑, 产生信号后直接发给NVIC. - 第三类: 外部IO中断(External Interrupts)
由GPIO引脚触发, 引脚信号经EXTI传递给NVIC进行处理
中断的核心就是NVIC控制器, EXTI唯一的职责就是监测GPIO引脚的电平变化, 然后将触发的中断信号传递给NVIC.
中断优先级
中断优先级的判定并不是直观的理解, 优先级数字越小, 就越优先被触发. 这里面涉及到一个中断优先级分组(Priority Grouping)的概念.
STM32中优先级寄存器中只有4个bit位负责优先级, 这4位又可以分为两组, 一组名称为抢占优先级, 另一组名称为响应优先级, 然后两组优先级各有不同的排序逻辑.
抢占优先级(Preemption Priority) 逻辑: 优先级高的打断优先级低的
响应优先级/子优先级(Sub-priority) 逻辑: 优先级低的排在优先级高的后面, 不打断, 只排队
优先级分组共有下图中5种组合

Group 0: 只排队, 不打断
Group 1: 有1位负责抢占, 中断最多可以嵌套2层, 剩余3位决定排队先后
Group 2: 有2位负责抢占, 中断最多可以嵌套4层, 剩余2位决定排队先后
Group 3: 有3位负责抢占, 中断最多可以嵌套8层, 剩余1位决定排队先后
Group 4: 有4位负责抢占, 中断最多可以嵌套16层, 所有中断发生时只打断, 不排队
我们直觉上的中断优先级其实只属于Group 4的情况, 数字越小, 优先级越高的越优先执行
重点: 全局只允许有一套优先级分组规则, 程序启动时, 就先设定好优先级分组是那种, 后续所有的中断都将按照这组优先级逻辑进行执行裁定.
一般, 如果要追求系统实时性, 会选择Group 4, 全抢占. 但要注意如果中断嵌套层数太多, 会有栈溢出风险.
中断相关的关键词及含义
Level and pulse detection : 电平检测和脉冲(上升沿和下降沿)检测
Dynamic reprioritization of interrupts : 动态配置中断优先级
Grouping of priority values into group priority and subpriority fields : 优先级数值划分为抢占优先级和响应优先级两个字段
Interrupt tail-chaining 中断尾链功能
An external Non-maskable interrupt (NMI) 一路外部不可屏蔽中断
WFE(Wait For Event) 等待事件, 只要有一个唤醒信号, 就能让CPU开始工作, 不一定要执行终端函数
WFI (Wait For Interrupt) 等待中断, 必须要能够跳转中断函数才能唤醒CPU
按键中断实现
按键原理图如下:


CubeMX配置生成按键中断初始化代码
根据原理图, 设置PD8, PD9, PD10为EXTI外部中断线
然后配置页面中选择下降沿触发检测, 也就是按键按下时触发操作

下一步统一配置NVIC中断优先级. 点击右侧System View, 点击下方NVIC, 中间出现中断优先级设置界面. 左上角选择中断优先级分组, 我这里选择2位抢占, 2位响应

根据手册 RM0351 Reference manual 描述, EXTI 第5-9线的中断被合并到了EXTI9_5寄存器中,
EXTI第10-15线的中断被合并到了EXTI15_10寄存器中.
所以PD8, PD9的中断, 要在CubeMX中勾选 EXTI Line[9:5] interrupts
PD10的中断, 要在CubeMX中勾选 EXTI Line[15:10] interrupts

这里设置EXTI Line[9:5]和EXTI Line[15:10]的抢占优先级相同, 但响应优先级不同, 来区分他们, 这也是中断优先级的意义所在
生成代码
生成的统一配置优先级分组的代码在文件 stm32l4xx_hal_msp.c中
c
void HAL_MspInit(void)
{
/* USER CODE BEGIN MspInit 0 */
/* USER CODE END MspInit 0 */
__HAL_RCC_SYSCFG_CLK_ENABLE();
__HAL_RCC_PWR_CLK_ENABLE();
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
/* System interrupt init*/
/* USER CODE BEGIN MspInit 1 */
/* USER CODE END MspInit 1 */
}
然后配置3个GPIO引脚的中断使能, 以及配置中断优先级的代码在gpio.c中
c
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOE, LED_R_Pin|LED_G_Pin|LED_B_Pin, GPIO_PIN_SET);
/*Configure GPIO pin : BEEP_Pin */
GPIO_InitStruct.Pin = BEEP_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(BEEP_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pins : LED_R_Pin LED_G_Pin LED_B_Pin */
GPIO_InitStruct.Pin = LED_R_Pin|LED_G_Pin|LED_B_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
/*Configure GPIO pins : KEY2_Pin KEY1_Pin KEY0_Pin */
GPIO_InitStruct.Pin = KEY2_Pin|KEY1_Pin|KEY0_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
中断处理函数在 stm32l4xx_it.c中
c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file stm32l4xx_it.c
* @brief Interrupt Service Routines.
******************************************************************************
* @attention
*
* Copyright (c) 2026 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 "stm32l4xx_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 Prefetch 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 */
}
/******************************************************************************/
/* STM32L4xx 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_stm32l4xx.s). */
/******************************************************************************/
/**
* @brief This function handles EXTI line[9:5] interrupts.
*/
void EXTI9_5_IRQHandler(void)
{
/* USER CODE BEGIN EXTI9_5_IRQn 0 */
/* USER CODE END EXTI9_5_IRQn 0 */
/* 由于PD8和PD9的优先级是相同的, 在代码中,
代码的先后顺序再一次区分了它们的优先级,
如果要调整优先级, 可以调整这里的代码顺序 */
HAL_GPIO_EXTI_IRQHandler(KEY2_Pin);
HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
/* USER CODE BEGIN EXTI9_5_IRQn 1 */
/* USER CODE END EXTI9_5_IRQn 1 */
}
/**
* @brief This function handles EXTI line[15:10] interrupts.
*/
void EXTI15_10_IRQHandler(void)
{
/* USER CODE BEGIN EXTI15_10_IRQn 0 */
/* USER CODE END EXTI15_10_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(KEY0_Pin);
/* USER CODE BEGIN EXTI15_10_IRQn 1 */
/* USER CODE END EXTI15_10_IRQn 1 */
}
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
然后我们通过自定义void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)回调函数, 来实现按钮按下所触发的逻辑.
在这里, 我们要实现一个按键消抖逻辑, 实现稳定的按钮按下一次就触发一次逻辑的功能
自定义key.h和key.c文件
key.h
c
#ifndef KEY_H
#define KEY_H
#include <stdint.h>
#include <stdbool.h>
typedef enum _KEY_PUSH_BUTTON_ {
BTN_KEY_0,
BTN_KEY_1,
BTN_KEY_2
} KEY_PUSH_BUTTON;
typedef struct _KeyPushEvent_ {
KEY_PUSH_BUTTON btn;
} KeyPushEvent;
// ==================== 队列配置(根据需求修改) ====================
#define QUEUE_MAX_LEN 16 // 队列最大长度,建议2的幂,可根据需求调整
#define KEY_DATA_TYPE KeyPushButton// 队列元素类型,存储按键按下的时间戳(如ms级)
// =================================================================
// 循环队列结构体
typedef struct {
KeyPushEvent buffer[QUEUE_MAX_LEN];
uint8_t front;
uint8_t rear;
uint8_t count;
} KeyEventQueue;
extern KeyEventQueue key_queue;
// 初始化按键事件队列
void key_event_queue_init(void);
// 获取按键事件
bool key_event_queue_pop(KeyPushEvent *data);
// BUTTON KEY 0 按下事件处理
void button_key_0_pressed(void);
// BUTTON KEY 1 按下事件处理
void button_key_1_pressed(void);
// BUTTON KEY 2 按下事件处理
void button_key_2_pressed(void);
#endif
key.c
c
#include "key.h"
#include "gpio.h"
#include "main.h"
#include "trace.h"
// 全局队列实例(用于按键中断和主循环共享)
static KeyEventQueue key_queue;
/**
* @brief 初始化循环队列
*/
void key_event_queue_init(void) {
key_queue.front = 0;
key_queue.rear = 0;
key_queue.count = 0;
}
/**
* @brief 判断队列是否为空
* @return true-空,false-非空
*/
bool key_event_queue_is_empty(void) {
return (key_queue.count == 0);
}
/**
* @brief 判断队列是否为满
* @return true-满,false-未满
*/
bool key_event_queue_is_full(void) {
return (key_queue.count == QUEUE_MAX_LEN);
}
/**
* @brief 队列入队(中断中调用)
* @param data 要入队的按键时间戳
* @return true-入队成功,false-队列满失败
*/
bool key_event_queue_push(KeyPushEvent data) {
// 进入临界区:关闭总中断,防止中断与主循环同时操作队列
__disable_irq();
bool ret = false;
if (!key_event_queue_is_full()) {
key_queue.buffer[key_queue.rear] = data;
key_queue.rear = (key_queue.rear + 1) % QUEUE_MAX_LEN;
key_queue.count++;
ret = true;
}
// 退出临界区:开启总中断
__enable_irq();
return ret;
}
/**
* @brief 队列出队(主循环中调用)
* @param data 出队数据的存储地址
* @return true-出队成功,false-队列空失败
*/
bool key_event_queue_pop(KeyPushEvent *data) {
if (data == NULL) return false;
// 进入临界区:关闭总中断(防止出队时中断写入)
__disable_irq();
bool ret = false;
if (!key_event_queue_is_empty()) {
*data = key_queue.buffer[key_queue.front];
key_queue.front = (key_queue.front + 1) % QUEUE_MAX_LEN;
key_queue.count--;
ret = true;
}
// 退出临界区:开启总中断
__enable_irq();
return ret;
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
static uint32_t prev_tick = 0;
uint32_t cur_tick = HAL_GetTick();
if (cur_tick - prev_tick < 50) {
return;
}
prev_tick = cur_tick;
KeyPushEvent k_event = {.btn = BTN_KEY_0};
switch (GPIO_Pin)
{
case KEY0_Pin:
k_event.btn = BTN_KEY_0;
break;
case KEY1_Pin:
k_event.btn = BTN_KEY_1;
break;
case KEY2_Pin:
k_event.btn = BTN_KEY_2;
break;
default:
break;
}
key_event_queue_push(k_event);
}
/**
* @brief BUTTON KEY 0 按下后, 耗时操作
*/
void button_key_0_pressed() {
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_SET);
static uint8_t i = 0;
i++;
switch (i % 3)
{
case 0:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_RESET);
TRACE_DEBUG("Key 0 been pressed, turn red on");
break;
case 1:
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_RESET);
TRACE_DEBUG("Key 0 been pressed, turn green on");
break;
case 2:
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_RESET);
TRACE_DEBUG("Key 0 been pressed, turn blue on");
break;
default:
break;
}
}
/**
* @brief BUTTON KEY 1 按下后, 耗时操作
*/
void button_key_1_pressed() {
TRACE_DEBUG("Key 1 been pressed, do switch light");
}
/**
* @brief BUTTON KEY 2 按下后, 耗时操作
*/
void button_key_2_pressed() {
TRACE_DEBUG("Key 2 been pressed, do switch light");
}
main.c中轮训引脚变化
c
/* USER CODE BEGIN WHILE */
KeyPushEvent k_event = {.btn = BTN_KEY_0};
while (1)
{
if(key_event_queue_pop(&k_event)) {
switch (k_event.btn)
{
case BTN_KEY_0:
button_key_0_pressed();
break;
case BTN_KEY_1:
button_key_1_pressed();
break;
case BTN_KEY_2:
button_key_2_pressed();
break;
default:
break;
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
按键中断运行效果