2. 基础知识:基本概念、常见术语、应用场景
Device Tree(设备树)是一种描述硬件资源的数据结构,它通过一种与操作系统无关、与具体硬件平台解耦的文本形式,将板级硬件细节从内核代码中剥离出来。在Linux内核引入Device Tree之前,ARM平台的内核中充斥着大量描述板级硬件细节的C语言代码(如 arch/arm/mach-xxx 下的 board-xxx.c 文件),这些代码导致内核“一人一版”,难以维护和统一。Device Tree的出现,使得同一份内核镜像可以支持多个不同的硬件平台,只需在启动时加载不同的设备树二进制文件(DTB)即可。本章将围绕Device Tree的核心概念、关键术语以及典型应用场景展开,为后续的编写实战打下坚实基础。
上图展示了Device Tree从源文件到内核设备模型建立的完整工程链路。理解这条链路,是掌握Device Tree编写与调试的基础。下面我们将从核心原理、术语解析、应用场景和常见误区四个维度展开详细讲解。
2.1 核心原理:从硬件描述到设备模型
Device Tree的核心原理可以概括为“三段式”工作流:描述(Describe)→ 编译(Compile)→ 解析(Parse)。首先,硬件工程师或BSP开发者使用DTS(Device Tree Source)文件描述板级硬件资源,包括CPU型号、内存基地址与大小、外设控制器(如UART、I2C、SPI、GPIO、DMA、中断控制器等)的寄存器地址、中断号、时钟频率、引脚复用关系等。这些描述以树形结构组织,根节点 / 下挂载若干子节点,每个子节点代表一个硬件设备或总线。
其次,DTS文件通过DTC(Device Tree Compiler)工具编译为二进制的DTB(Device Tree Blob)文件。DTB是内核启动时实际加载的数据格式,它紧凑且易于解析。编译过程还会生成一个DTS的“展开”版本(*.dts 经过预处理和包含展开),便于调试时查看最终生效的配置。
最后,在系统启动阶段,Bootloader(如U-Boot)会将DTB加载到内存中,并将物理地址传递给内核。内核在启动早期通过 unflatten_device_tree() 函数将DTB解析为内存中的 device_node 结构体树,随后通过 of_platform_populate() 遍历这棵树,为每个带有 compatible 属性的节点创建对应的 platform_device。这些设备随后会与内核中的驱动(通过 of_match_table 匹配)进行绑定,触发驱动的 probe() 函数,完成设备的初始化。
关键理解:Device Tree不是“配置寄存器”的脚本,而是“描述硬件拓扑”的元数据。它告诉内核“有什么设备、在哪里、怎么访问”,而具体的寄存器操作逻辑仍然由驱动代码完成。这种分离使得同一驱动可以支持多个不同板卡,只需更换DTB即可。
2.2 常见术语与语法要素
掌握Device Tree的术语是阅读和编写DTS文件的前提。下表列出了最核心的术语及其含义:
| 术语 | 全称/含义 | 示例 | 说明 |
|---|---|---|---|
| Node | 节点 | uart0: serial@10000000 |
表示一个硬件设备或总线,节点名格式为 [label:] name@unit-address |
| Property | 属性 | compatible = "ns16550a"; |
描述节点的特性,键值对形式,值可以是字符串、整数、数组等 |
| compatible | 兼容性字符串 | "ti,omap3-uart" |
用于驱动匹配,格式为 "厂商,型号",按优先级排列 |
| reg | 寄存器地址/长度 | reg = <0x10000000 0x1000>; |
描述设备在总线上的地址范围,由 #address-cells 和 #size-cells 决定 |
| interrupts | 中断描述 | interrupts = <0 29 4>; |
包含中断控制器phandle、中断号、触发类型等 |
| phandle | 节点句柄 | interrupt-parent = <&intc>; |
在DTS中通过 &label 引用节点,编译后成为唯一的32位句柄 |
| aliases | 别名节点 | aliases { serial0 = &uart0; }; |
为节点指定短别名,方便Bootloader或内核引用 |
| chosen | 启动参数节点 | bootargs = "console=ttyS0,115200"; |
传递内核启动参数、stdout-path等 |
| DTC | Device Tree Compiler | dtc -I dts -O dtb -o xxx.dtb xxx.dts |
将DTS编译为DTB,也可反编译 |
| DTSI | Device Tree Source Include | #include "soc.dtsi" |
公共的SoC级描述文件,通过 #include 包含 |
在语法层面,DTS文件使用类似C语言的预处理指令(#include、#define),支持宏定义和头文件包含。节点通过花括号 { } 定义,属性以分号结尾。地址和大小由父节点的 #address-cells 和 #size-cells 属性决定,通常为1(32位)或2(64位)。中断描述则依赖中断控制器节点的 #interrupt-cells 属性。
2.3 应用场景:为什么需要Device Tree
Device Tree的应用场景非常广泛,几乎覆盖了所有基于Linux的嵌入式系统。以下是几个典型的应用场景:
- 多板卡支持:同一款SoC(如i.MX8M Plus)可能被用在几十种不同的开发板上,这些板卡的外设配置(如DDR容量、网卡型号、LCD分辨率)各不相同。通过为每块板卡提供一个独立的DTS文件,内核无需重新编译即可支持所有板卡。
- Bootloader与内核解耦:U-Boot负责加载DTB并传递给内核,内核不再需要从Bootloader获取ATAGs(旧式启动参数)来了解硬件布局。这使得U-Boot和内核可以独立升级,只要DTB匹配即可。
- 外设配置动态化:某些设备(如FPGA)可以在运行时通过加载不同的DTB overlay(设备树覆盖层)来改变硬件功能。例如,在Zynq平台上,可以通过
configfs动态加载FPGA bitstream和对应的DTBO,实现外设的即插即用。 - 减少内核代码冗余:在Device Tree之前,ARM Linux内核中充斥着
machine_desc结构体和board_xxx.c文件。现在这些板级信息全部移入DTS,内核代码更加干净,社区维护成本大幅降低。 - 驱动开发与验证:驱动开发者可以通过修改DTS中的
compatible字符串或reg属性,快速验证驱动对不同硬件版本的兼容性,而无需修改C代码。
经验提醒:在项目早期,建议先梳理硬件原理图,列出所有外设的寄存器地址、中断号、GPIO引脚和时钟关系,再动手编写DTS。一个良好的DTS设计应当遵循“SoC级DTSI + 板级DTS”的分层结构,避免在板级文件中重复定义SoC级内容。
2.4 案例拆解:一个简化的UART节点
为了将上述概念串联起来,我们以一个典型的UART控制器为例,展示DTS中如何描述一个实际设备。假设某SoC的UART0控制器寄存器基地址为 0x10000000,地址范围为 0x1000 字节,中断号为 29(SPI类型,高电平触发),时钟频率为 24MHz。
// 在SoC级DTSI文件中定义
/ {
compatible = "acme,coyote-soc";
#address-cells = <1>;
#size-cells = <1>;
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
intc: interrupt-controller@10000000 {
compatible = "acme,coyote-intc";
reg = <0x10000000 0x1000>;
interrupt-controller;
#interrupt-cells = <3>;
};
uart0: serial@10001000 {
compatible = "acme,coyote-uart", "ns16550a";
reg = <0x10001000 0x1000>;
interrupts = <0 29 4>; // 0=SPI, 29=IRQ号, 4=高电平触发
interrupt-parent = <&intc>;
clocks = <&uart_clk 0>;
clock-names = "uartclk";
status = "okay";
};
};
};
// 在板级DTS文件中使能并配置
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
current-speed = <115200>;
};
在这个例子中,compatible 属性提供了两个字符串:第一个是SoC特定的 "acme,coyote-uart",第二个是通用的 "ns16550a"。内核驱动会优先匹配第一个,如果找不到再尝试第二个。这种“从特定到通用”的匹配策略,保证了驱动代码的复用性。reg 属性中的两个值分别对应 #address-cells 和 #size-cells 为1时的地址和长度。interrupts 属性中的三个值由 #interrupt-cells = <3> 决定,分别表示中断类型(0=SPI,1=PPI)、中断号和触发标志。
在板级DTS中,我们通过 &uart0 引用节点,并添加了引脚控制(pinctrl)和波特率配置。这种“基板定义 + 板级覆盖”的模式,是Device Tree最强大的特性之一——它允许在不修改SoC级文件的情况下,为不同板卡定制外设配置。
风险提示:在编写DTS时,务必注意 reg 属性中的地址值必须与硬件原理图完全一致,且不能与其他设备重叠。地址冲突会导致内核启动时设备注册失败,甚至系统崩溃。此外,interrupts 属性中的中断号必须与中断控制器(GIC)的硬件连接一致,错误的中断号会导致设备无法正常工作或产生中断风暴。
2.5 常见误区与总结复盘
初学者在接触Device Tree时,常会陷入以下几个误区,需要特别警惕:
- 误区一:认为DTS是“万能配置”。DTS只能描述硬件资源的存在和位置,不能描述硬件的工作逻辑。例如,你不能在DTS中指定“UART在发送数据前先拉高某个GPIO”,这属于驱动代码的职责。
- 误区二:忽略
status属性。默认情况下,如果节点没有status属性,内核会认为设备是启用的。但很多SoC的DTSI中会将暂时不用的外设设为status = "disabled",板级DTS中需要显式改为"okay"才能使用。 - 误区三:混淆
#address-cells和#size-cells的作用域。这两个属性是“继承”的,子节点会继承父节点的设置。如果父节点设置为2(64位地址),子节点的reg属性中地址部分就必须写两个32位整数。 - 误区四:不检查DTB的编译警告。DTC编译器会输出很多有用的警告,如
unit_address_format、node_name_chars等。这些警告往往暗示着潜在的兼容性问题,应当全部消除。