一、按键消抖与中断:从问题到 HAL 库实现(新手必懂的按键处理逻辑)
(一)、问题引入:为什么会 “丢失按键”?
(1)死循环检测的缺陷
用 while(1)
死循环读取按键引脚电平,就像 “盲人摸象”—— 如果按键动作(电平变化)发生在两次检测之间,程序就会 “看不见”。
- 按键按下→松开→按下→松开,产生 4 次电平变化(①→②→③→④)
- 但程序如果在④位置才检测,就会丢失中间 2 次按键动作!
(二)生活类比理解
想象你在看书(主程序),每隔 10 分钟才抬头看一眼门铃(死循环检测):
- 如果门铃在 “非抬头时间” 响(按键动作),你就会错过(丢失按键)!
- 解决办法:门铃响时主动打断你(中断机制),不管你在看哪页书,都会立即处理。
二、中断机制:解决 “丢失按键” 的钥匙
(一)中断处理流程
中断就像 “紧急门铃”,能主动打断主程序,流程分 7 步:
- 主程序正常执行(①)
- 按键按下→触发中断(②)
- 暂停主程序(③)→保存当前状态(比如 “看到书的第几页”)(④)
- 跳转到中断任务(⑤)→执行按键处理逻辑(⑥)
- 恢复主程序状态(⑧)→继续执行(⑨)
(二)中断的硬件架构
CPU 运行时,会被各种 “异常” 打断,按键是其中一种中断源:
- 按键、定时器、ADC 等事件→通过 “中断控制器” 汇总→通知 CPU
- 中断控制器像 “前台”,帮 CPU 筛选最紧急的事件(比如同时按键和定时器中断,优先处理高优先级的)
三、ARM 芯片中断处理全流程
(一)准备阶段:给中断 “搭好台子”
想象 ARM 芯片是个 “多功能工厂”,中断就是工厂里的 “紧急任务通道”。要让这个通道能用,得先做 3 件事:
- 指定谁能触发中断(设置中断源):
比如告诉芯片 “当按键被按下时,要走紧急通道”,让按键这个 “触发事件” 能唤醒中断机制。 - 设置任务优先级(配置中断控制器):
工厂里可能同时有多个紧急任务(按键、定时器、串口数据),得规定谁先处理。比如 “按键中断比普通定时器中断更紧急”,避免重要任务被耽误。 - 打开紧急通道总开关(使能 CPU 中断):
就算设置了触发条件和优先级,要是总开关没开,紧急任务来了也没人理,所以得给 CPU 说 “允许处理中断”。
完成这三步,芯片就 “知道要监听哪些紧急事件、怎么排序、要不要响应” 了,接下来就能正常跑主程序(比如工厂里的常规生产流程)。
(二)触发阶段:中断是怎么 “跳出来” 的
当你按下按键(触发中断源),就像给工厂打了个 “紧急电话”:
- 按键把信号传给中断控制器(工厂的 “调度室”),调度室确认 “这是个要处理的中断”。
- CPU 每干完一件小事(执行完一条指令),都会 “瞄一眼” 调度室(检查有没有中断)。
- 一旦发现有紧急任务(中断触发),CPU 就会暂停手头的活(比如主程序里的循环),准备处理中断。
(三)处理阶段:中断函数是怎么执行的
CPU 暂停主程序后,会做三件事(像 “紧急任务处理流程”):
- 记下来当前干到哪了(保存现场):
把主程序执行到一半的状态(比如变量值、执行到哪行代码)存起来,就像看书看到一半插个书签。 - 找到对应的处理方法(跳转到中断函数):
芯片里有个 “紧急任务手册”(异常向量表),写着 “按键中断该调用哪个函数处理”。CPU 会根据中断类型,找到对应的函数(比如 “按键按下后翻转 LED” 的逻辑)。 - 处理完回到书签继续干(恢复现场):
中断函数执行完(比如 LED 翻转完成),CPU 会把之前存的主程序状态读回来,继续从暂停的地方接着执行(就像从书签位置继续看书)。
(四)总结:中断处理像 “临时插播”
整个流程就像:
你正在看电影(主程序)→ 门铃突然响了(中断触发)→ 你暂停电影、记住看到哪(保存现场)→ 去开门(执行中断函数)→ 回来接着从暂停处看电影(恢复现场)。
ARM 芯片的中断处理,本质就是这套 “暂停 - 处理 - 恢复” 的逻辑,不管多复杂的中断,都是这个思路~
四、中断触发全流程拆解:从按键按下到执行函数
(一)源头:按键如何触发中断信号?
把按键想象成 “门铃”,要让门铃能触发中断,得完成 3 步 “接线 + 配置”:
1. 第一步:选 “门铃线”(配置中断源映射)
- STM32 有很多 GPIO 引脚(PA0、PB0…PG0),但 EXTI 中断线只有 16 条(EXTI0~EXTI15)。
- 想让
PA0
按键触发中断,得把PA0
“连” 到EXTI0
线上 —— 这就需要配置AFIO_EXTICR
寄存。
看寄存器图(AFIO_EXTICR1
):
EXTI0[3:0]
这 4 位,决定EXTI0
线连到哪个 GPIO。- 如果设置为
0000
,就表示EXTI0
连到PA0
;设0001
则连到PB0
,以此类推。
白话解释:就像给门铃选 “入户线”,你得告诉系统 “PA0 这个按键,走 EXTI0 这条中断线”。
2. 第二步:设置 “门铃响的方式”(配置触发模式)
- 按键按下时,电平会变化:可能是 “从高变低”(下降沿),也可能是 “从低变高”(上升沿)。
- 你得告诉 EXTI 控制器:“当 PA0 电平出现下降沿时,触发中断”(或上升沿,看需求)。
对应手绘原理图里的 “触发方式”:在代码里配置 GPIO_MODE_IT_FALLING
(下降沿触发)或 GPIO_MODE_IT_RISING
(上升沿触发)。
3. 第三步:打开 “门铃接收器”(使能 EXTI 中断)
- 即使接了线、设了触发方式,EXTI 控制器也得 “允许接收” 这个中断。
- 这一步需要 使能 EXTI 对应的中断线(比如
EXTI0
),让它能把信号传给 NVIC(中断控制器)。
(二)中转站:NVIC 如何 “调度” 中断?
NVIC 是 “中断调度中心”,它要做 2 件事:
1. 第一步:设置 “优先级”(谁先处理)
- 系统可能同时触发多个中断(比如按键中断 + 定时器中断),得规定谁更紧急。
- 通过 NVIC 配置 “抢占优先级” 和 “子优先级”:数值越小,优先级越高。
优先级裁决:确定中断响应顺序(谁先处理)
系统可能同时触发多个中断(如按键中断 + 定时器中断 ),NVIC 靠 “抢占优先级 + 子优先级” 区分紧急程度,规则如下:
- 抢占优先级:决定 “中断能否嵌套” 。数值越小,优先级越高,高抢占优先级中断可打断低抢占优先级的中断执行(类似 “紧急任务插队” )。比如 EXTI0 抢占优先级为 0 ,EXTI1 为 1 ,若 EXTI0 正在执行,EXTI1 触发不会打断它;但如果 EXTI1 抢占优先级是 0 、EXTI0 是 1 ,EXTI1 就能抢占执行。
- 子优先级:当多个中断 “抢占优先级相同” 时,子优先级高(数值小)的先被响应;若抢占、子优先级都相同,编号小的中断优先。比如 EXTI0 和 EXTI1 抢占优先级相同,子优先级高的 EXTI0 会先处理;若子优先级也相同,EXTI0(编号小)优先。
【新增关键补充】优先级寄存器的 8 位 “隐藏规则”
很多初学者会疑惑:NVIC 里每个中断的优先级用 8 位寄存器存,这 8 位都咋用?这里藏着两个关键细节
- ① 8 位不一定全实现:像 STM32F103 系列芯片,实际只实现了 4 位 。芯片厂商会根据硬件复杂度、功耗等需求,决定真正启用几位优先级位,具体看芯片手册。
- ② 分组优先级和子优先级的划分可配置:NVIC 内部有 “优先级分组配置寄存器” ,它决定 8 位里 “哪些位算分组优先级(抢占用)、哪些位子优先级(同抢占时排序用)” 。比如:
优先级分组 抢占优先级域 子优先级域 0(默认) Bit[7:1] Bit[0] 1 Bit[7:2] Bit[1:0] ... ... ... 不同分组下,抢占和子优先级的位数拆分不同,配置时用 HAL_NVIC_SetPriorityGrouping()
函数设置(比如HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_3);
),选好后所有中断的优先级拆分规则就跟着变啦。
代码配置示例(以 EXTI0 为例 ):
// EXTI0_IRQn:中断通道;第一个 0:抢占优先级;第二个 0:子优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
对应硬件原理:每个中断在 NVIC 里对应 8 位优先级寄存器(如 NVIC->IP[0]
对应外部中断 #0 ),寄存器被拆分为 “抢占优先级 + 子优先级” 两部分,通过代码配置这 8 位寄存器的值,实现优先级设定。
2. 总开关使能:允许中断到达 CPU
即使外设(如 EXTI )触发了中断信号,NVIC 还需 “允许转发” 给 CPU ,才能真正响应中断。
代码实现(以 EXTI0 为例 ):
// 使能 EXTI0 对应的 NVIC 中断通道,让中断信号能传递到 CPU
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
这一步是 “中断生效的最后开关” ,配置了优先级但未使能通道,中断依然无法被 CPU 响应。
简单说,NVIC 调度中断就像 “先给中断排好紧急程度(优先级裁决,含 8 位寄存器的分组秘密),再打开通道让紧急信号能传到 CPU 处理(使能开关)” ,确保系统按规则响应最紧急的中断 。
(三)终点:CPU 如何执行中断函数?(结合向量表)
当 NVIC 把中断信号传给 CPU 后:
1. 第一步:查 “导航表”(异常向量表)
- CPU 每执行完一条指令,都会 “检查有没有中断”。一旦发现有中断,就去查 异常向量表。
- 向量表里存着 “每种中断对应的处理函数地址”,比如
EXTI0
中断对应EXTI0_IRQHandler
函数。
2. 第二步:执行 “中断逻辑”(回调函数)
EXTI0_IRQHandler
是硬件规定的入口函数,它会调用 HAL 库的HAL_GPIO_EXTI_IRQHandler
。- 最终会执行我们写的 回调函数
HAL_GPIO_EXTI_Callback
,在这里写按键逻辑(比如翻转 LED)。
(四)总结:中断触发的完整流程
把整个过程想象成 “寄快递”:
- 按键按下 = 你把快递(中断信号)放到门口。
- EXTI 配置 = 选快递通道(EXTI0 线)、写快递要求(触发方式)、激活通道(使能 EXTI)。
- NVIC 调度 = 快递站(NVIC)看快递优先级(谁先送)、决定是否转发给 CPU(使能通道)。
- CPU 执行 = 快递员(CPU)查地址表(向量表)、找到收件地址(处理函数)、送快递(执行逻辑)。
五、STM32 GPIO 中断按键控制超详细教学(零基础友好版)
(一) 为什么用中断控制按键?
之前学按键,是不是一直用while(1)
死循环检测?这样 CPU 啥都干不了,只能盯着按键,太浪费啦!中断方式 就像给按键装了 “报警器”,按下时主动喊 CPU 来处理,平时 CPU 该干嘛干嘛,效率直接拉满~今天带大家用 STM32 实现「按键中断控制 LED」,超详细步骤,跟着做一定能成!
(二)硬件原理 —— 按键怎么和 STM32 互动?
先看硬件电路:
- 按键没按下:PB14 通过电阻(或内部上拉)保持高电平
- 按键按下:PB14 直接连 GND,变成低电平
STM32 就是通过检测 PB14 的电平变化(高→低 / 低→高)触发中断,实现按键功能!
(三)软件流程 —— 从配置到代码的完整步骤
回忆老师讲的中断流程,咱们一步步来:
步骤 1:用 CubeMX 配置 GPIO(图形化操作超简单)
打开 STM32CubeMX,找到你的开发板(比如 STM32F103C8T6 ),做这 2 件事:
(1)配置按键引脚(PB14)
- 找到
PB14
引脚,右键选择External Interrupt Mode with Rising/Falling Edge
(上升沿 + 下降沿触发,按键按下、松开都能触发中断) - 上下拉电阻:如果硬件没外接电阻,选
No pull-up and no pull-down
也能用(实际建议选内部上拉更稳定,看需求调整)
(2)配置 LED 引脚(比如 PC13)
- 找到
PC13
(开发板常用小灯引脚),配置为Output Push Pull
(推挽输出模式,控制灯亮灭)
步骤 2:配置 NVIC—— 让中断被 “听到”
NVIC 是 STM32 的 “中断管家”,得告诉它:“这个按键中断要优先处理!”
- 点击左侧
NVIC
,找到EXTI line[15:10] interrupts
(因为 PB14 对应 EXTI14,属于这一组) - 勾选
Enabled
启用中断 - 设置优先级:
Preemption Priority
(抢占优先级) 和Sub Priority
(子优先级) 填0
就行(数值越小优先级越高,简单场景不用纠结)
步骤 3:生成代码 ——CubeMX 帮你写框架
点击 GENERATE CODE
,CubeMX 会生成初始化代码,重点看这 2 个文件:
stm32f1xx_hal_msp.c
:里面有 GPIO 和 NVIC 的初始化代码(不用管,CubeMX 自动配好的)main.c
:主函数逻辑,我们要在这里写中断回调
步骤 4:写中断回调函数 —— 按键逻辑在这里
STM32 HAL 库有个 “回调函数” 机制,中断触发后会自动调用 HAL_GPIO_EXTI_IRQHandler
,它会帮我们清中断标志,然后调用 HAL_GPIO_EXTI_Callback
—— 我们只需要重写这个回调函数!
在 main.c
里添加代码(或者自己建个文件,记得包含头文件):
// 中断回调函数,按键触发时自动执行
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// 判断是不是 PB14 触发的中断
if (GPIO_Pin == GPIO_PIN_14)
{
// 读取 PB14 当前电平:按下是低电平,松开是高电平
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_RESET)
{
// 按键按下:控制 PC13 灯亮(假设小灯接 PC13)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
}
else
{
// 按键松开:控制 PC13 灯灭
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
}
}
}
(四)下载验证 —— 按下按键,看灯反应!
把代码编译、下载到开发板,试试:
- 按下按键 K1(接 PB14 )→ PC13 灯亮
- 松开按键 → PC13 灯灭
如果没反应,检查这 3 点:
- 硬件连接:按键真的接 PB14、灯接 PC13 吗?
- CubeMX 配置:中断模式、NVIC 使能有没有漏?
- 代码逻辑:
HAL_GPIO_EXTI_Callback
里的引脚判断对不对?
(五)完整流程(核心逻辑总结):
(按键按下 → PB14 电平变化 → EXTI 触发中断 → NVIC 转发 → CPU 执行回调函数 → 控制 PC13 灯亮灭
)
(六)一句话总结 —— 学会中断按键有多爽?
用中断控制按键,CPU 不用 “死等” 按键,效率翻倍!核心就 4 步:
CubeMX 配 GPIO → 配 NVIC 优先级 → 写回调函数 → 下载验证
下次做项目,不管是按键控制灯、还是触发复杂逻辑,都能用这套方法~快试试吧