4、GPIO中断与轮询读取:中断服务函数编写、轮询模式对比、去抖动处理
好,咱们今天聊点实在的。GPIO怎么读?说白了就两种方式:轮询和中断。很多新手上来就写轮询,觉得简单。但实际项目里,这两种方式的选择,直接决定了你的系统能不能扛得住。
我个人习惯是:能用中断就别轮询。但这话也不绝对,咱们得看场景。
4.1 轮询模式:简单但“傻等”
轮询,就是CPU一直在那盯着引脚看。像这样:
while(1) {
if(GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 1) {
// 检测到高电平
do_something();
}
}
你想想看,CPU大部分时间都在空转。我在一个智能家居项目里就吃过这个亏——用轮询读门磁传感器,结果主循环里其他任务全被拖慢了。说白了,轮询适合那种“你本来就要一直等”的场景,比如按键扫描。
轮询的优缺点:
- 优点:代码简单,逻辑直观,没有中断嵌套的烦恼
- 缺点:CPU利用率低,响应延迟不可控(取决于轮询周期)
4.2 中断模式:让CPU“随叫随到”
中断就不一样了。CPU平时该干嘛干嘛,引脚一有变化,立马跳过来处理。嗯,这里要注意:中断服务函数(ISR)要短小精悍,千万别在里面做延时或者复杂运算。
我曾经在一个项目中,ISR里调了个printf,结果整个系统卡死。为什么?因为printf本身可能触发中断,造成递归调用。血的教训啊。
标准的中断服务函数写法:
void EXTI0_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
// 清除中断标志位(必须做!)
EXTI_ClearITPendingBit(EXTI_Line0);
// 设置一个标志位,主循环去处理
g_button_pressed = 1;
}
}
我的习惯:ISR里只做两件事——清标志位、设全局标志。真正的业务逻辑放到主循环里。这样既保证了响应速度,又避免了ISR过长导致的问题。
4.3 中断 vs 轮询:怎么选?
我整理了一个对比表,你一看就明白:
| 对比项 | 轮询 | 中断 |
|---|---|---|
| 响应延迟 | 取决于轮询周期,通常ms级 | us级,几乎实时 |
| CPU占用 | 高(一直在查) | 低(有事件才处理) |
| 代码复杂度 | 低 | 中(需处理中断优先级、嵌套) |
| 适用场景 | 按键扫描、低速传感器 | 边沿触发、紧急事件、高速信号 |
我个人建议:如果信号变化频率低于100Hz,轮询也能凑合。但一旦超过这个数,或者你需要精确捕捉边沿,果断上中断。
4.4 去抖动处理:别让毛刺坑了你
机械按键、碰撞传感器这类东西,按下瞬间会弹跳。你想想看,一个按键按下去,实际上在几毫秒内会反复通断好几次。如果不处理,一次按键可能触发十几次中断。
我遇到过最夸张的一次——一个门磁传感器,因为没做去抖,一晚上触发了2000多次报警。业主差点报警说家里闹鬼。
去抖的常见做法有两种:
4.4.1 硬件去抖(RC滤波)
在GPIO引脚和地之间加一个电容(通常0.1uF),再串一个电阻(10kΩ)。简单粗暴,但电容值要算好,不然会把有效信号也滤掉。
4.4.2 软件去抖(延时采样)
检测到边沿后,等10-20ms再读一次。如果状态一致,才认为是有效信号。代码示例:
void EXTI0_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line0);
// 软件去抖:延时10ms后再次确认
delay_ms(10);
if(GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 1) {
g_button_pressed = 1;
}
}
}
注意:在ISR里直接调用delay_ms()其实不推荐,因为会阻塞其他中断。更好的做法是:在ISR里启动一个定时器,定时器到期后再去读引脚。这样既去抖又不阻塞。
4.5 知识体系总览
下面这张图,把咱们今天讲的核心逻辑串起来了:
你看,从读取方式的选择,到中断服务函数的编写,再到去抖处理,这是一条完整链路。任何一个环节出问题,数据都可能不准。
避坑指南:我曾经在一个工业项目里,因为没做去抖,传感器每触发一次就连续上报3-5次数据。后来加了10ms的软件去抖,问题立刻解决。记住:机械触点必有抖动,别偷懒。
好了,这一节的内容就到这。中断和轮询没有绝对的好坏,关键看你的系统对实时性和CPU利用率的要求。去抖处理更是不能省——你总不想被“幽灵触发”折磨吧?
公众号:蓝海资料掘金营,微信deep3321