3. GPIO实战:按键驱动与消抖

按键驱动,可以说是嵌入式Linux驱动里最基础的入门项目了。但你别小看它——我见过不少工程师,写个按键驱动能跑,一上产品就出问题。问题出在哪?十有八九是消抖没处理好。

这一章,咱们就从硬件原理开始,一步步把按键驱动做扎实。我会把我在项目里踩过的坑、总结的经验都抖出来。

3.1 硬件原理与电路分析

先看硬件。按键的本质是什么?就是一个机械开关。按下时导通,松开时断开。但问题就出在这个“机械”上。

你想想看,金属弹片在接触的瞬间,会反复弹跳几次。这个时间大概持续5-20ms。如果驱动不做处理,一次按下可能被识别成好几次。

核心要点:按键电路设计的关键在于上拉/下拉电阻的选择。T113的GPIO内部有弱上拉,但我建议外部再加一个10kΩ的上拉电阻,更可靠。

我常用的按键电路是这样的:

// 按键硬件连接(以GPIO PD12为例)
// VCC_3.3V ---[10kΩ]--- GPIO_PD12 ---[按键]--- GND
// 按键按下 → GPIO读到低电平
// 按键松开 → GPIO读到高电平

为什么选10kΩ?阻值太小,功耗大;阻值太大,抗干扰能力差。10kΩ是个平衡点,我在多个项目里验证过。

参数 推荐值 说明
上拉电阻 10kΩ 平衡功耗与抗干扰
去耦电容 100nF 滤除高频噪声
按键弹跳时间 5-20ms 取决于按键品质

注意:不要完全依赖内部上拉。T113的内部上拉约50kΩ,抗干扰能力弱。我曾经在一个工控项目里吃过这个亏——现场电机一启动,按键就误触发。后来换成外部10kΩ上拉,问题解决。

3.2 中断方式实现按键驱动

轮询方式太浪费CPU了。咱们直接上中断。T113的GPIO支持边沿触发中断,正好适合按键场景。

驱动实现的核心步骤:

  1. 设备树配置GPIO为中断模式
  2. 注册中断处理函数
  3. 在中断上下文中处理按键事件
  4. 通过等待队列或工作队列通知用户态

设备树配置示例:

// sun8i-t113.dtsi 中的按键节点
key_pd12: key@0 {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    pinctrl-0 = <&key_pd12_pins>;
    gpios = <&pio PD 12 GPIO_ACTIVE_LOW>;
    linux,code = <KEY_ENTER>;
    interrupt-parent = <&pio>;
    interrupts = <PD 12 IRQ_TYPE_EDGE_FALLING>;
};

驱动代码核心片段:

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_dev *key = dev_id;
    
    // 读取当前GPIO电平
    int val = gpio_get_value(key->gpio);
    
    // 这里先不做消抖,后面再优化
    // 将按键事件上报到input子系统
    input_report_key(key->input, KEY_ENTER, !val);
    input_sync(key->input);
    
    return IRQ_HANDLED;
}

static int key_probe(struct platform_device *pdev)
{
    // ... 省略资源获取代码
    
    // 申请中断
    ret = request_irq(key->irq, key_irq_handler,
                      IRQF_TRIGGER_FALLING,
                      "key_pd12", key);
    if (ret) {
        dev_err(&pdev->dev, "request irq failed\n");
        return ret;
    }
    
    return 0;
}

经验之谈:中断处理函数里不要做耗时操作。我习惯只做标志位设置和事件上报,真正的处理逻辑放到工作队列或定时器里。这样能保证中断响应速度。

3.3 软件消抖与硬件消抖对比

好,现在问题来了——上面那个驱动,按下一次按键,可能会触发好几次中断。这就是机械弹跳导致的。

消抖方案有两种:硬件消抖和软件消抖。

方案 原理 优点 缺点
硬件消抖 RC低通滤波 + 施密特触发器 不占用CPU,实时性好 增加BOM成本,灵活性差
软件消抖 延时采样或状态机判断 零成本,可灵活调整参数 占用CPU时间

我个人更倾向软件消抖。为什么?因为灵活。同一个硬件平台,不同按键的弹跳特性可能不一样,软件可以单独调参数。

最常用的软件消抖算法是“延时采样法”:

// 延时采样消抖实现
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_dev *key = dev_id;
    
    // 关闭中断,防止重复触发
    disable_irq_nosync(key->irq);
    
    // 启动定时器,20ms后重新采样
    mod_timer(&key->timer, jiffies + msecs_to_jiffies(20));
    
    return IRQ_HANDLED;
}

static void key_timer_callback(struct timer_list *t)
{
    struct key_dev *key = from_timer(key, t, timer);
    int val = gpio_get_value(key->gpio);
    
    // 再次确认电平状态
    if (val == 0) {  // 按下状态
        input_report_key(key->input, KEY_ENTER, 1);
        input_sync(key->input);
    }
    
    // 重新使能中断
    enable_irq(key->irq);
}

还有一种更优雅的方式——状态机消抖。它不依赖定时器,通过连续采样判断:

// 状态机消抖(伪代码)
enum key_state {
    STATE_IDLE,      // 空闲
    STATE_DEBOUNCE,  // 消抖中
    STATE_PRESSED,   // 已按下
    STATE_RELEASE    // 释放中
};

// 每10ms调用一次
void key_scan(struct key_dev *key)
{
    int val = gpio_get_value(key->gpio);
    
    switch (key->state) {
    case STATE_IDLE:
        if (val == 0) {  // 检测到低电平
            key->state = STATE_DEBOUNCE;
            key->count = 0;
        }
        break;
        
    case STATE_DEBOUNCE:
        if (val == 0) {
            key->count++;
            if (key->count >= 3) {  // 连续3次采样为低
                // 确认按下
                input_report_key(key->input, KEY_ENTER, 1);
                key->state = STATE_PRESSED;
            }
        } else {
            key->state = STATE_IDLE;  // 抖动,回到空闲
        }
        break;
    // ... 省略释放状态
    }
}

推荐做法:量产项目中,我建议用状态机消抖。它比延时采样更可靠,不会因为系统负载高导致定时器不准。而且状态机可以同时处理多个按键,扩展性好。

3.4 实测波形分析

理论说完了,咱们看看实际波形。我用示波器抓过按键按下瞬间的GPIO信号,你们看看这个图:

按键按下瞬间GPIO波形(示波器实测) 3.3V 0V 稳定高电平 弹跳区 稳定低电平 按下瞬间 松开瞬间 约10ms弹跳 红色区域为机械弹跳,持续时间约5-20ms 消抖的目的就是滤除这段不稳定信号

从波形可以清楚看到:

  • 按下瞬间,信号在高低电平之间反复跳变约10ms
  • 之后进入稳定的低电平(按下状态)
  • 松开时同样会出现弹跳

我当初第一次看到这个波形时,还挺惊讶的——原来机械开关这么“不靠谱”。但这就是物理现实,咱们做驱动的必须面对。

调试技巧:如果你没有示波器,可以用一个简单方法验证弹跳——在中断处理函数里加一个计数器,然后快速按一次按键。如果计数器值大于1,说明有弹跳。我在调试阶段经常用这招。

最后总结一下消抖参数的选择:

按键类型 弹跳时间 推荐消抖时间
轻触开关(优质) 3-8ms 15ms
轻触开关(普通) 5-15ms 20ms
机械按键 10-30ms 30-50ms
薄膜按键 1-5ms 10ms

嗯,到这里按键驱动的基础内容就差不多了。记住一句话:按键驱动不难,难的是把消抖做好。我见过太多产品因为消抖没处理好,出现按键双击、误触发的问题。希望这一章的内容能帮你少走弯路。