按键中断:按下触发回调
源码路径:
example/HC32F460JEUA/4.key/source/main.cBSP 实现:bsp/ev_hc32f460_lqfp100_v2/ev_hc32f460_lqfp100_v2.c
1. 功能简介
这个例子让单片机能感知按键被按下,而且用的是最专业的方式——外部中断。
「中断」这个词听着高深,其实特别好理解。打个比方:
- 轮询就像你在等快递,每隔十秒就跑到门口看一眼「来了吗?来了吗?」——什么正事都干不了。
- 中断就像你留了个门铃,你安心在屋里干活,快递到了会按铃通知你,你放下手里的活去开门,开完门回来继续干。
按键中断就是给引脚装了个「门铃」:平时 CPU 该干啥干啥,一旦你按下按键(引脚电平从高变低),硬件立刻打断 CPU,跳到你写好的回调函数里执行,处理完再回到原来的地方继续。
这个例子的完整链路是:按下 PC13 → 触发 EXINT13 下降沿中断 → 执行 Key_Callback → 置标志位 u8GlobalKey = 1 → 主循环发现标志位,做出反应。
2. 硬件准备
用 HC32F460JEUA KIT 开发板,板载按键 K1 接在 PC13,无需接线。
|
|
如果要自己外接按键(面包板):
|
|
只要两根线:引脚 → 按键 → GND。上拉靠芯片内部的,省一个电阻。
常见错误:把按键接到 3.3V(按下拉高)。那样「没按」时引脚是悬空的,读到的电平完全随机,程序会乱触发。按键一定接 GND,配合上拉使用。
3. 完整代码
3.1 main.c
|
|
main.c 看着很短,因为真正的活都在 BSP_KEY_Init() 里干了。想学明白,必须看它的实现。
3.2 BSP 里的按键中断实现(核心)
|
|
4. 代码分段讲解
4.1 EXINT 通道号 = 引脚号
|
|
记住这个绑定关系:ExtiChNN 只能用于任意端口的 PinNN。按键在 PC13,所以必须用 ExtiCh13,用别的通道收不到信号。
副作用是:PA13、PB13、PC13 这些「同号引脚」只能有一个用作外部中断,它们共享通道 13。设计电路时要注意别撞车。
4.2 硬件滤波——一行代码搞定消抖
|
|
这两行是 HC32 的贴心之处。开启后,EXINT 模块内部的数字滤波器会把宽度太窄的毛刺直接过滤掉,只有稳定持续的电平变化才算真触发。
按键触点弹跳产生的那些几十微秒的尖峰,就在这里被吃掉了。相比在软件里写 delay(10) 消抖,硬件滤波不占 CPU 时间,更优雅。
Pclk3Div8 是滤波采样时钟,分频越大滤波时间越长、能滤掉更宽的毛刺,但响应也越慢。默认值一般够用。
4.3 enExInt = Enable 是命门
|
|
初学者最容易漏的就是 enExInt。它相当于给引脚和 EXINT 模块之间接上一根线。EXINT 那边配得再完美,这根线没接,中断永远不会来。
enPullUp = Enable 打开内部上拉,保证没按键时引脚是稳定的高电平。这样才有「按下 → 高变低 → 下降沿」这个过程。
注意这里没有设置 enPinMode,因为清零后默认就是输入模式(Pin_Mode_In 对应 0),符合按键需求。
4.4 回调里的「等待释放」写法
|
|
这个 while 的意思是:只要引脚还是低电平(按键还按着),就一直循环。松手了才跳出来。
好处是天然把整个抖动过程包住了,只会产生一次「按下」事件。缺点是它是阻塞的——你按着不放,CPU 就一直卡在中断里出不来。
这是可以改进的地方。工程实践中,中断回调应当尽量短,标准做法是:
1 2 3 4 5 6 7 8void Key_Callback(void) { if (Set == EXINT_IrqFlgGet(ExtiCh13)) { u8GlobalKey = 1; // 只置个标志,立刻返回 EXINT_IrqFlgClr(ExtiCh13); } }然后在主循环里检查标志、做消抖确认、执行具体动作。这样中断响应快、不阻塞。
4.5 中断和主循环的协作:标志位模式
这是嵌入式最经典的设计模式:
|
|
原则:中断里只做「记录事件」这一件事,具体处理留给主循环。
重要提示:这种被中断修改、被主循环读取的变量,应当声明为
volatile:
1volatile uint8_t u8GlobalKey;否则编译器可能认为「主循环里没人改这个变量」而把它优化成常量,导致
if永远不成立。SDK 的 BSP 里没加,属于可以改进的点,实际项目里建议加上。
4.6 EXINT_IrqFlgClr 不能忘
|
|
外部中断的头号 bug 就是忘记清标志。硬件中断标志置位后不清,CPU 一退出中断就发现「标志还在,还有中断要处理」,于是立刻又跳进来——程序死循环在中断里,主循环再也执行不到,看起来就像「按一下按键程序就卡死了」。
5. 实验现象
原版例子里主循环的处理代码是注释掉的,所以直接烧录看不到明显现象(只有 del++ 在空转)。
想看到效果,把主循环改成这样:
|
|
改完后的现象:每按一次 K1(PC13),红灯亮灭切换一次。按一下亮,再按一下灭。
如果加上串口打印,还能在串口助手里看到每次按键的记录。
6. 常见问题
Q1:按键完全没反应,中断进不去? 按这个顺序查:
stcPortInit.enExInt = Enable写了吗?(头号原因)- EXINT 通道号和引脚号匹配吗?(PC13 必须用
ExtiCh13) enIntSrc填的是INT_PORT_EIRQ13吗?NVIC_EnableIRQ()调了吗?- 按键真的接在 PC13、真的接到 GND 吗?
Q2:按一下按键程序就卡死了?
- 十有八九是忘了
EXINT_IrqFlgClr(),程序困在中断里反复进出。 - 也可能是回调里的
while等待释放,你按着没松手。
Q3:按一下,程序却响应了好几次?
- 消抖没生效。检查
enFilterEn = Enable是否配上。 - 也可以在主循环处理时再加一层软件确认(延时 10ms 后再读一次电平)。
Q4:主循环里的 if (u8GlobalKey == 1) 永远不成立?
- 变量没加
volatile,被编译器优化了。改成volatile uint8_t u8GlobalKey;。 - 或者忘了在处理完后
u8GlobalKey = 0,导致只响应第一次(其实是一直成立,看着像没反应)。
Q5:按住不放,LED 疯狂闪?
- 说明触发方式配成了
ExIntLowLevel(低电平触发),按着就一直触发。改成ExIntFallingEdge。
Q6:中断里能不能用 printf?
- 能用,但不推荐。
printf通过串口一个字符一个字符地发,非常慢(几百微秒到几毫秒),会严重拖累中断响应。调试时临时用可以,正式代码里请置标志位、到主循环去打印。
7. 小结
- 按键中断链路:按下 → 下降沿 → EXINT 触发 → 回调 → 置标志 → 主循环处理。
ExtiChNN通道号必须和PinNN引脚号对应。- 引脚配置里
enExInt = Enable是命门,漏了必不通。 - 硬件滤波一行搞定消抖,比软件延时优雅。
- 回调里必须
EXINT_IrqFlgClr(),否则卡死在中断。 - 中断里只置标志,重活交给主循环;共享变量加
volatile。
8. 拓展应用
按键 + 中断是人机交互的基础,能玩出很多东西:
- 多按键菜单系统:三四个按键(上/下/确认/返回)配合 LCD,就是一套完整的菜单界面,工业设备的操作面板都是这么做的。
- 长按 / 短按 / 双击识别:用
ExIntBothEdge双边沿触发,配合定时器测量按下的时长——短按 < 500ms 是一个功能,长按 > 2s 是另一个功能(比如长按关机)。 - 组合键:同时按住两个键触发特殊操作(如恢复出厂设置)。
- 旋转编码器:两个引脚的相位差信号,用外部中断捕捉,就能做出音量旋钮那样的调节器。
- 外部传感器触发:把按键换成红外对管、霍尔开关、震动传感器——任何输出开关信号的传感器都能用同样的外部中断方式接入,做门磁报警、转速计数、计步器。
- 低功耗唤醒:外部中断能把睡眠中的 MCU 唤醒。做电池供电的设备时,平时深度睡眠,按一下按键才醒来工作,能把待机功耗降到微安级。
- 紧急停止:安全相关的输入必须用中断而不是轮询——因为中断响应是微秒级的,绝不会漏掉信号。