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 值,万一别的模块也用了这个号,调试起来多头疼。
| 接口 | 作用 | 注意事项 |
|---|---|---|
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 动态映射,这样设备树才能灵活配置。
中断注册的典型流程:
- 在 probe 中获取 GPIO 对应的中断号:
irq = gpiod_to_irq(desc); - 调用
devm_request_threaded_irq()注册中断处理函数。 - 在中断处理函数中,用工作队列或 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_chip 的回调函数里。每一层都有清晰的边界。我在项目中调试 GPIO 问题时,就是顺着这条链路一层层排查,基本没有找不到根因的。
4.5 避坑总结
最后,我把这些年踩过的 GPIO 相关的坑列出来,你写代码时多留个心眼:
- 中断上下文不能睡眠:spinlock 保护共享数据,用 threaded irq 处理耗时操作。
- 设备树 GPIO 属性别写错:
gpios和gpio就差一个 s,解析结果完全不同。我曾经因为这个查了两天。 - 注意引脚复用:GPIO 功能可能和别的外设冲突,记得在设备树里配好 pinctrl。
- 用户空间操作时加锁:多个进程同时操作同一个 GPIO,电平状态可能乱掉。应用层要做好互斥。
- 中断触发方式要匹配硬件:上升沿、下降沿、电平触发,选错了要么丢中断,要么频繁触发。