设备树基础:设备树的起源与目的、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 设备树的知识结构

为了让你更直观地理解设备树的全貌,我画了一张结构图:

设备树知识结构图 根节点 / 文件格式 DTS → DTC → DTB .dtsi 头文件复用 基本语法 节点 / 属性 / 值 标签 & 引用覆盖 核心属性 compatible / reg interrupts / clocks 编译 / 反编译 / 调试 U-Boot / 内核 加载与解析 设备树是硬件描述数据,不是驱动代码。它告诉内核“有什么”,不告诉内核“怎么用”。

这张图把设备树的知识体系串起来了。从上到下,从文件格式到语法,再到核心属性和调试方法。你学完这一章,应该能回答三个问题:设备树是什么?怎么写?怎么用?

嗯,基础打牢了,后面讲设备树与U-Boot的交互、驱动匹配流程,你才能跟得上。