4、GPIO驱动开发:从框架到实战

GPIO,说白了就是芯片的“手脚”。你想让灯亮,它就输出高电平;你想检测按键,它就读取电平变化。在机顶盒里,GPIO控制着红外接收头、前面板LED、复位按键……几乎无处不在。

这一章,我们就把GPIO驱动开发的完整链路走一遍。从内核框架,到API调用,再到设备树描述,最后写一个带中断的按键驱动。嗯,都是实战干货。

4.1 GPIO子系统框架:别被“分层”吓到

Linux GPIO子系统分三层:

  • 硬件层:芯片厂商实现的gpio_chip,负责读写寄存器
  • 核心层:gpiolib,提供统一的API接口
  • 用户层:/sys/class/gpio 或者字符设备

我个人习惯把核心层看作“中间人”。你写驱动时,基本只跟gpiolib打交道。它帮你屏蔽了不同芯片的差异。

关键数据结构

struct gpio_chip {
    const char *label;
    int base;           // GPIO编号起始值
    int ngpio;          // 本芯片管理的GPIO数量
    int (*direction_input)(struct gpio_chip *gc, unsigned offset);
    int (*direction_output)(struct gpio_chip *gc, unsigned offset, int value);
    int (*get)(struct gpio_chip *gc, unsigned offset);
    void (*set)(struct gpio_chip *gc, unsigned offset, int value);
    int (*to_irq)(struct gpio_chip *gc, unsigned offset);
    // ... 还有不少,但上面这几个最常用
};

我在项目中遇到过,有些芯片的to_irq接口没实现,导致GPIO无法申请中断。排查了半天,最后发现是芯片BSP包没更新。所以拿到新板子,先检查这个回调函数有没有注册。

4.2 gpiolib API使用:记住这几个就够了

gpiolib提供的API,说白了就三类:申请、读写、释放。我整理了一张速查表:

功能API说明
申请GPIOgpio_request(unsigned gpio, const char *label)返回0表示成功
设置方向gpio_direction_input(gpio)输入模式
设置方向gpio_direction_output(gpio, value)输出模式,同时设置初始值
读取电平gpio_get_value(gpio)返回0或1
设置电平gpio_set_value(gpio, value)0或1
申请中断gpio_to_irq(gpio)返回中断号
释放GPIOgpio_free(gpio)别忘了调用

小技巧:gpio_request失败时,用cat /sys/kernel/debug/gpio查看哪些GPIO已被占用。我调试时经常用这招。

你想想看,如果只是点个灯,代码其实就几行:

gpio_request(100, "my_led");
gpio_direction_output(100, 0);
gpio_set_value(100, 1);  // 亮灯
// 用完后
gpio_set_value(100, 0);
gpio_free(100);

但实际项目中,我建议用devm_gpio_request这类资源管理版本。它会在驱动卸载时自动释放,省得你忘了调用gpio_free。

4.3 在设备树中描述GPIO:让硬件“说话”

设备树就是硬件的“身份证”。GPIO在设备树里怎么描述?看个例子:

/ {
    leds {
        compatible = "gpio-leds";
        power_led {
            label = "power";
            gpios = &gpio1 5 GPIO_ACTIVE_HIGH;
            default-state = "on";
        };
    };

    keys {
        compatible = "gpio-keys";
        power_key {
            label = "power key";
            gpios = &gpio2 3 GPIO_ACTIVE_LOW;
            linux,code = <KEY_POWER>;
        };
    };
};

这里要注意几点:

  • gpios = &gpio1 5 GPIO_ACTIVE_HIGH 表示使用gpio1控制器,偏移5,高电平有效
  • GPIO_ACTIVE_HIGHGPIO_ACTIVE_LOW 是宏定义,告诉内核电平极性
  • gpio-keys是内核自带的驱动,你只需要在设备树里描述按键属性

我曾经踩过一个坑:设备树里写的是GPIO_ACTIVE_LOW,但电路设计是按键按下拉低。驱动里又做了一次取反,结果逻辑反了。嗯,这里要记住:设备树描述的是硬件行为,驱动里不要再额外处理。

4.4 编写GPIO按键驱动:从零开始

好,我们动手写一个完整的按键驱动。它检测GPIO电平变化,上报给输入子系统。

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

struct my_key_data {
    int gpio;
    int irq;
    int key_code;
    struct input_dev *input;
};

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct my_key_data *data = dev_id;
    int val;

    val = gpio_get_value(data->gpio);
    // 上报按键事件
    input_report_key(data->input, data->key_code, !val);
    input_sync(data->input);

    return IRQ_HANDLED;
}

static int my_key_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_key_data *data;
    int ret;

    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    // 从设备树获取GPIO
    data->gpio = of_get_named_gpio(dev->of_node, "key-gpios", 0);
    if (!gpio_is_valid(data->gpio)) {
        dev_err(dev, "invalid GPIO\n");
        return -EINVAL;
    }

    // 申请GPIO并设置为输入
    ret = devm_gpio_request_one(dev, data->gpio, GPIOF_IN, "my_key");
    if (ret) {
        dev_err(dev, "gpio request failed\n");
        return ret;
    }

    // 获取中断号
    data->irq = gpio_to_irq(data->gpio);
    if (data->irq < 0) {
        dev_err(dev, "gpio_to_irq failed\n");
        return data->irq;
    }

    // 申请中断
    ret = devm_request_irq(dev, data->irq, key_irq_handler,
                           IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                           "my_key", data);
    if (ret) {
        dev_err(dev, "irq request failed\n");
        return ret;
    }

    // 注册输入设备
    data->input = devm_input_allocate_device(dev);
    if (!data->input)
        return -ENOMEM;

    data->input->name = "my_key";
    set_bit(EV_KEY, data->input->evbit);
    set_bit(KEY_POWER, data->input->keybit);

    ret = input_register_device(data->input);
    if (ret) {
        dev_err(dev, "input register failed\n");
        return ret;
    }

    platform_set_drvdata(pdev, data);
    dev_info(dev, "my key driver loaded\n");

    return 0;
}

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

static struct platform_driver my_key_driver = {
    .probe = my_key_probe,
    .driver = {
        .name = "my_key",
        .of_match_table = my_key_of_match,
    },
};

module_platform_driver(my_key_driver);
MODULE_LICENSE("GPL");

这段代码里,我用了devm_系列函数,省去了手动释放资源的麻烦。中断处理函数里,读取GPIO电平并取反——因为设备树里定义的是低电平有效。

注意:中断处理函数里不要做耗时操作。按键消抖、长按检测这些逻辑,建议放到用户空间或者用内核的input子系统自带功能处理。

4.5 中断处理:别让CPU空转

GPIO按键驱动里,中断处理是核心。为什么不用轮询?你想想看,如果CPU每秒轮询100次,就为了等一个按键,那得多浪费。

中断处理分上半部和下半部:

  • 上半部:快速处理,标记中断状态。比如读取GPIO电平,上报事件。
  • 下半部:处理耗时操作。比如按键消抖、长按检测。可以用tasklet、workqueue或threaded IRQ。

我个人习惯用threaded IRQ,它把中断处理放到内核线程里,可以睡眠,适合做复杂操作:

ret = devm_request_threaded_irq(dev, irq, NULL,
                                key_irq_handler_thread,
                                IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                                "my_key", data);

第一个参数是上半部处理函数,传NULL表示不用上半部。第二个参数是下半部线程函数。这样写,代码更清晰。

我曾经在项目里遇到一个问题:按键按下时,中断触发多次。原因是按键抖动,电平在短时间内跳变多次。解决方案是在硬件上加RC滤波,或者在软件里做消抖——比如在中断触发后,延迟20ms再读取电平。

避坑指南:我曾经在中断处理函数里调用了printk,结果系统直接卡死。因为printk可能引起调度,而中断上下文不允许睡眠。调试时用pr_debug或者tracepoint,别用printk。

4.6 总结:GPIO驱动开发的“三板斧”

回顾一下,GPIO驱动开发其实就三步:

  1. 设备树描述:告诉内核,哪个引脚是什么功能
  2. 驱动初始化:申请GPIO、设置方向、注册中断
  3. 中断处理:读取电平、上报事件、处理抖动

嗯,掌握了这些,机顶盒上常见的GPIO按键、LED、红外接收头驱动,你都能轻松搞定。下一章,我们会深入I2C驱动开发,那又是另一片天地了。