嵌入式 Linux 驱动开发学习笔记:设备树、pinctrl 与 GPIO 驱动开发
一、前言
在嵌入式 Linux 驱动开发中,驱动程序不仅需要实现相应的控制逻辑,还需要获取硬件资源信息。例如,LED 连接在哪个 GPIO 引脚、引脚应该配置成什么功能,以及设备使用了哪些寄存器等。
如果将这些硬件信息全部直接写在驱动代码中,那么当硬件平台发生变化时,就可能需要修改甚至重新编写驱动。因此,Linux 引入了设备树和设备驱动分离的设计思想,将硬件描述信息与驱动逻辑分开管理。
本文主要介绍设备树的基本结构、设备树节点解析、pinctrl 引脚配置,以及 GPIO 的常用操作方法,并以 LED 驱动为例梳理相关开发流程。
二、设备树基础
2.1 什么是设备树
设备树(Device Tree,简称 DT)用于描述硬件平台的组成和资源信息。内核可以根据设备树中的描述,识别设备并获取相应的硬件配置。
常见的设备树文件包括:·
DTS:设备树源文件,通常用于描述具体开发板的硬件信息。·
DTSI:设备树头文件,可以被多个 DTS 文件引用,用于复用公共硬件描述。·
DTB:设备树二进制文件,通常由 DTS 编译生成,并在系统启动时提供给内核。
设备树的基本作用,是让硬件描述不必全部写死在驱动代码中。
2.2 设备树节点
设备树通常采用树形结构描述硬件设备。一个节点可以包含子节点,也可以包含多个属性。
例如:
led0 {
compatible = "demo,led";
status = "okay";
};
其中:·
led0:节点名称。
compatible:设备与驱动匹配时使用的兼容字符串。
status:表示设备当前是否启用。
常见属性还包括:·
reg:描述设备的地址和资源范围,具体含义取决于父节点的地址空间定义。
interrupts:描述设备使用的中断资源。
gpios:描述设备使用的 GPIO。·
pinctrl-names、pinctrl-0:用于指定引脚配置状态。
需要注意,属性是否有效、如何解析,都与具体设备节点及其绑定规则有关。
三、Linux 驱动如何解析设备树
Linux 内核提供了 OF(Open Firmware)相关接口,用于访问设备树节点和属性。
3.1 查找设备树节点
struct device_node *np;
np = of_find_node_by_path("/led0");
if (!np) {
pr_err("cannot find led node\n");
return -ENODEV;
}
of_find_node_by_path() 根据设备树路径查找节点。实际项目中,应根据板级 DTS 中的真实路径填写参数。
3.2 读取整数属性
u32 value;
int ret;
ret = of_property_read_u32(np, "some-value", &value);
if (ret) {
pr_err("read property failed\n");
return ret;
}
该接口用于读取单个 32 位整数属性。
如果属性包含多个整数,可以使用:
u32 values2;
ret = of_property_read_u32_array(
np, "some-values", values, 2);
需要注意:属性名称、数据类型、数组长度都必须与 DTS 中的实际定义一致。读取失败时,应检查节点是否正确、属性是否存在以及数据格式是否匹配。
3.3 compatible 匹配
在 platform 驱动中,通常会通过 compatible 属性将设备树节点与驱动匹配起来。例如:
static const struct of_device_id led_of_match\[\] = {
{ .compatible = "demo,led" },
{ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
设备树中的兼容字符串必须与驱动中的字符串一致,否则可能无法正常匹配。
四、pinctrl:配置引脚功能
4.1 为什么需要 pinctrl
一个芯片引脚往往具有多种复用功能。例如,同一个引脚可能被配置为 GPIO,也可能被配置为 UART、SPI 或 I²C 功能。
因此,在使用 GPIO 之前,需要确认引脚已经配置成正确的功能。
pinctrl 主要负责引脚复用及相关电气配置,具体能力取决于芯片和对应的 pinctrl 驱动。
可以将整个过程理解为:
1.选择正确的引脚。
2.将引脚配置成所需功能。
3.根据需要设置上下拉、驱动能力等参数。
4.再通过 GPIO 接口控制引脚电平。
4.2 pinctrl 与 GPIO 的区别
两者容易混淆,但职责不同:
|------------|----------------------|
| 模块 | 主要作用 |
| pinctrl | 配置引脚复用功能和相关电气属性 |
| GPIO | 设置引脚输入或输出方向,并读取或控制电平 |
例如,要控制一个 LED,通常需要先保证对应引脚处于 GPIO 功能,再将 GPIO 配置为输出,最后设置高低电平。
五、GPIO 常用操作接口
Linux 内核提供了 GPIO 相关接口。在较早的内核版本中,常见的整数 GPIO 接口包括以下函数。
5.1 申请 GPIO
ret = gpio_request(gpio, "led_gpio");
if (ret) {
pr_err("gpio request failed\n");
return ret;
}
申请 GPIO 可以避免不同驱动随意占用同一资源。
5.2 配置输出方向
ret = gpio_direction_output(gpio, 0);
if (ret) {
gpio_free(gpio);
return ret;
}
第二个参数通常表示初始输出电平。实际 LED 的亮灭状态还取决于硬件连接方式:有些 LED 是低电平点亮,有些则是高电平点亮。
5.3 设置 GPIO 电平
gpio_set_value(gpio, 1);
gpio_set_value(gpio, 0);
这两个调用分别设置高电平和低电平。应当在 GPIO 已成功申请并配置为输出后使用。
5.4 释放 GPIO
gpio_free(gpio);
当驱动不再使用 GPIO 时,应释放已经申请的资源。
需要注意,GPIO 接口属于较早的整数编号接口。新驱动通常更推荐使用 GPIO descriptor 接口,例如 gpiod_get()、gpiod_set_value() 等,具体选择应结合目标内核版本和平台支持情况。
六、LED 驱动开发流程
一个基于设备树的 LED 驱动,大体可以按照以下步骤实现:
1.在 DTS 中描述 LED 设备及相关资源。
2.在驱动中编写 of_match_table。
3.实现驱动的 probe() 函数。
4.在 probe() 中获取设备资源并申请 GPIO。
5.配置 GPIO 的输入输出方向。
6.实现 LED 控制逻辑。
7.在设备移除时释放资源。
如果采用 platform 驱动模型,驱动通常在匹配成功后进入 probe(),而不是仅仅依靠模块加载函数自行查找和操作所有硬件。
此外,设备树节点中应明确设置正确的 status,并确认 DTS 已经编译进系统实际使用的 DTB。
七、常见问题排查
7.1 找不到设备树节点
重点检查:
节点路径是否正确。
实际启动时使用的 DTB 是否包含该节点。
status 是否为可用状态。
驱动是否采用了正确的匹配方式。
7.2 读取属性失败
如果出现 of_property_read_u32_array() 返回错误,应优先检查:
属性名称是否一致。
属性是否存在。
属性中的元素数量是否满足读取要求。
数据类型是否正确。
不要只根据驱动代码推测属性一定存在,应以实际 DTS 内容为准。
7.3 GPIO 申请失败
可能的原因包括 GPIO 编号错误、GPIO 已被其他驱动占用,或者设备树资源描述不正确。
7.4 LED 无法正常亮灭
应按顺序排查引脚复用、GPIO 方向、电平控制、LED 有效电平以及硬件连接,而不是直接认定为 GPIO 接口失效。
八、总结
设备树、pinctrl 和 GPIO 是嵌入式 Linux 驱动开发中经常接触的基础内容。
设备树用于描述硬件资源,驱动通过 OF 接口获取相关信息;pinctrl 负责配置引脚功能及电气属性;GPIO 则用于控制或读取引脚电平。
掌握这三部分之后,就可以进一步学习中断、等待队列以及更完整的设备驱动框架。
学习建议: 先确认 DTS 中的硬件描述,再检查驱动匹配和资源获取,最后验证 GPIO 的实际电平变化。按这个顺序排查,通常比直接修改驱动代码更有效。