在嵌入式 Linux 开发中,设备树(Device Tree)是一个绕不开的核心概念。它是描述硬件的数据结构,起源于 Open Firmware,旨在解决硬件描述信息与内核代码深度耦合的问题。对于驱动开发者而言,理解设备树不仅是配置板级信息的需要,更是深入理解 Linux 内核驱动模型的关键。

在 Linux 2.6 内核及之前的时代,ARM 架构的硬件描述信息是直接硬编码在内核源码中的。那时候,arch/arm/plat-xxx 和 arch/arm/mach-xxx 目录下充斥着大量的 C 语言文件,这些文件用来描述板卡上的 GPIO、I2C 设备、SPI 设备等硬件资源。随着 ARM 处理器和开发板数量的爆发式增长,这些板级信息文件急剧膨胀,导致 Linux 内核代码变得极其臃肿和难以维护。
这种情况最终引发了 Linux 创始人 Linus Torvalds 的强烈不满。在 2011 年 3 月的一封邮件中,他愤怒地写道:“This whole ARM thing is a f*cking pain in the ass”,并敦促 ARM 社区整改。为了解决这一问题,ARM 社区借鉴了 PowerPC 等架构已经使用的 Open Firmware 标准,引入了设备树机制。
设备树的核心思想是将硬件描述信息从内核源代码中剥离出来,形成独立的文件。这样,Linux 内核就变成了一个通用的“黑盒”,它可以运行在不同的硬件平台上,而具体的硬件配置则由外部的设备树文件在启动时传递给内核。这大大提高了内核代码的可重用性和可维护性,实现了内核与硬件的解耦。
设备树体系主要包含 DTS、DTSI、DTC 和 DTB 四个核心概念,它们共同构成了从源码到二进制的编译流程。
DTS(Device Tree Source)是设备树的源码文件,采用文本格式编写,扩展名为.dts。它描述了特定开发板的硬件信息,便于人类阅读和编辑。在一个 DTS 文件中,我们可以定义 CPU、内存、外设接口以及挂载在这些接口上的具体设备。
DTSI(Device Tree Source Include)类似于 C 语言中的头文件,扩展名为.dtsi。它通常包含某一类芯片或开发板的公共硬件信息。例如,同一款 SoC 芯片被用于多个不同的开发板,那么 SoC 内部的 CPU、控制器等固定资源就会被定义在.dtsi 文件中,而各个开发板特有的差异化配置(如 LED 连接的 GPIO 引脚)则定义在各自的.dts 文件中。.dts 文件可以通过 #include 指令包含.dtsi 文件。
DTC(Device Tree Compiler)是设备树编译器。它负责将文本格式的 DTS/DTSI 文件编译成二进制格式。在 Linux 内核源码的 scripts/dtc 目录下提供了这个工具。
DTB(Device Tree Blob)是编译后生成的二进制文件,扩展名为.dtb。它是设备树在内存中的表现形式,也是 Bootloader 传递给内核的最终文件。内核启动时会解析这个二进制文件,从而获取硬件配置信息。
设备树采用树状结构来描述硬件,由一系列节点(Node)和属性(Property)组成。根节点用斜杠 / 表示。
每个节点的定义格式通常为 [label:] node-name[ @unit-address]。其中 node-name 是节点名称,要求长度不超过 31 个字符。 @unit-address 是单元地址,通常用来表示设备的基地址,如果设备没有地址(如 CPU 节点),则可以省略。label 是节点的标签,后面可以通过 &label 的形式引用该节点,这在覆盖(Override)节点属性时非常有用。
属性是节点内部的键值对,用来描述设备的具体特征。属性值可以是空、32 位整数、字符串、字符串列表或字节数组。
在众多属性中,有几个标准属性尤为重要。compatible 属性是设备与驱动匹配的关键,它是一个字符串列表,通常格式为“厂商,型号”。内核在启动时,会拿驱动程序中的 compatible 值与设备树节点的 compatible 值进行比对,若一致则加载该驱动。**reg 属性用来描述设备的地址空间,通常包含基地址和长度。#address-cells 和#size-cells **属性决定了子节点 reg 属性中地址和长度字段分别占用的 32 位字数量。interrupts 属性描述设备使用的中断号和触发方式,而 interrupt-parent则指定了该设备连接的中断控制器。status 属性用于控制设备的启用状态,如果设置为“disabled”,内核将忽略该设备。
除了普通设备节点,设备树中还有一些特殊节点。aliases 节点用于定义别名,方便通过简短的名字访问节点。chosen 节点并不代表真实的硬件,而是用于 Bootloader 向内核传递运行时参数,例如启动参数 bootargs。memory 节点用来描述系统的物理内存布局,包括起始地址和大小。
设备树从编译到被内核使用,经历了一个完整的生命周期。
首先,开发者编写.dts 和.dtsi 文件,描述板卡的硬件信息。然后使用 DTC 工具将其编译为.dtb 二进制文件。
在系统启动阶段,Bootloader(如 U-Boot)会将 Linux 内核镜像(zImage 或 Image)和.dtb 文件加载到内存中。Bootloader 在跳转到内核执行前,会将存放.dtb 文件的内存首地址通过寄存器(ARM 架构通常是 r2 寄存器)传递给内核。
内核启动后,会从指定的内存地址读取设备树二进制数据。内核中的初始化代码会解析这些数据,将其展开为内核内部的 device_node 结构体树。随后,内核会遍历这棵树,将描述平台设备的节点转换为 platform_device 结构体,并注册到平台总线上。
最后,当开发者加载平台驱动(platform_driver)时,驱动程序中定义的 of_match_table 包含了一个 compatible 字符串列表。平台总线会拿着这个列表去匹配已注册的 platform_device。一旦匹配成功,内核就会调用驱动的 probe 函数,并将解析好的设备信息传递给驱动程序,完成驱动的初始化。
Linux 内核提供了一套以 of_(Open Firmware)开头的 API 函数,用于在驱动代码中获取设备树信息。这些函数声明在 include/linux/of.h 及其相关头文件中。
查找节点是第一步。of_find_node_by_path 可以根据路径(如“/soc/serial @1c28000”)查找节点。of_find_node_by_name 和 of_find_node_by_type 分别根据节点名和设备类型查找。更常用的是 of_find_compatible_node,它可以根据 compatible 属性查找指定类型的节点。
获取到节点指针(struct device_node *)后,就需要读取其属性。of_get_property 是一个通用函数,可以获取属性的值和长度。针对特定类型,内核提供了更便捷的接口:of_property_read_u32 用于读取单个 32 位整数,of_property_read_string 用于读取字符串。如果属性是数组,可以使用 of_property_read_u32_array 等函数。
此外,还有处理节点关系的函数。of_get_parent 可以获取父节点,of_get_child_by_name 可以根据名字查找子节点。在处理完节点后,如果不再需要,应当调用 of_node_put 减少引用计数(尽管在现代驱动模型中,很多时候框架已经帮我们处理好了)。
设备树不仅存在于源码和二进制文件中,系统启动后,内核会将解析后的设备树信息以文件系统的形式暴露给用户空间。这为调试提供了极大的便利。
在/sys/firmware/devicetree/base 目录下,我们可以看到与 DTS 文件结构对应的目录树。每个目录代表一个节点,目录下的文件代表属性。我们可以使用 cat 命令查看属性的内容。例如,查看某个节点的 compatible 属性,可以直接读取对应的文件。
另外,/sys/firmware/fdt 文件包含了原始的设备树二进制数据(DTB)。我们可以使用 hexdump 工具查看它,或者将其复制出来反编译为 DTS 文件,以确认内核实际加载的设备树内容是否符合预期。
下面通过一个具体的例子来展示如何在驱动中使用设备树。假设我们在开发板上连接了一个自定义的 LED 设备。
首先,在设备树文件(.dts)中添加一个节点:
/ {
my_led {
compatible = "mycorp,my-led";
reg = <0x12340000 0x100>;
label = "status-led";
status = "okay";
};
};
接下来,编写一个平台驱动来解析这个节点:
#include<linux module.h="">
#include<linux platform_device.h="">
#include<linux of.h="">
#include<linux of_device.h="">
// 匹配表
staticconststructof_device_idmy_led_of_match[] = {
{ .compatible = "mycorp,my-led" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
staticintmy_led_probe(struct platform_device *pdev)
{
structdevice_node *node = pdev->dev.of_node;
constchar *label;
u32 reg_val[2];
int ret;
printk(KERN_INFO "My LED driver probed!");
// 读取label属性
ret = of_property_read_string(node, "label", &label);
if (ret == 0) {
printk(KERN_INFO "LED Label: %s", label);
}
// 读取reg属性(假设#address-cells=1, #size-cells=1)
// 注意:在platform驱动中,通常直接使用platform_get_resource获取资源,
// 这里仅演示of_API的使用。
ret = of_property_read_u32_array(node, "reg", reg_val, 2);
if (ret == 0) {
printk(KERN_INFO "Reg address: 0x%x, size: 0x%x", reg_val[0], reg_val[1]);
}
return0;
}
staticintmy_led_remove(struct platform_device *pdev)
{
printk(KERN_INFO "My LED driver removed");
return0;
}
staticstructplatform_drivermy_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my-led-driver",
.of_match_table = my_led_of_match, // 绑定匹配表
},
};
module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Engineer");
MODULE_DESCRIPTION("A simple device tree example driver");
</linux></linux></linux></linux>
在这个示例中,当内核发现设备树中的 my_led 节点与驱动中的 my_led_of_match 表匹配时,会自动调用 probe 函数。
随着系统的复杂化,设备树技术也在不断演进。Device Tree Overlay(DTBO) 允许我们在系统运行时动态地修改设备树。这对于类似树莓派这样支持多种扩展板(HAT)的平台非常有用。通过加载不同的 Overlay 文件,可以在不重新编译内核和主设备树的情况下,动态启用或配置特定的硬件外设。
对于开发者来说,编写设备树时最大的困惑往往是属性该怎么写。Linux 内核源码中的 Documentation/devicetree/bindings 目录是唯一的权威指南。这里存放了所有支持设备的绑定文档,详细说明了每个 compatible 对应的节点应该包含哪些属性,哪些是必须的,哪些是可选的。在编写新的设备节点前,查阅这些文档是必不可少的步骤。

添加小助手 领取学习包

添加后回复 “单片机” 更快领取哦
