设备树基础:设备树的起源与目的、DTS/DTC/DTB文件格式、设备树的基本语法与节点
做嵌入式开发的朋友,一定都跟设备树打过交道。我记得刚接触Linux内核移植那会儿,看到.dts文件里密密麻麻的节点,心里直犯嘀咕——这玩意儿到底是干嘛的?
说白了,设备树就是一套描述硬件信息的“数据手册”。它把CPU、内存、外设、中断、GPIO这些硬件资源,用一种树形结构组织起来。内核启动时读这份数据,就知道自己跑在什么板子上。
2.1 设备树的起源与目的
在设备树出现之前,Linux内核是怎么知道硬件信息的?答案是——硬编码。每个板子都在arch/arm/mach-xxx目录下有一堆board-xxx.c文件,里面写死了寄存器地址、中断号、时钟频率。
这种做法有什么问题?我举个例子。当年TI的OMAP系列芯片,一个芯片能用在几十种板子上。每个板子改一点外设,就得新建一个board文件。内核社区维护这些文件,简直要疯掉。
2005年左右,PowerPC架构率先引入了设备树(Device Tree)的概念。后来ARM社区也跟进,到了Linux 3.x时代,设备树成了ARM平台的标准配置。
设备树的核心目的就三个:
- 硬件描述与内核代码分离——换板子不用改内核,改.dts文件就行
- 减少板级文件数量——一个.dts对应一个板子,清晰明了
- 统一硬件描述接口——不管ARM、RISC-V还是x86,都用同一套语法
核心观点:设备树不是Linux内核的一部分,它是内核的“输入数据”。内核启动时读取设备树,根据里面的描述来初始化硬件。这个设计思路,跟BIOS里的ACPI表很像。
2.2 DTS / DTC / DTB 文件格式
这三个缩写,你得记牢。我见过不少工程师把它们搞混,调试时走了弯路。
| 缩写 | 全称 | 说明 | 文件后缀 |
|---|---|---|---|
| DTS | Device Tree Source | 设备树源文件,文本格式,工程师直接编写 | .dts / .dtsi |
| DTC | Device Tree Compiler | 设备树编译器,把DTS编译成DTB | 可执行程序 |
| DTB | Device Tree Blob | 设备树二进制文件,内核/U-Boot实际加载的格式 | .dtb |
工作流程很简单:你写.dts文件,然后用dtc工具编译成.dtb。U-Boot或内核启动时,把.dtb加载到内存里解析。
这里有个细节要注意——.dtsi文件。它相当于C语言里的头文件,用来放芯片级通用的硬件描述。比如一个SoC的CPU核心、内部中断控制器、时钟控制器,这些写在.dtsi里。板级差异(比如用哪颗Flash、GPIO怎么分配)写在.dts里。
个人习惯:我一般把.dtsi放在arch/arm/boot/dts/目录下,命名用芯片型号,比如imx6ull.dtsi。板级.dts用板子型号,比如myboard-imx6ull.dts。这样找文件一目了然。
2.3 设备树的基本语法与节点
设备树的语法,其实不复杂。它用树形结构描述硬件,根节点是“/”,下面挂各种子节点。每个节点代表一个硬件设备或总线。
来看一个最简单的例子:
/dts-v1/;
/ {
model = "MyBoard i.MX6ULL";
compatible = "mycompany,myboard-imx6ull", "fsl,imx6ull";
memory {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
chosen {
stdout-path = &uart1;
};
uart1: serial@02020000 {
compatible = "fsl,imx6ull-uart", "fsl,imx-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 4>;
clocks = <&clks IMX6ULL_CLK_UART1>;
status = "okay";
};
};
咱们拆开来看:
- /dts-v1/; —— 版本声明,告诉编译器用v1语法
- / { }; —— 根节点,所有设备都挂在这里
- model —— 板子名称,字符串
- compatible —— 兼容性字符串,内核靠它匹配驱动
- memory节点 —— 描述内存,reg属性给出起始地址和大小
- chosen节点 —— 启动参数,比如控制台用哪个串口
- uart1节点 —— 串口外设,包含地址、中断、时钟等信息
节点名后面可以跟标签(label),比如uart1:。这个标签可以在其他地方引用,用&uart1就行。我在项目中经常用这个特性来覆盖节点属性,比如在板级.dts里修改某个外设的status。
避坑指南:我曾经在一个项目里,把reg属性的地址写错了。0x02020000写成了0x2020000,少了一个0。结果串口驱动死活不工作,调试了两天才发现。记住——设备树里的地址都是完整的32位或64位值,不要省略前导0。
2.4 设备树的核心属性
除了上面看到的compatible、reg、interrupts,还有几个属性你一定会用到:
| 属性名 | 含义 | 示例 |
|---|---|---|
| compatible | 驱动匹配字符串,内核靠它找驱动 | "fsl,imx6ull-uart" |
| reg | 寄存器地址和长度 | <0x02020000 0x4000> |
| interrupts | 中断号、触发类型 | <0 26 4> |
| clocks | 时钟源引用 | <&clks IMX6ULL_CLK_UART1> |
| status | 设备状态,"okay"或"disabled" | "okay" |
| pinctrl-* | 引脚复用配置 | pinctrl-0 = <&pinctrl_uart1> |
compatible属性特别重要。内核驱动通过of_match_table来匹配这个字符串。匹配规则是从左到右,越靠前的越精确。比如"mycompany,myboard-imx6ull"是板级匹配,"fsl,imx6ull"是芯片级匹配。
你想想看,如果驱动里只写了"fsl,imx6ull-uart",而设备树里写的是"mycompany,my-uart",那驱动就绑不上。这个问题我帮同事排查过好几次。
2.5 设备树编译与反编译
写好了.dts文件,怎么变成.dtb?用dtc工具:
# 编译
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译(调试时很有用)
dtc -I dtb -O dts -o myboard.dts myboard.dtb
# 查看dtb内容
fdtdump myboard.dtb
反编译这个操作,我经常用。有时候从板子上拿到的.dtb文件,不知道里面写了什么。反编译成.dts一看,所有属性一目了然。特别是排查“为什么驱动没加载”这类问题时,先反编译dtb确认硬件描述对不对,能省很多时间。
调试技巧:U-Boot里可以用fdt list命令查看设备树内容。比如fdt list /显示根节点,fdt list /uart1显示串口节点。这个命令在定位设备树加载问题时特别好用。
2.6 设备树的知识结构
为了让你更直观地理解设备树的全貌,我画了一张结构图:
这张图把设备树的知识体系串起来了。从上到下,从文件格式到语法,再到核心属性和调试方法。你学完这一章,应该能回答三个问题:设备树是什么?怎么写?怎么用?
嗯,基础打牢了,后面讲设备树与U-Boot的交互、驱动匹配流程,你才能跟得上。