4. GPIO 子系统驱动:gpiolib 框架、中断注册、用户空间接口

GPIO,说白了就是芯片上那些可以拉高拉低的引脚。你写嵌入式,天天跟它打交道。但真正把它用对、用好,其实有不少门道。这一章,我带你从驱动层面把 GPIO 子系统彻底吃透。

4.1 gpiolib 框架:别自己操作寄存器了

早期做 GPIO 驱动,大家都是直接操作寄存器。我刚开始做项目时也这么干,结果换了个芯片平台,代码几乎全废。后来内核有了 gpiolib,说白了就是帮你把 GPIO 的操作抽象成一套标准接口。

gpiolib 的核心结构体是 struct gpio_chip。你写驱动时,只需要填充这个结构体,然后注册到内核。框架会自动帮你处理设备树解析、sysfs 接口、中断映射等杂事。

核心要点:gpiolib 把 GPIO 控制器抽象为 gpio_chip,每个引脚抽象为 gpio_desc。你操作的是 desc,而不是物理地址。

来看一个典型的 gpio_chip 实现片段:

static struct gpio_chip my_gpio_chip = {
    .label          = "my-gpio",
    .owner          = THIS_MODULE,
    .direction_input  = my_gpio_direction_input,
    .direction_output = my_gpio_direction_output,
    .get            = my_gpio_get,
    .set            = my_gpio_set,
    .base           = -1,                // 动态分配
    .ngpio          = 16,
    .can_sleep      = false,
};

static int my_gpio_probe(struct platform_device *pdev)
{
    // 从设备树获取寄存器基址等资源
    // 初始化硬件
    return gpiochip_add_data(&my_gpio_chip, priv_data);
}

这里有个细节:base = -1 表示让内核动态分配 GPIO 编号。我个人习惯用动态分配,避免跟其他驱动冲突。你想想看,如果硬编码一个 base 值,万一别的模块也用了这个号,调试起来多头疼。

gpiolib 核心接口速查
接口 作用 注意事项
gpiochip_add_data() 注册 gpio_chip 到内核 probe 中调用,失败要回滚
gpiochip_remove() 注销 gpio_chip remove 中调用,确保无引用
gpiod_get() 获取 GPIO desc 配合设备树使用,自动解析
gpiod_direction_output() 设置输出方向并初始化电平 第二个参数是初始值

4.2 中断注册:别让 CPU 空转

GPIO 最常用的场景就是检测外部事件。比如按键按下、传感器触发。如果轮询,CPU 就废了。所以必须用中断。

gpiolib 把中断也封装好了。你只需要在 gpio_chip 里实现 .to_irq 回调,框架会自动把 GPIO 号和中断号关联起来。

我的经验:很多新手直接在 gpio_chip 里写死中断号,这是错的。应该通过 .to_irq 动态映射,这样设备树才能灵活配置。

中断注册的典型流程:

  1. 在 probe 中获取 GPIO 对应的中断号:irq = gpiod_to_irq(desc);
  2. 调用 devm_request_threaded_irq() 注册中断处理函数。
  3. 在中断处理函数中,用工作队列或 tasklet 处理耗时操作。

代码示例:

static irqreturn_t my_gpio_irq_handler(int irq, void *data)
{
    struct my_priv *priv = data;
    // 注意:这里不能睡眠,不能调用可能导致阻塞的函数
    schedule_work(&priv->work);
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_priv *priv;
    int irq, ret;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    // ... 获取 gpio desc ...

    irq = gpiod_to_irq(priv->gpio);
    if (irq < 0)
        return irq;

    ret = devm_request_threaded_irq(&pdev->dev, irq, NULL,
                    my_gpio_irq_handler,
                    IRQF_TRIGGER_RISING | IRQF_ONESHOT,
                    "my-gpio-irq", priv);
    return ret;
}

避坑指南:我曾经在一个项目里,中断处理函数里直接调用了 i2c 读写,结果系统随机死锁。后来才意识到,中断上下文不能调用可能睡眠的函数。解决办法就是用 threaded irq 或者 workqueue。

4.3 用户空间接口:让应用层也能玩 GPIO

有时候,驱动层不需要太复杂的逻辑,直接把 GPIO 暴露给用户空间更灵活。gpiolib 提供了两种方式:sysfs(旧)和 chardev(新)。

我建议新项目直接用 chardev 接口。sysfs 虽然简单,但每个 GPIO 一个文件,大量操作时性能很差。chardev 通过 ioctl 批量操作,效率高得多。

用户空间操作 GPIO 的步骤:

  • 打开 /dev/gpiochipN 设备文件。
  • 通过 GPIO_GET_LINEHANDLE_IOCTL 请求一个或多个引脚。
  • 使用 GPIOHANDLE_SET_LINE_VALUES_IOCTL 读写电平。
  • 使用 GPIO_GET_LINEEVENT_IOCTL 监听中断事件。

来看一个用户空间读取按键状态的例子:

#include <linux/gpio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/dev/gpiochip0", O_RDONLY);
    struct gpiohandle_request req = {
        .lineoffsets[0] = 4,      // GPIO4
        .flags = GPIOHANDLE_REQUEST_INPUT,
        .lines = 1,
    };

    ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);

    struct gpiohandle_data data;
    ioctl(req.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
    printf("GPIO4 value: %d\n", data.values[0]);

    close(req.fd);
    close(fd);
    return 0;
}

嗯,这里要注意:用户空间操作 GPIO 时,驱动层必须确保对应的引脚没有被其他驱动占用。否则 ioctl 会返回 -EBUSY。

4.4 整体框架:一张图看懂

说了这么多,我把整个 GPIO 子系统的数据流画出来。你一看就明白:

GPIO 子系统整体框架 用户空间 /dev/gpiochipN (chardev) | /sys/class/gpio (sysfs) 内核空间 gpiolib 核心:gpio_desc、gpio_chip、中断映射 gpiod_xxx() 系列 API、pinctrl 子系统交互 硬件抽象层 gpio_chip 回调:direction_input / get / set / to_irq 硬件层 SoC GPIO 寄存器、外部中断控制器 用户空间通过 ioctl 或 sysfs 操作 GPIO

从这张图你能看到,用户空间请求最终会落到 gpio_chip 的回调函数里。每一层都有清晰的边界。我在项目中调试 GPIO 问题时,就是顺着这条链路一层层排查,基本没有找不到根因的。

4.5 避坑总结

最后,我把这些年踩过的 GPIO 相关的坑列出来,你写代码时多留个心眼:

  • 中断上下文不能睡眠:spinlock 保护共享数据,用 threaded irq 处理耗时操作。
  • 设备树 GPIO 属性别写错:gpios 和 gpio 就差一个 s,解析结果完全不同。我曾经因为这个查了两天。
  • 注意引脚复用:GPIO 功能可能和别的外设冲突,记得在设备树里配好 pinctrl。
  • 用户空间操作时加锁:多个进程同时操作同一个 GPIO,电平状态可能乱掉。应用层要做好互斥。
  • 中断触发方式要匹配硬件:上升沿、下降沿、电平触发,选错了要么丢中断,要么频繁触发。