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支持边沿触发中断,正好适合按键场景。
驱动实现的核心步骤:
- 设备树配置GPIO为中断模式
- 注册中断处理函数
- 在中断上下文中处理按键事件
- 通过等待队列或工作队列通知用户态
设备树配置示例:
// 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信号,你们看看这个图:
从波形可以清楚看到:
- 按下瞬间,信号在高低电平之间反复跳变约10ms
- 之后进入稳定的低电平(按下状态)
- 松开时同样会出现弹跳
我当初第一次看到这个波形时,还挺惊讶的——原来机械开关这么“不靠谱”。但这就是物理现实,咱们做驱动的必须面对。
调试技巧:如果你没有示波器,可以用一个简单方法验证弹跳——在中断处理函数里加一个计数器,然后快速按一次按键。如果计数器值大于1,说明有弹跳。我在调试阶段经常用这招。
最后总结一下消抖参数的选择:
| 按键类型 | 弹跳时间 | 推荐消抖时间 |
|---|---|---|
| 轻触开关(优质) | 3-8ms | 15ms |
| 轻触开关(普通) | 5-15ms | 20ms |
| 机械按键 | 10-30ms | 30-50ms |
| 薄膜按键 | 1-5ms | 10ms |
嗯,到这里按键驱动的基础内容就差不多了。记住一句话:按键驱动不难,难的是把消抖做好。我见过太多产品因为消抖没处理好,出现按键双击、误触发的问题。希望这一章的内容能帮你少走弯路。