设备树(Device Tree)解析:dts 文件结构、绑定规则与硬件描述
设备树,说白了就是一套描述硬件的“身份证”。
我刚开始接触 Linux 驱动时,最头疼的就是看那些 .dts 文件。密密麻麻的层级,一堆看不懂的 compatible 字符串。后来被一个 bug 折磨了三天,才发现是设备树里一个中断号写错了。嗯,从那以后,我再也不敢小看这个“树”了。
1. 为什么需要设备树?
在 ARM Linux 早期,所有硬件信息都硬编码在 board-xxx.c 文件里。换一个板子就要重新编译内核,维护成本极高。设备树的出现,把硬件描述从内核源码中剥离出来,变成一份独立的数据结构。
你想想看,内核启动时只需要解析这个树形结构,就能知道:
- CPU 是什么型号
- 内存有多大
- 外设挂在哪里
- 中断怎么路由
- 时钟频率多少
说白了,设备树就是硬件和驱动之间的“翻译官”。
2. dts 文件结构
一个典型的 .dts 文件长这样:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
reg = <0x0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
uart0: serial@44e09000 {
compatible = "ti,am3352-uart", "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "okay";
};
};
我来拆解一下这个结构:
- 根节点:
/是整个树的起点,包含 model 和 compatible 属性 - 节点名称:格式是
node-name@unit-address,比如uart0: serial@44e09000 - 属性:键值对,描述硬件特性
- reg:地址和长度信息,由
#address-cells和#size-cells决定
find /sys/firmware/devicetree/ 查找非常方便。
3. 绑定规则:compatible 的玄机
compatible 是设备树里最重要的属性。它决定了驱动和设备如何“配对”。
格式是 "厂商,型号",比如 "ti,am3352-uart"。内核驱动通过 of_match_table 来匹配这个字符串。
我遇到过一个问题:某款传感器芯片,厂商给的 dts 里 compatible 写的是 "bosch,bmp280",但内核驱动里匹配的是 "bmp280"。结果驱动死活加载不上。查了两天才发现是字符串不匹配。
of_device_id 表完全一致,大小写、逗号、空格都不能错。我曾经因为多了一个空格,浪费了一整天。
4. 硬件描述的核心属性
除了 compatible,还有几个属性是驱动开发必须掌握的:
| 属性 | 作用 | 示例 |
|---|---|---|
reg |
描述地址空间 | reg = <0x44e09000 0x2000> |
interrupts |
中断号 | interrupts = <0 72 4> |
clocks |
时钟源 | clocks = <&clk_uart0> |
gpios |
GPIO 引脚 | gpios = <&gpio1 15 0> |
status |
设备状态 | status = "okay" 或 "disabled" |
这里要注意 interrupts 的格式。它通常包含三个字段:中断控制器类型、中断号、触发方式。不同架构的编码方式不一样,ARM GIC 和 RISC-V PLIC 就有差异。
5. 设备树的核心逻辑:一张图看懂
下面这张图是我自己总结的设备树解析流程,每次写驱动前我都会过一遍:
6. 实战:如何编写一个简单的设备树节点
假设我们要添加一个 I2C 温度传感器,挂在 I2C0 总线上,地址是 0x48。我会这样写:
&i2c0 {
status = "okay";
clock-frequency = <100000>;
temp_sensor: temperature@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupts = <14>;
interrupt-parent = <&gpio1>;
};
};
这里有几个要点:
&i2c0是引用已有的 I2C 控制器节点temperature@48是子节点名称,@后面跟设备地址interrupt-parent指定中断控制器,这个很容易漏掉
7. 避坑指南:我踩过的几个坑
- reg 的 cells 数量搞错:
#address-cells和#size-cells决定了 reg 的解析方式。ARM 平台通常是 1 个地址 + 1 个长度,但有些总线是 2 个地址 + 1 个长度。写错了驱动就读不到正确的地址。 - 中断号偏移:不同中断控制器对中断号的编码不一样。GIC 的 SPI 中断号从 32 开始,但有些 dts 里写的是硬件原始号。我遇到过驱动里中断号对不上,导致中断无法触发的 case。
- status 默认是 okay:有些 dts 里没写 status,默认就是 enabled。但如果你引用了某个节点,又在另一个文件里把它 disable 了,就会产生冲突。我建议显式写出 status。
8. 调试技巧
设备树写对了没有?我一般用这几个方法验证:
- dtc 编译检查:
dtc -I dts -O dtb -o test.dtb test.dts,看有没有语法错误 - 内核启动日志:dmesg 里搜
OF: fdt,能看到设备树解析过程 - sysfs 查看:
ls /sys/firmware/devicetree/base/,看节点是否生成 - 驱动 probe 日志:在驱动里加
dev_info,看 probe 是否被调用
设备树这东西,说难不难,说简单也不简单。关键是要理解它的设计思想:把硬件描述和驱动代码解耦。你只要掌握了节点、属性、compatible 匹配这三个核心概念,剩下的就是查手册、对着写。
嗯,这一章的内容就到这里。记住,写设备树时多想想:如果我是内核,看到这个节点,能不能找到对应的驱动?能不能正确初始化硬件?想通了这两个问题,你就出师了。