4、GPIO驱动设计(下):中断触发方式、中断服务函数编写、去抖动处理
上一讲我们把GPIO的基本读写搞定了。但说真的,如果只会轮询,那驱动还停留在“小学生水平”。真正让外设“活”起来的,是中断。
我记得刚入行那会儿,做一个按键唤醒项目。产品经理说“按一下就能唤醒”,我天真地以为轮询就够了。结果功耗测试直接翻车——CPU根本没法睡。后来改成中断触发,功耗降了90%。从那以后,我对中断就特别上心。
这一讲,我们就来啃下GPIO中断这块硬骨头。包括触发方式怎么选、ISR怎么写才规范、还有那个让人头疼的按键抖动怎么处理。
4.1 中断触发方式:边沿 vs 电平
GPIO中断触发方式,说白了就两种:边沿触发和电平触发。但选错了,坑能让你调一整天。
| 触发方式 | 触发条件 | 典型场景 | 注意事项 |
|---|---|---|---|
| 上升沿触发 | 信号从低到高跳变 | 按键释放检测 | 防抖不到位会误触发 |
| 下降沿触发 | 信号从高到低跳变 | 按键按下检测 | 同上 |
| 双边沿触发 | 任意跳变 | 编码器脉冲计数 | ISR执行时间要极短 |
| 高电平触发 | 信号持续为高 | 外部报警信号 | 电平未恢复会反复触发 |
| 低电平触发 | 信号持续为低 | 紧急停机按钮 | 同上 |
我的经验之谈:按键类外设,优先用下降沿触发。电平触发容易在信号毛刺上反复进中断,CPU根本扛不住。我在一个工控项目里吃过这个亏——电平触发的中断,一秒钟进了上千次,系统直接假死。
4.2 中断服务函数编写:短、平、快
ISR(Interrupt Service Routine)的写法,有个铁律:越快越好。你想想看,CPU正在处理中断,其他中断都在排队。你在ISR里磨蹭100毫秒,系统实时性就崩了。
我个人的ISR编写规范是这样的:
- 只做最必要的事——清中断标志、读数据、设一个标志位。
- 绝对不调延时函数——delay_ms() 在ISR里就是毒药。
- 不调用printf——串口打印太慢了,而且可能重入。
- 复杂逻辑扔到任务或主循环——ISR只负责“通知”,不负责“处理”。
来看一个标准的按键中断ISR示例:
// 按键中断ISR - 标准写法
void EXTI0_IRQHandler(void)
{
// 1. 检查中断标志
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 2. 设一个volatile标志位
g_key_pressed = 1;
// 3. 记录触发时间(用于去抖)
g_last_tick = get_system_tick();
// 4. 清中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
小技巧:标志位一定要用 volatile 修饰。我之前在一个优化等级开-O2的项目里,忘了加volatile,编译器直接把标志位优化掉了——中断来了,主循环死活检测不到。排查了整整一个下午。
4.3 去抖动处理:硬件 vs 软件
按键抖动,是每个嵌入式工程师都会遇到的“老朋友”。机械按键按下和释放的瞬间,信号会弹跳几次,持续5-20毫秒。如果不处理,一次按键可能触发3-5次中断。
去抖动有两种思路:硬件去抖和软件去抖。
4.3.1 硬件去抖
硬件去抖,就是在按键引脚上加一个RC低通滤波器。电容充电放电,把毛刺平滑掉。
典型电路参数:
- 电阻R:10kΩ
- 电容C:0.1μF
- 时间常数τ = R × C = 1ms
- 实际去抖效果:约3-5个τ,即3-5ms
硬件去抖的好处是不占CPU,坏处是增加了BOM成本,而且参数固定后不好调。我一般只在量产产品上用硬件去抖,开发阶段先用软件方案。
4.3.2 软件去抖
软件去抖更灵活,也是我推荐的做法。核心思路就一句话:检测到边沿后,延时一段时间再确认电平状态。
常用的软件去抖算法有两种:
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 延时采样法 | 检测到变化后,延时10-20ms再读一次 | 实现简单 | 阻塞延时浪费CPU |
| 状态机去抖 | 用状态机记录按键状态,连续采样N次一致才确认 | 非阻塞,适合RTOS | 代码稍复杂 |
我个人偏爱状态机去抖。来看一个经典实现:
// 按键状态机去抖 - 非阻塞版本
typedef enum {
KEY_STATE_IDLE, // 空闲
KEY_STATE_DEBOUNCE, // 去抖中
KEY_STATE_PRESSED, // 已按下
KEY_STATE_RELEASE // 释放中
} key_state_t;
key_state_t g_key_state = KEY_STATE_IDLE;
uint32_t g_debounce_start = 0;
#define DEBOUNCE_MS 20 // 去抖时间20ms
void key_debounce_task(void)
{
uint8_t level = GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN);
uint32_t now = get_system_tick();
switch (g_key_state)
{
case KEY_STATE_IDLE:
if (level == 0) // 检测到按下
{
g_debounce_start = now;
g_key_state = KEY_STATE_DEBOUNCE;
}
break;
case KEY_STATE_DEBOUNCE:
if ((now - g_debounce_start) >= DEBOUNCE_MS)
{
if (level == 0) // 再次确认按下
{
g_key_state = KEY_STATE_PRESSED;
g_key_event = KEY_EVENT_PRESS;
}
else // 抖动,回到空闲
{
g_key_state = KEY_STATE_IDLE;
}
}
break;
case KEY_STATE_PRESSED:
if (level == 1) // 检测到释放
{
g_debounce_start = now;
g_key_state = KEY_STATE_RELEASE;
}
break;
case KEY_STATE_RELEASE:
if ((now - g_debounce_start) >= DEBOUNCE_MS)
{
if (level == 1) // 确认释放
{
g_key_state = KEY_STATE_IDLE;
g_key_event = KEY_EVENT_RELEASE;
}
else
{
g_key_state = KEY_STATE_PRESSED;
}
}
break;
}
}
曾经踩过的坑:我在一个低功耗项目里,用了延时采样法去抖。按键按下后delay(20ms),CPU白白烧了20毫秒的电。后来换成状态机去抖,配合MCU的WFI指令,功耗从2mA降到了50μA。记住:中断里绝对不要delay,去抖逻辑放到任务或主循环里做。
4.4 完整的中断驱动按键框架
把上面这些知识点串起来,一个完整的中断驱动按键框架就出来了。我画了张图,帮你理清数据流:
这个框架的核心思想就一个:分层解耦。ISR只负责“通知”,软件去抖负责“确认”,应用任务负责“处理”。每一层各司其职,出了问题也好定位。
总结一下:
- 中断触发方式:按键用下降沿,电平触发慎用
- ISR编写:短平快,只设标志不清除复杂逻辑
- 去抖处理:推荐状态机方案,非阻塞、低功耗
- 整体框架:中断通知 → 软件去抖 → 事件队列 → 应用处理