3、GPIO驱动实战:GPIO子系统框架、设备树配置、gpiolib API使用
GPIO,说白了就是芯片最基础的“手脚”。拉高拉低,输入输出,看似简单,但实际项目里翻车最多的往往就是它。我刚开始做驱动时,觉得GPIO嘛,直接操作寄存器不就完了?后来发现,Linux内核早就给你搭好了一套完整的框架——GPIO子系统。你不用它,代码就进不了主线,别人也看不懂。
这一节,我们就来拆解这个子系统。从框架设计,到设备树怎么配,再到API怎么调,一条线走通。
3.1 GPIO子系统框架:三层架构
GPIO子系统的设计,其实就三层:
- 硬件层(GPIO Controller Driver):直接操作寄存器,跟具体芯片打交道。
- 核心层(gpiolib):提供统一的接口管理,屏蔽硬件差异。
- 用户层(Consumer API):给其他驱动或用户空间用的标准API。
你想想看,如果没有这个分层,每个驱动都去读datasheet操作寄存器,那代码得多乱?
核心思想:GPIO子系统把“谁控制”和“谁使用”彻底分开。控制者只负责实现底层操作,使用者只管调用标准接口。
下面这张图,是我画的一个简化框架。你看一眼就能明白数据是怎么流动的。
我个人习惯,在写一个新的GPIO控制器驱动时,先搞清楚它属于哪一层。如果是芯片原厂,你写的是硬件层;如果是做板级支持,你大概率只用用户层API。
3.2 设备树配置:把引脚信息告诉内核
设备树,就是用来描述硬件拓扑的。GPIO的配置,主要涉及两个东西:
- GPIO Controller节点:描述芯片有多少个GPIO bank,每个bank的基地址、中断号等。
- GPIO Consumer节点:描述某个外设用了哪些GPIO,以及怎么用。
先看一个典型的控制器节点。以某款ARM芯片为例:
gpio0: gpio@ff7a0000 {
compatible = "vendor,gpio-controller";
reg = <0xff7a0000 0x1000>;
interrupts = <0 42 4>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
这里有几个关键属性,我解释一下:
| 属性 | 含义 | 说明 |
|---|---|---|
gpio-controller |
标识这是一个GPIO控制器 | 必须有,否则内核不认 |
#gpio-cells |
描述一个GPIO引用需要几个cell | 通常为2:引脚号 + 标志位 |
interrupt-controller |
标识它也能处理中断 | 可选,但常用 |
#interrupt-cells |
中断描述需要的cell数 | 通常为2:中断类型 + 触发方式 |
小提示:#gpio-cells = <2> 中的第二个cell,通常用来表示GPIO的极性。比如 GPIO_ACTIVE_HIGH 或 GPIO_ACTIVE_LOW。这个标志位在驱动里会直接影响 gpiod_set_value 的行为。
再看一个消费节点的例子。比如一个LED设备,用到了GPIO0的第5号引脚:
led_red: led {
compatible = "gpio-leds";
label = "red";
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
这里 gpios 属性引用了 gpio0 控制器的第5号引脚,高电平有效。内核解析时,会通过 of_get_named_gpio_flags 拿到引脚号和标志位。
我曾经在一个项目里遇到过一个问题:设备树里GPIO的cell数配错了,导致驱动申请引脚时一直返回-EINVAL。查了半天才发现是 #gpio-cells 写成了3,而驱动里只传了2个参数。嗯,这种低级错误,配完设备树一定要对一下。
3.3 gpiolib API使用:从申请到释放
gpiolib提供了一套标准的API,分为“旧版”(gpio_xxx)和“新版”(gpiod_xxx)。我建议你直接用新版,因为它是基于描述符的,更安全,也支持设备树自动解析。
下面是一个典型的使用流程:
- 获取GPIO描述符:通过设备树节点或引脚号拿到
struct gpio_desc *。 - 设置方向:输入还是输出。
- 读写值:
gpiod_set_value/gpiod_get_value。 - 释放:不用时归还。
看代码:
#include <linux/gpio/consumer.h>
struct gpio_desc *desc;
int ret;
// 从设备树节点获取GPIO
desc = gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(desc)) {
dev_err(&pdev->dev, "Failed to get GPIO\n");
return PTR_ERR(desc);
}
// 设置方向(如果上面没指定,可以单独调)
ret = gpiod_direction_output(desc, 0);
if (ret) {
dev_err(&pdev->dev, "Failed to set direction\n");
gpiod_put(desc);
return ret;
}
// 输出高电平
gpiod_set_value(desc, 1);
// 读取输入值
int val = gpiod_get_value(desc);
// 释放
gpiod_put(desc);
这里要注意几个细节:
gpiod_get的第二个参数是con_id,对应设备树属性名的后缀。比如属性是led-gpios,con_id就是led。GPIOD_OUT_LOW是初始化标志,表示申请时直接设为输出低电平。还有GPIOD_IN、GPIOD_OUT_HIGH等。- 返回值一定要用
IS_ERR检查,不要直接跟NULL比。
注意:旧版API gpio_request 和 gpio_direction_output 仍然可用,但新驱动不建议再用。新版API会自动处理设备树的极性反转,旧版不会。如果你混用,很容易出现“明明设了高电平,量出来却是低”的诡异现象。
3.4 中断支持:GPIO的另一半能力
GPIO不只是用来读写的,它还能触发中断。gpiolib把中断也封装好了。
申请一个GPIO中断的典型步骤:
int irq;
unsigned long flags = IRQF_TRIGGER_RISING;
// 先拿到GPIO描述符
desc = gpiod_get(&pdev->dev, "button", GPIOD_IN);
// 把GPIO转成中断号
irq = gpiod_to_irq(desc);
if (irq < 0) {
dev_err(&pdev->dev, "Failed to get IRQ\n");
gpiod_put(desc);
return irq;
}
// 注册中断
ret = devm_request_threaded_irq(&pdev->dev, irq, NULL,
button_isr, flags,
"button", dev);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
gpiod_put(desc);
return ret;
}
这里我用了 devm_request_threaded_irq,它会在设备卸载时自动释放中断,省得你手动清理。我个人习惯,能用 devm_ 系列函数就用,减少资源泄漏的风险。
中断服务函数里,记得调用 gpiod_get_value 来消抖或确认电平状态。我曾经遇到过因为没消抖,按键按一次触发两次中断的情况。加个10ms的定时器延迟读取,问题就解决了。
3.5 避坑指南:我踩过的几个坑
做GPIO驱动,有些坑是共通的。我列几个典型的:
- 引脚复用冲突:同一个引脚既配成GPIO,又被其他外设(比如I2C、UART)占用。设备树里一定要检查
pinctrl配置,确保复用关系正确。 - 极性反转:设备树里配了
GPIO_ACTIVE_LOW,但驱动里直接用gpiod_set_value(1)以为就是高电平。实际上,gpiolib会自动反转,你设1,它输出0。所以,别自己再手动取反一次。 - 未申请就操作:直接调用
gpiod_set_value但之前没gpiod_get。内核会报错,但有时候是异步的,不容易定位。建议每次操作前都检查描述符是否有效。 - 中断未释放:模块卸载时忘了释放中断,下次加载时申请失败。用
devm_系列可以避免。
总结一下:GPIO子系统把复杂的东西藏起来了。你只要配好设备树,调对API,大部分工作就完成了。但底层原理还是要懂,不然出了问题,你连日志都看不懂。