1. 设备树基础回顾:设备树起源、语法结构、节点与属性、dts/dtsi/dtb文件关系
1.1 设备树的起源——为什么要有它?
说实话,我刚入行那会儿,Linux内核里还没有设备树这东西。那时候板级文件满天飞,每个ARM平台都有自己的board-xxx.c,里面密密麻麻全是硬件寄存器地址、中断号、GPIO映射。你想想看,换一个Flash型号,就得改C代码、重新编译内核。这哪是写驱动,简直是给硬件做硬编码。
设备树(Device Tree)的出现,说白了就是要把硬件描述从内核代码里剥离出来。它最早源于PowerPC架构,后来被ARM社区大力推广。我记得2011年左右,ARM Linux主线开始强制要求使用设备树,那会儿不少老工程师都在抱怨——但回过头看,这步棋走得太对了。
核心思想:用一套独立的数据结构来描述硬件,内核在启动时解析它,动态生成平台设备。硬件变了,改dts文件就行,不用动内核代码。
1.2 语法结构——树是怎么长出来的?
设备树的语法,其实特别直观。它就是一个倒挂的树,根节点是/,下面挂各种总线、外设。每个节点代表一个硬件设备或总线,节点里放属性来描述它的特征。
我习惯把设备树想象成一张「硬件清单」:
- 节点(node):就是清单上的一个条目,比如「UART0」、「I2C控制器」
- 属性(property):条目的具体内容,比如地址0x10000000、中断号33
- 值(value):属性的具体数值,可以是32位整数、字符串、字节数组
来看一个最简单的例子:
/dts-v1/;
/ {
model = "MyBoard V1.0";
compatible = "vendor,myboard";
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
reg = <0>;
};
uart@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
嗯,这里要注意:@后面的数字是地址,用来区分同类型设备。比如两个UART,可以写成uart@10000000和uart@20000000。这个地址不一定是物理地址,但通常我们会用它来对应。
1.3 节点与属性——核心要素详解
节点和属性,是设备树里最基础的两个概念。我在项目中遇到过不少新手,把节点名和属性名搞混,结果内核死活认不出设备。
节点命名规则:
- 格式:
node-name@unit-address node-name:描述设备类型,比如uart、i2c、spiunit-address:设备的地址,通常是寄存器基址
常用属性一览:
| 属性名 | 含义 | 示例 |
|---|---|---|
compatible |
设备兼容性字符串,驱动匹配的关键 | "vendor,device" |
reg |
寄存器地址和长度 | <0x10000000 0x1000> |
interrupts |
中断号、触发类型 | <0 33 4> |
status |
设备状态,okay或disabled |
"okay" |
clocks |
时钟源引用 | <&clk_uart0> |
我的小技巧:写compatible属性时,建议遵循「厂商,设备型号」的格式。比如"ti,am335x-uart"。这样驱动匹配时,可以做到精确匹配,避免误认。
1.4 dts、dtsi、dtb——三兄弟的关系
这三个文件,我经常被问到有什么区别。其实特别简单:
- .dts:设备树源文件,你手写的硬件描述
- .dtsi:设备树头文件,公共部分放这里,用
#include引用 - .dtb:编译后的二进制文件,内核启动时直接加载
为什么要有.dtsi?你想想看,一个SoC可能有几十种开发板,但CPU核心、内存控制器、中断控制器这些是共用的。如果每个板子都写一遍,那跟以前的板级文件有什么区别?
所以,我们通常这样组织:
// am335x.dtsi —— SoC公共部分
#include "skeleton.dtsi"
/ {
compatible = "ti,am335x";
ocp {
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
};
};
// myboard.dts —— 具体板级配置
#include "am335x.dtsi"
/ {
model = "MyBoard AM335x";
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
};
};
编译命令也很简单:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
曾经踩过的坑:我曾经在项目中,把dtsi里的status = "disabled"忘了,结果所有板子都默认开启了某个外设,导致功耗超标。排查了两天才发现。所以,SoC公共部分的外设,我建议默认都disabled,板级文件里再逐个okay。
1.5 设备树在内核中的生命周期
设备树从源文件到被内核使用,大致走这么几步:
- 编译阶段:
dtc把.dts编译成.dtb - 启动阶段:Bootloader(如U-Boot)把
.dtb加载到内存,把地址传给内核 - 内核解析:内核在启动早期调用
unflatten_device_tree(),把二进制数据解析成树状结构 - 设备创建:内核遍历设备树,为每个
compatible匹配的节点创建平台设备 - 驱动绑定:驱动通过
of_match_table匹配节点,获取资源并初始化
我个人觉得,理解这个生命周期特别重要。很多驱动问题,其实不是代码写错了,而是设备树没被正确加载。比如我曾经遇到一个案子,U-Boot传的dtb地址不对,内核解析出来全是乱码,设备一个都没认到。
1.6 小结
设备树不是什么高深的技术,它就是一个硬件描述语言。你只要记住三点:
- 节点描述设备,属性描述特征
dts是源文件,dtsi是公共头文件,dtb是编译产物compatible是驱动匹配的钥匙,写错了设备就认不出来
嗯,基础就这些。后面的章节,我们会深入到性能优化和资源管理,到时候你会发现——设备树写得好不好,直接影响到系统的启动速度和内存占用。准备好了吗?我们继续往下走。