4、GPIO驱动设计(下):中断触发方式、中断服务函数编写、去抖动处理

上一讲我们把GPIO的基本读写搞定了。但说真的,如果只会轮询,那驱动还停留在“小学生水平”。真正让外设“活”起来的,是中断。

我记得刚入行那会儿,做一个按键唤醒项目。产品经理说“按一下就能唤醒”,我天真地以为轮询就够了。结果功耗测试直接翻车——CPU根本没法睡。后来改成中断触发,功耗降了90%。从那以后,我对中断就特别上心。

这一讲,我们就来啃下GPIO中断这块硬骨头。包括触发方式怎么选、ISR怎么写才规范、还有那个让人头疼的按键抖动怎么处理。

4.1 中断触发方式:边沿 vs 电平

GPIO中断触发方式,说白了就两种:边沿触发和电平触发。但选错了,坑能让你调一整天。

触发方式 触发条件 典型场景 注意事项
上升沿触发 信号从低到高跳变 按键释放检测 防抖不到位会误触发
下降沿触发 信号从高到低跳变 按键按下检测 同上
双边沿触发 任意跳变 编码器脉冲计数 ISR执行时间要极短
高电平触发 信号持续为高 外部报警信号 电平未恢复会反复触发
低电平触发 信号持续为低 紧急停机按钮 同上

我的经验之谈:按键类外设,优先用下降沿触发。电平触发容易在信号毛刺上反复进中断,CPU根本扛不住。我在一个工控项目里吃过这个亏——电平触发的中断,一秒钟进了上千次,系统直接假死。

4.2 中断服务函数编写:短、平、快

ISR(Interrupt Service Routine)的写法,有个铁律:越快越好。你想想看,CPU正在处理中断,其他中断都在排队。你在ISR里磨蹭100毫秒,系统实时性就崩了。

我个人的ISR编写规范是这样的:

  1. 只做最必要的事——清中断标志、读数据、设一个标志位。
  2. 绝对不调延时函数——delay_ms() 在ISR里就是毒药。
  3. 不调用printf——串口打印太慢了,而且可能重入。
  4. 复杂逻辑扔到任务或主循环——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 完整的中断驱动按键框架

把上面这些知识点串起来,一个完整的中断驱动按键框架就出来了。我画了张图,帮你理清数据流:

中断驱动按键框架数据流 物理按键 硬件去抖(RC) GPIO外设 中断控制器(NVIC) ISR 软件去抖状态机 按键事件队列 应用任务 说明: 硬件去抖和软件去抖可二选一,也可同时使用。ISR只设标志位,去抖和事件处理都在任务中完成。

这个框架的核心思想就一个:分层解耦。ISR只负责“通知”,软件去抖负责“确认”,应用任务负责“处理”。每一层各司其职,出了问题也好定位。

总结一下:

  • 中断触发方式:按键用下降沿,电平触发慎用
  • ISR编写:短平快,只设标志不清除复杂逻辑
  • 去抖处理:推荐状态机方案,非阻塞、低功耗
  • 整体框架:中断通知 → 软件去抖 → 事件队列 → 应用处理