4、设备树基础概念:Device Tree的起源与作用、DTS/DTC/DTB文件格式、节点与属性的语法规则
4.1 为什么要有设备树?—— 一段“野蛮生长”的历史
在讲设备树之前,我想先聊聊没有设备树的日子。我入行那会儿,Linux 内核里全是板级文件。每个开发板一个 board-xxx.c,里面密密麻麻地注册各种硬件资源:中断号、GPIO、内存地址、时钟频率……全写死在 C 代码里。
这有什么问题?问题大了。你想想看,一个 SoC 可能对应几十种开发板,每个板子外设稍有不同,就得改内核源码重新编译。内核社区维护这些板级文件,简直像在泥潭里走路。我记得有一次,为了给一块新板子移植内核,我光改 board-xxx.c 就花了两天,还因为一个 GPIO 号写错,点不亮 LCD,排查了整整一个下午。
所以,设备树(Device Tree)应运而生。说白了,就是把硬件描述从内核源码里“剥离”出来,变成一份独立的数据结构。内核启动时解析这份数据,动态构建设备模型。从此,换板子不再需要改内核,改一份 .dts 文件就行。
4.2 设备树的“三兄弟”:DTS、DTC、DTB
这三个缩写你肯定见过,但它们的区别和关系,我建议你一次理清。
| 名称 | 全称 | 本质 | 谁在用 |
|---|---|---|---|
| DTS | Device Tree Source | 文本文件,人类可读 | 开发者编写 |
| DTC | Device Tree Compiler | 编译器,将 DTS 编译为 DTB | 构建系统调用 |
| DTB | Device Tree Blob | 二进制文件,机器可读 | Bootloader 传递给内核 |
流程很简单:你写 .dts 文件,然后用 dtc 工具编译成 .dtb 文件。Bootloader(比如 U-Boot)在启动内核时,把 .dtb 的地址通过寄存器传给内核。内核解析 DTB,构建设备模型。
嗯,这里要注意:.dtsi 文件是“头文件”或“包含文件”,通常放 SoC 公共部分。比如 imx6ull.dtsi 定义 i.MX6ULL 芯片内部所有外设,而具体的板子文件 myboard.dts 通过 #include 引用它,再添加板级差异。
.dtsi,板级差异放在 .dts。这样复用性最好,也方便后续升级内核版本。
4.3 节点与属性:设备树的“骨架”和“血肉”
设备树的结构,本质上是一棵倒置的树。根节点是 /,下面挂 CPU、内存、外设总线等。每个节点代表一个设备或总线,节点里用属性描述设备的细节。
我直接给你看一个典型的设备树片段,比讲一百句都管用:
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "My i.MX6ULL Board";
compatible = "mycompany,imx6ull-board", "fsl,imx6ull";
chosen {
stdout-path = &uart1;
};
memory {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB RAM
};
leds {
compatible = "gpio-leds";
led0 {
label = "heartbeat";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
linux,default-trigger = "heartbeat";
};
};
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};
};
我们来拆解一下这个例子里的关键概念。
4.3.1 节点(Node)
- 根节点:
/,整棵树的起点。 - 子节点:
chosen、memory、leds、uart1都是根节点的子节点。 - 节点名:比如
led0,通常格式是名称@地址,当地址唯一时可以不写。 - 节点路径:从根到节点的完整路径,比如
/leds/led0。
4.3.2 属性(Property)
- compatible:最重要的属性。内核通过它匹配驱动。格式是
"厂商,设备型号"。比如"mycompany,imx6ull-board"。内核驱动里会声明一个of_match_table,设备树里的 compatible 和驱动里的匹配上,驱动才会 probe。 - reg:描述地址空间。格式是
<起始地址 长度>。比如内存的reg = <0x80000000 0x20000000>表示从 0x80000000 开始,512MB 空间。 - status:设备状态。常用值:
"okay"(启用)、"disabled"(禁用)。 - gpios:描述 GPIO 引脚。格式是
<&gpio控制器 引脚号 标志>。 - pinctrl-*:引脚复用配置。通过
pinctrl-names和pinctrl-0引用 pinmux 节点。
of_match_table 严格一致。
4.4 设备树的核心语法规则
我整理了一份速查表,你写 DTS 时可以对照着看:
| 语法元素 | 规则 | 示例 |
|---|---|---|
| 节点定义 | 节点名 { ... }; |
uart1 { ... }; |
| 属性赋值 | 属性名 = 值; |
status = "okay"; |
| 字符串 | 双引号包裹 | "okay" |
| 32位整数 | <数值> |
<0x80000000> |
| 64位整数 | 两个32位整数组合 | <0x00000000 0x80000000> |
| 字节数组 | [十六进制字节] |
[00 01 02] |
| 引用 | &节点标签 |
&uart1 |
| 包含文件 | #include "文件" |
#include "imx6ull.dtsi" |
4.5 设备树的核心结构图
下面这张图,我建议你多看几遍。它把设备树的层级关系、节点属性、以及和内核驱动的匹配逻辑都串起来了。
4.6 避坑指南:我见过的设备树“翻车”现场
做设备树配置,有几个地方特别容易出错。我把自己踩过的坑和帮别人排查的案例整理一下:
- reg 属性地址写错:我曾经把内存的起始地址写成了
0x80000000,但实际硬件是0x60000000。内核启动到一半就崩了。排查方法:用dtc -I dtb -O dts反编译 DTB,核对地址值。 - 中断号不对应:设备树里的中断号是硬件中断号,不是 Linux IRQ 号。我见过有人直接把
cat /proc/interrupts看到的号写进设备树,结果完全不工作。记住:设备树里写的是 硬件中断号,内核会做映射。 - pinctrl 配置冲突:两个设备共用了同一个引脚,但都设成了
status = "okay"。结果一个设备工作正常,另一个完全没反应。排查时用cat /sys/kernel/debug/gpio看引脚状态,才发现冲突。 - 忘记加
#include或路径不对:编译 DTS 时,如果引用了.dtsi但路径没写对,DTC 会报错。我建议在 Makefile 里用DTC_FLAGS指定搜索路径,避免这种低级错误。
1. 反编译 DTB:
dtc -I dtb -O dts -o dump.dts boot.dtb,看实际传给内核的数据对不对。2. 查看设备树在 /sys 下的映射:
ls /sys/firmware/devicetree/base/,看节点是否被正确解析。3. 查看驱动是否 probe:
cat /sys/kernel/debug/devices_deferred,看哪些设备被延迟 probe 了。
4.7 小结
设备树是嵌入式 Linux 驱动开发的基石。你不需要背下所有语法,但一定要理解它的设计思想:硬件描述与代码分离。DTS 是源码,DTC 是编译器,DTB 是内核吃进去的二进制数据。节点描述设备,属性描述细节,compatible 是驱动匹配的钥匙。