前言
在上一篇中,我们学会了用串口观察程序的运行状态。但你是否想过:如果程序一直卡在HAL_Delay()里等待,那它怎么同时去检测按键是否被按下?
答案就是中断(Interrupt)。中断是嵌入式开发的灵魂,它让单片机能够“一心多用”——平时做自己的事,一旦有紧急事件(比如按键按下、数据到达),立刻暂停当前工作去处理,处理完再回来继续。
今天,我们就来学习 STM32 最基础也最常用的外部中断(EXTI),并通过一个按键控制 LED的实例,彻底搞懂中断配置和按键消抖。
一、什么是外部中断 EXTI?
EXTI(External Interrupt/Event Controller)是 STM32 的外部中断/事件控制器。简单来说,它能让 GPIO 引脚上的电平变化(上升沿、下降沿或双边沿)触发一个中断信号,从而执行我们预设的回调函数。
常见应用场景:
- 按键检测(按下瞬间触发中断)
- 传感器信号触发(如人体红外感应)
- 通信模块的状态通知(如 Wi-Fi 模块收到数据)
二、CubeMX 配置外部中断
假设我们要用PA0连接一个按键,按下时为低电平(下降沿触发),用PC13控制 LED。
1. 配置按键引脚为外部中断模式
- 在 CubeMX 的芯片引脚图中,找到 PA0,左键点击选择 GPIO_EXTI0。
- 注意:不是 GPIO_Input,而是 GPIO_EXTI0,这表示将该引脚映射到外部中断线 0。
2. 配置 GPIO 参数
在左侧 System Core -> GPIO 中,选中 PA0:
- GPIO mode: External Interrupt Mode with Falling edge trigger detection(下降沿触发)
- GPIO Pull-up/Pull-down: Pull-up(上拉,按键未按下时保持高电平)
3. 开启 NVIC 中断
- 进入 NVIC 选项卡,勾选 EXTI line0 interrupt 的 Enabled。
- 可以设置中断优先级(Preemption Priority),数字越小优先级越高。对于简单的按键,默认优先级即可。
4. 生成代码
点击 GENERATE CODE,CubeMX 会自动生成中断初始化代码,包括 NVIC 配置和 EXTI 线路使能。
三、编写中断回调函数
HAL 库为外部中断提供了一个标准的回调函数。当 PA0 检测到下降沿时,HAL 库会自动调用它:
/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 按键被按下了!
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转 LED
}
}
/* USER CODE END 4 */
注意:这个函数需要写在 main.c 的 USER CODE BEGIN 4 区域里。
编译下载后,每按一次按键,LED 就会翻转一次。是不是很简单?
……但先别高兴太早。如果你实际测试,可能会发现:按一次按键,LED 却翻转了好几次!这就是机械按键的抖动问题。
四、按键消抖:理论与实践
什么是按键抖动?
机械按键在按下或松开的瞬间,内部的金属触点并不会立刻稳定接触/断开,而是会快速通断多次,产生一连串的电平跳变。对于单片机来说,这看起来就像是按键被按下了很多次。
消抖的本质:在检测到电平变化后,等待一段时间(通常是 10~20ms),再次确认电平状态,如果仍然保持变化后的状态,才认为按键确实被按下了。
消抖方案一:延时消抖(简单但不推荐)
在中断回调函数里直接用 HAL_Delay() 延时消抖:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
HAL_Delay(20); // 延时 20ms 消抖
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
}
为什么不推荐?
HAL_Delay()依赖 SysTick 中断,而在中断服务函数里调用延时函数,可能会阻塞其他中断,甚至导致程序卡死。- 中断函数应该越快越好,不要做耗时操作。
消抖方案二:标志位 + 主循环检测(推荐)
更合理的做法是在中断里只设置一个标志位,具体的消抖和逻辑处理放在主循环里完成:
/* 定义全局变量 */
uint8_t key_flag = 0; // 按键中断标志
uint32_t key_press_time = 0; // 按键触发时间戳
/* 中断回调函数 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
key_flag = 1;
key_press_time = HAL_GetTick(); // 记录当前系统时间
}
}
/* main 函数中的 while(1) 循环 */
while (1)
{
if (key_flag == 1)
{
// 等待 20ms 后再次检测按键状态
if (HAL_GetTick() - key_press_time >= 20)
{
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 确认按下,翻转 LED
}
key_flag = 0; // 清除标志位
}
}
/* 这里可以继续执行其他任务 */
}
优点:
- 中断函数极短,不阻塞系统
- 消抖逻辑在主循环中完成,不影响其他中断
- 程序结构清晰,易于扩展
消抖方案三:定时器消抖(工业级方案)
对于更复杂的项目,可以用定时器每隔 10ms 扫描一次按键状态。如果连续两次扫描都检测到按键按下,才认为是有效按键。这是最稳定、最常用的消抖方案,我们后续会专门出一篇定时器的文章来详细讲解。
五、中断使用中的常见坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 按一次按键触发多次中断 | 按键抖动 | 加入消抖逻辑 |
| 中断里调用 HAL_Delay 卡死 | SysTick 优先级低于当前中断 | 中断里不要调用延时函数 |
| 中断不触发 | NVIC 未使能或引脚配置错误 | 检查 CubeMX 的 NVIC 和 GPIO 模式 |
| LED 状态与预期相反 | 触发沿设置错误或上下拉配置不对 | 确认按键电路是高电平还是低电平触发 |
六、总结
通过今天的学习,你应该已经掌握了:
- 如何在 CubeMX 中配置外部中断(EXTI)
- 如何编写 HAL 库的中断回调函数
- 机械按键抖动的原因及消抖方法
- 中断编程中的注意事项和避坑指南
中断让单片机从“单线程”变成了“多任务”,是嵌入式开发中不可或缺的核心技能。掌握它,你就迈出了从“点灯小白”到“嵌入式工程师”的关键一步。
下一篇预告:STM32 HAL库极简入门:定时器与PWM输出。



