第4章 GPIO子系统与按键驱动

各位同学,今天我们来聊聊GPIO子系统。说实话,这是嵌入式驱动开发里最基础也最常用的部分。我刚开始做POS机外设时,第一个独立完成的驱动就是GPIO按键——虽然简单,但麻雀虽小五脏俱全。

4.1 GPIO子系统框架(gpiolib)

Linux内核的GPIO子系统,我们通常叫它gpiolib。它的核心作用就一个:统一管理GPIO资源。你想想看,不同芯片厂商的GPIO控制器千差万别,但上层驱动开发者不想关心这些细节。gpiolib就是中间人,把底层差异全封装了。

gpiolib的架构分三层:

  • 底层:芯片厂商实现的gpio_chip,负责硬件操作
  • 中间层:gpiolib核心代码,提供统一接口
  • 上层:我们写的驱动,调用gpio_request等API

我在项目中遇到过一个问题:某款POS主板的GPIO编号和芯片手册对不上。后来发现是厂商在设备树里重新映射了编号。嗯,这里要注意——永远不要硬编码GPIO号,要用设备树或平台数据来传递。

4.2 GPIO控制接口

gpiolib提供了一套标准的API,我挑几个最常用的说说。

4.2.1 申请与释放

int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);

gpio_request是第一步。我个人习惯在probe函数里申请,在remove函数里释放。label参数建议写清楚用途,比如"power_key"、"paper_out_sensor",方便调试时查看。

注意:gpio_request失败时,别急着报错。先检查这个GPIO是不是已经被其他驱动占用了。我曾经遇到过两个驱动抢同一个GPIO的情况,最后发现是设备树里重复定义了。

4.2.2 方向设置

int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);

按键驱动一般用输入模式。但有个细节:设置方向前,GPIO的默认状态是什么?有些芯片默认是输出低电平,如果你直接设成输入,外部电路可能已经出问题了。

4.2.3 读写值

int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);

这两个函数很简单,但要注意:gpio_get_value返回的是逻辑值,不是物理电平。如果外部有上拉或下拉,返回值可能和你预想的不一样。

4.3 中断注册

按键驱动最核心的就是中断。轮询?那太浪费CPU了。POS机对响应速度有要求,用户按一下键,你得立刻反应。

4.3.1 申请中断号

int gpio_to_irq(unsigned gpio);

这个函数把GPIO号转成中断号。我建议你不要假设中断号等于GPIO号,虽然有些芯片确实这么设计,但大部分不是。

4.3.2 注册中断处理函数

int request_irq(unsigned int irq, irq_handler_t handler,
                unsigned long flags, const char *name, void *dev);

参数里flags很关键。按键一般用IRQF_TRIGGER_FALLINGIRQF_TRIGGER_RISING。但有个坑:如果按键按下时电平抖动,你会收到多次中断。这就是所谓的"按键抖动"问题。

我的经验:硬件上可以加RC滤波,软件上可以做去抖。我一般用定时器延迟10-20ms再读取GPIO状态。别用太长的延迟,用户会感觉按键反应慢。

4.4 实现一个GPIO按键驱动

好了,理论说完了,咱们动手写一个。这个驱动很简单:检测按键按下,打印一条日志,并通过input子系统上报按键事件。

4.4.1 设备树节点

key_device {
    compatible = "my-company,gpio-key";
    gpios = &gpio1 15 GPIO_ACTIVE_LOW;
    interrupt-parent = <&gpio1>;
    interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
    label = "power_key";
};

4.4.2 驱动代码框架

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/input.h>

struct gpio_key_data {
    int gpio;
    int irq;
    struct input_dev *input;
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key_data *data = dev_id;
    int val = gpio_get_value(data->gpio);
    
    // 上报按键事件
    input_report_key(data->input, KEY_POWER, !val);
    input_sync(data->input);
    
    return IRQ_HANDLED;
}

static int gpio_key_probe(struct platform_device *pdev)
{
    struct gpio_key_data *data;
    int ret;
    
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    // 获取GPIO
    data->gpio = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);
    ret = devm_gpio_request(&pdev->dev, data->gpio, "gpio_key");
    if (ret)
        return ret;
    
    gpio_direction_input(data->gpio);
    
    // 获取中断号并注册
    data->irq = gpio_to_irq(data->gpio);
    ret = devm_request_irq(&pdev->dev, data->irq, gpio_key_isr,
                           IRQF_TRIGGER_FALLING, "gpio_key", data);
    if (ret)
        return ret;
    
    // 注册input设备
    data->input = devm_input_allocate_device(&pdev->dev);
    data->input->name = "GPIO Key";
    set_bit(EV_KEY, data->input->evbit);
    set_bit(KEY_POWER, data->input->keybit);
    
    ret = input_register_device(data->input);
    if (ret)
        return ret;
    
    platform_set_drvdata(pdev, data);
    return 0;
}

static const struct of_device_id gpio_key_of_match[] = {
    { .compatible = "my-company,gpio-key" },
    { }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);

static struct platform_driver gpio_key_driver = {
    .probe = gpio_key_probe,
    .driver = {
        .name = "gpio_key",
        .of_match_table = gpio_key_of_match,
    },
};
module_platform_driver(gpio_key_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPIO key driver");

4.4.3 关键点说明

  • devm_系列函数:自动资源管理,probe失败或remove时自动释放。我强烈建议用这个,省得你忘了free。
  • input子系统:按键事件通过input子系统上报,上层应用(比如Android的KeyEvent)就能收到。
  • 中断标志:这里用了IRQF_TRIGGER_FALLING,因为设备树里GPIO_ACTIVE_LOW,按键按下时电平从高变低。

避坑指南:我曾经在某个项目里,按键按下后系统卡死。查了半天,发现是中断处理函数里调用了可能导致睡眠的函数(比如printk在特定配置下会睡眠)。记住:中断上下文不能睡眠。如果要做复杂操作,用工作队列或tasklet。

4.5 调试与验证

驱动写完了,怎么验证?我一般这么做:

  1. 加载驱动后,查看/proc/interrupts,确认中断号已注册
  2. 按一下按键,用cat /proc/interrupts看中断计数是否增加
  3. evtest工具监听input事件,看是否能收到按键上报

如果中断计数没变,先检查硬件连接——用万用表量一下GPIO引脚电平。如果硬件没问题,再查设备树里的GPIO号对不对。我遇到过最离谱的一次,是设备树里GPIO号写对了,但引脚复用配置错了,GPIO功能没使能。

4.6 小结

这一章我们走通了GPIO按键驱动的完整流程。从gpiolib框架到中断注册,再到实际代码实现。说实话,这个驱动虽然简单,但它是理解Linux驱动模型的好例子。你掌握了它,后面学I2C、SPI驱动就会轻松很多。

下一章我们会讲定时器和PWM输出,到时候会用到本章的知识。嗯,先消化一下,有问题随时交流。