引言
在嵌入式 Linux 开发中,硬件资源的描述至关重要。驱动代码负责操作设备,而设备结构体(struct device)则描述硬件资源。问题在于,这些硬件信息如何进入内核并与驱动配对?
在单片机裸机编程中,硬件地址硬编码在代码里。但在 Linux 中,为了保证驱动的通用性,避免在代码中出现具体硬件地址是核心原则。因此,需要一种机制将硬件信息从驱动代码中剥离。
这就是本文的主题——设备树。
1. 为什么要引入设备树
1.1 当时的环境
2000 年代末期,ARM 架构迎来井喷式发展。相比 x86 架构的标准化,ARM 呈现碎片化状态。各家 SOC 厂商(高通、三星、TI 等)推出不同芯片,同一款 SOC 也可能有不同的外设配置。Linux 想统一支持所有设备,但当时 ARM 设备基本靠硬编码告诉内核硬件信息。
这导致 Linux 内核 arch/arm 目录下代码量急速膨胀。支持新板子需添加专属文件。
1.2 board file 的缺陷
早期 ARM Linux 采用 board file(板级支持文件模式),即每个设备板子都有独立的 board-xxx.c 文件。文件中硬编码了内存映射、时钟配置、GPIO 引脚复用等信息。
这种模式在 2010 年左右暴露出问题:
- 内核里有数千个 board file,arch/arm 目录下文件数量激增,大量重复代码。
- 修改通用驱动需同步改几十个 board file,合并冲突频繁。
- 一个编译好的内核只能支持特定板子,无法像 x86 那样一个内核跑所有 PC。
- 厂商维护分支,上游合并慢,主线内核支持新硬件滞后。
Linus Torvalds 曾批评 ARM 社区的碎片化,要求推动根本性变革。
1.3 设备树的引入
社区最终借鉴 PowerPC 架构的 Open Firmware 机制。核心想法是把硬件描述从 C 源码中剥离出来,用独立的数据文件描述硬件,由 bootloader 传入内核,让内核在运行时动态解析。
使用 .dts(Device Tree Source)文件描述硬件树状结构,编译成 .dtb(Device Tree Blob)文件传给内核。
好处包括:删除重复代码,一个内核镜像支持多种硬件,易读性和可维护性提升,便于上游合并。
2. 初识设备树
设备树是由节点和属性构成的树。
2.1 三个 D
- DTS:源码文件 .dts,修改硬件配置主要修改它。
- DTC:编译器,把 .dts 编译成二进制文件。
- DTB:编译出来的二进制文件 .dtb,烧录到 flash 或放在文件系统。Bootloader 加载后传给内核。
2.2 类似于头文件包含的关系
Linux 采用分层和包含机制。以 NXP i.MX6ULL 芯片为例。
2.2.1 SOC 级文件 (.dtsi)
由芯片原厂提供,如 imx6ull.dtsi,描述芯片内部固有的硬件资源。物理地址和中断号不会变。
文件体现继承与重写策略。例如引用父类标签只修改需要的部分,或删除不存在的节点。
2.2.2 板级文件 (.dts)
开发板厂商编写描述文件,如 imx6ull-14x14-evk.dts。文件开头引用 SOC 级文件,开发者只需关注板子上的外设配置。
通常在最外层使用 &标签名方式引用节点开启默认关闭的外设,或在根节点内部添加外接新设备。
2.3 如何与驱动匹配
在没有设备树时,platform_device 名字必须和 platform_driver 名字一样。
在设备树世界里,匹配规则主角变成了 compatible 属性。只要设备树节点和驱动程序的 compatible 属性相同即可匹配。
compatible 属性通常是字符串列表,格式为'厂商,芯片型号 - 模块名'。
驱动程序中会定义相应的 compatible 属性数组,Platform 总线据此匹配。
3. 内核如何处理设备树
内核在启动过程中充当中间人,把设备树从文本变成结构体。
3.1 从 flash 到 RAM
Bootloader(U-Boot)把内核镜像和 .dtb 文件加载到内存。启动内核时,把 .dtb 文件的内存首地址通过寄存器传给内核。
3.2 展开
内存里的 .dtb 是二进制数据。内核在 setup_arch 阶段执行 unflatten_device_tree 函数,解析二进制数据,把它们展开成内核里的结构体链表。
为设备树中的每一个节点创建一个 struct device_node 结构体。
3.3 生成设备
内核调用核心函数 of_platform_populate(),遍历 device_node 树,把看起来像是设备的节点转换成 Linux 驱动模型中的 struct platform_device。
只有挂载在根节点或 simple-bus 下的子节点才会被转换。
内核解析 reg 属性转换成 IORESOURCE_MEM,interrupts 属性转换成 IORESOURCE_IRQ,最后调用 platform_device_register 注册到总线上。
4. 驱动怎么读取设备树
在 probe 函数里,通常需要获取硬件资源和设备树里面的一些自定义配置。Linux 内核提供了一套以 of_开头的函数。
4.1 获取标准资源
reg 和 interrupts 已由 of_platform_populate 自动转换。直接用 Platform 子系统的标准接口。
// 在驱动的 probe 函数中
static int my_driver_probe(struct platform_device *pdev)
{
struct resource *res;
int irq;
// 获取寄存器地址,第三个参数 0 表示第 0 组 reg
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENOMEM;
// 拿到物理地址后,要把它映射成虚拟地址才能用
void __iomem *base_addr = devm_ioremap_resource(&pdev->dev, res);
// 获取中断号,参数 0 表示第 0 个中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
//......
}
获取寄存器地址函数原型:
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);
获取中断号函数原型:
int platform_get_irq(struct platform_device *dev, unsigned int num);
4.2 获取自定义属性
对于自定义属性,内核没有自动转换,需要手动查表,函数都在 <linux/of.h> 中。
4.2.1 读取数字
struct device_node *np = pdev->dev.of_node;
u32 delay_time;
if (of_property_read_u32(np, "my-delay-ms", &delay_time) == 0) {
printk("get delay time: %d\n", delay_time);
} else {
printk("未找到该属性\n");
}
4.2.2 读取字符串
const char *str;
of_property_read_string(np, "my-name", &str);
4.2.3 读取数组
u32 data_array[3];
of_property_read_u32_array(np, "my-data", data_array, 3);
4.3 两个特殊的 API
4.3.1 获取 GPIO
现代内核推荐使用更高级的 GPIO Descriptor (gpiod) 接口。
struct gpio_desc *rst_gpio;
rst_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_LOW);
4.3.2 查找节点
struct device_node *target_node;
target_node = of_find_node_by_path("/backlight/lcd-backlight");
if (target_node) {
// 找到后,就可以用 of_property_read_xxx 去读它的属性了
}
5. 在系统运行时查看设备树
Linux 内核通过 procfs 文件系统展现解析好的设备树结构。
5.1 设备树在哪
设备树在文件系统中的实体位于 /proc/device-tree。实际上这是一个软链接,指向/sys/firmware/devicetree/base。
可以使用 tree 命令查看设备树层级。
5.2 如何验证修改的设备树是否生效
在 DTS 中添加节点后,可在 /proc/device-tree 下找到该节点。注意 name 属性即使未定义也会自动存在。
对于数值型属性(reg, interrupts, gpios 等),需用 hexdump 命令按十六进制打印,因为 cat 命令可能因空字符显示异常。
5.3 验证设备是否生成
在 /sys/bus/platform/devices/目录下执行 ls 命令,能看到熟悉的设备名。这说明设备树不仅解析成功,而且已经成功注册为板载设备。
6. 总结
设备树的引入彻底结束了 ARM 社区代码脏乱差的时代。对于驱动开发者来说,它让代码变得更加纯粹——驱动只管逻辑,硬件交给设备树。


