1. 设备树基础概念:什么是设备树、为什么需要设备树、设备树文件结构
各位好,我是老陈。做嵌入式 Linux 开发十几年了,今天咱们聊聊设备树。说实话,我刚入行那会儿还没有设备树这东西,板级文件满天飞,每次换一个外设都得改内核源码,编译、烧录、重启……那叫一个痛苦。后来设备树出来了,我一开始还觉得「又多了一个要学的东西」,但真正用上之后,只能说——真香。
1.1 什么是设备树
设备树,英文叫 Device Tree,是一套用来描述硬件资源的数据结构。说白了,它就是一个硬件清单,告诉内核:你的 CPU 是什么、内存多大、I2C 控制器挂在哪、GPIO 怎么用、中断路由怎么走……
我习惯把它理解成「硬件的身份证」。内核不再需要硬编码这些信息,而是启动时从设备树里读。你想想看,这多灵活?同一份内核镜像,换一个设备树文件,就能跑在不同的板子上。
核心定义:设备树是一种树形结构的、键值对形式的硬件描述语言,用于将硬件配置从内核源码中剥离出来,交给 bootloader 在启动时传递给内核。
1.2 为什么需要设备树
在设备树出现之前,Linux 内核是怎么处理硬件差异的?答案是——板级文件(Board File)。
每个板子都要在 arch/arm/mach-xxx 下写一个 board-xxx.c,里面用 C 代码注册各种平台设备、资源、中断。我当年维护过一个项目,同时支持 5 块板子,每次内核版本升级,光合并板级文件就要花掉我整整两天。而且,这些代码都是编译进内核的,换板子就得重新编译。
设备树解决了三个核心痛点:
- 硬件描述与内核代码解耦 —— 改硬件配置不用改内核源码,改 dts 文件就行
- 多平台支持 —— 同一份内核镜像,配不同的 dtb 文件就能跑在不同板子上
- 社区标准化 —— ARM 生态碎片化严重,设备树统一了硬件描述方式
我记得有一次,客户临时换了一颗不同型号的 Flash 芯片。搁以前,我得改内核源码、重新编译、发补丁。有了设备树,我只改了 dts 文件里的 reg 属性,重新编译 dtb,烧进去就完事了。前后不到 10 分钟。
我的经验:设备树不是万能的。它只描述硬件「有什么」,不描述硬件「怎么用」。驱动的逻辑代码还是得写,设备树只是把配置参数从代码里抽出来了。
1.3 设备树文件结构:dts / dtsi / dtb
设备树涉及三种文件类型,很多新手一开始容易搞混。我画了一张图,帮你理清它们的关系:
1.4 三种文件的详细对比
| 文件类型 | 全称 | 格式 | 作用 | 谁编写 |
|---|---|---|---|---|
| .dtsi | Device Tree Source Include | 文本(人类可读) | 描述 SoC 内部通用资源(CPU、中断控制器、时钟等) | SoC 厂商提供 |
| .dts | Device Tree Source | 文本(人类可读) | 描述具体板级硬件(外设型号、引脚复用、内存布局等) | 板卡开发者 |
| .dtb | Device Tree Blob | 二进制(机器可读) | 内核启动时解析的最终格式 | 由 dtc 编译器生成 |
1.5 一个简单的 dts 示例
光说不练假把式。咱们看一个最简单的设备树文件长什么样:
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "MyCustomBoard";
compatible = "mycompany,myboard", "fsl,imx6ull";
memory {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB DDR */
};
&uart1 {
status = "okay";
pinctrl-0 = <&pinctrl_uart1>;
};
&i2c1 {
clock-frequency = <100000>;
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
};
这段代码我拆开讲一下:
/dts-v1/;—— 版本声明,告诉编译器这是 v1 格式#include "imx6ull.dtsi"—— 引入 SoC 级描述,类似 C 语言的#include/ { ... };—— 根节点,所有设备都挂在这个树下model和compatible—— 板子标识,内核靠它匹配驱动memory节点 —— 描述内存起始地址和大小&uart1—— 引用 .dtsi 中定义的节点,覆盖或追加属性eeprom@50—— 挂在 I2C 总线上的子设备,地址 0x50
注意:我曾经遇到过一个坑——compatible 属性的字符串顺序很重要。内核匹配驱动时,会按顺序尝试,第一个匹配到的驱动会被加载。如果你把通用的写在前面,专用的写在后面,可能会加载错驱动。建议把最具体的型号写在最前面。
1.6 编译与反编译
设备树的编译工具叫 dtc(Device Tree Compiler)。我常用的几个命令:
# 编译 dts 为 dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译 dtb 为 dts(调试时很有用)
dtc -I dtb -O dts -o myboard.dts myboard.dtb
# 查看 dtb 中的内容(不生成文件)
dtc -I dtb -O dts /boot/myboard.dtb | less
反编译这个功能,我调试时经常用。有时候拿到一个别人给的 dtb,不知道里面配了什么,反编译一看就全明白了。
小技巧:编译时加 -@ 选项可以生成符号节点,方便后续动态加载设备树插件(DT Overlay)。这个在 FPGA 和热插拔场景下特别有用。
1.7 设备树的启动流程
最后,咱们捋一下设备树从文件到内核的完整路径:
- 编写阶段:开发者编写 .dts 文件,通过
#include引用 .dtsi - 编译阶段:dtc 编译器将 .dts 编译为 .dtb 二进制文件
- 存储阶段:.dtb 文件存放在 boot 分区或与内核镜像打包在一起
- 加载阶段:bootloader(如 U-Boot)从存储介质读取 .dtb 到内存
- 传递阶段:bootloader 将 .dtb 的内存地址通过寄存器传递给内核
- 解析阶段:内核启动早期,解析 .dtb 并构建 device_node 树
- 驱动匹配:各驱动通过
of_match_table匹配设备节点,完成初始化