第一章:设备树基础——什么是设备树?为什么需要设备树?设备树在嵌入式Linux中的角色
大家好,我是你们的讲师。今天咱们聊聊设备树。说实话,我刚入行那会儿,设备树还没这么普及。那时候做嵌入式Linux,板级文件(board file)满天飞,每次换个芯片型号,都得改一堆C代码。后来设备树出现了,嗯,这玩意儿确实解决了不少痛点。
1.1 什么是设备树?
设备树,英文叫Device Tree,简称DT。说白了,它就是一个描述硬件信息的数据结构。你想想看,Linux内核要跑起来,总得知道CPU是什么、内存多大、外设挂在哪里吧?设备树就是干这个的。
设备树的文件格式有两种:
- .dts:设备树源文件(Device Tree Source),人类可读的文本格式
- .dtb:设备树二进制文件(Device Tree Blob),编译后的二进制格式
我习惯把.dts比作C语言的源文件,.dtb就是编译后的可执行文件。U-Boot启动时,会把.dtb加载到内存,然后传给内核。
核心概念:设备树本质上是一个树形结构,根节点是"/",下面挂载各种子节点,每个节点代表一个硬件设备或总线。
来看一个最简单的设备树示例:
/dts-v1/;
/ {
compatible = "vendor,board-name";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB内存
};
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
};
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 5 4>;
clock-frequency = <24000000>;
};
};
这段代码描述了什么?根节点下有三个子节点:memory(内存)、chosen(启动参数)、uart0(串口)。每个节点都有属性,比如compatible告诉内核这个设备兼容哪个驱动,reg描述地址和大小。
我的经验:刚开始写设备树时,最容易搞错的是reg属性的地址范围。我在项目中遇到过,因为reg写错了,导致内核访问外设时直接挂死。嗯,调试这种问题特别痛苦,建议你写完后先用dtc工具检查语法。
1.2 为什么需要设备树?
这个问题问得好。在设备树出现之前,嵌入式Linux是怎么处理硬件的?答案是——硬编码。
早期的内核里,每个板子都有一个对应的board文件,比如arch/arm/mach-xxx/board-yyy.c。这些文件里写满了硬件初始化代码、GPIO配置、中断号映射等等。你想想看,如果内核要支持100种板子,就得有100个board文件。维护起来简直是噩梦。
设备树解决了三个核心问题:
- 硬件描述与内核代码分离:硬件信息放在.dts里,内核驱动通过解析设备树来获取参数。换板子只需要换.dtb,不用改内核。
- 减少内核代码量:Linux主线内核现在支持上千种板子,如果没有设备树,代码量会膨胀到无法维护。
- 标准化接口:设备树规范是标准化的,ARM、RISC-V、MIPS等架构都在用。U-Boot也支持设备树,启动时直接传递.dtb给内核。
避坑指南:我曾经接手过一个项目,前任工程师把硬件配置写死在驱动里,完全没用设备树。结果客户要换一颗Flash芯片,得改三四个文件。后来我花了一周时间把所有硬件参数迁移到设备树里,再改驱动去解析设备树。嗯,从那以后,我再也不允许团队在驱动里硬编码硬件参数了。
1.3 设备树在嵌入式Linux中的角色
设备树在嵌入式Linux启动流程中扮演着"翻译官"的角色。咱们来看看整个启动链条:
| 阶段 | 角色 | 设备树的作用 |
|---|---|---|
| U-Boot | 引导加载程序 | 加载.dtb到内存,修改或添加节点(如内存大小、MAC地址) |
| Linux内核 | 操作系统内核 | 解析.dtb,创建设备模型,匹配驱动 |
| 设备驱动 | 硬件驱动模块 | 通过设备树API获取硬件资源(中断号、寄存器地址、DMA通道等) |
说白了,设备树就是U-Boot和内核之间的"契约"。U-Boot负责把硬件信息整理成.dtb,内核负责读懂它。我见过不少新手,在U-Boot里改了设备树,但内核没重新编译,结果启动时内核报"无法匹配设备"。其实问题很简单——U-Boot传的.dtb和内核期望的不一致。
设备树在Linux内核中的角色可以概括为三点:
- 硬件探测:内核启动时,通过设备树知道有哪些设备、在哪里、怎么配置。
- 驱动匹配:每个设备节点都有compatible属性,驱动通过这个属性来匹配自己该管哪个设备。
- 资源分配:中断、DMA、时钟、GPIO等资源,都通过设备树描述,驱动用of_xxx系列API来获取。
举个例子:你在设备树里写了一个I2C控制器节点,compatible = "snps,designware-i2c"。内核启动时,会遍历所有注册的I2C驱动,找到compatible匹配的那个驱动,然后调用它的probe函数。驱动在probe里通过设备树获取基地址、中断号、时钟频率,然后初始化硬件。整个过程不需要改一行内核代码。
你可能会问:设备树和U-Boot到底怎么协同?嗯,这个问题我们后面章节会详细讲。简单说,U-Boot在启动内核前,会做三件事:
- 从存储介质(Flash、SD卡、网络)加载.dtb文件到内存
- 根据实际硬件情况,修改.dtb中的某些节点(比如根据EEPROM读取的MAC地址,更新eth节点)
- 把.dtb的地址通过寄存器或参数传递给内核
我记得有一次调试,板子上的DDR容量是512MB,但设备树里写的是256MB。U-Boot启动时检测到实际内存大小,自动修改了memory节点的reg属性。嗯,这就是设备树的灵活性——它允许U-Boot在运行时动态调整硬件描述。
个人建议:刚开始学设备树,别急着写复杂的.dts。先拿一块开发板,看看它的设备树文件长什么样。用dtc工具把.dtb反编译成.dts,对照原理图一行一行看。我当年就是这么学的,虽然慢,但基础打得牢。
总结一下今天的内容:设备树是嵌入式Linux的硬件描述语言,它把硬件信息从内核代码中剥离出来,让U-Boot和内核通过.dtb这个"契约"来协同工作。没有设备树,嵌入式Linux不可能支持今天这么多五花八门的硬件平台。
下一章,咱们会深入设备树的语法和编译方法。到时候我会带大家手写一个完整的.dts文件,然后编译成.dtb,再放到U-Boot里测试。嗯,敬请期待。