1. 设备树基础:设备树(DTS)的起源与作用
1.1 为什么需要设备树?
先说说我早年做嵌入式开发时的经历吧。那时候,每次换一块新板子,都得重新编译内核。为什么?因为硬件信息是硬编码在C语言里的。你想想看,换个GPIO引脚、改个I2C地址,就得改arch/arm/mach-xxx下的板级文件。这活儿,说白了就是个体力活。
我记得有一次,客户临时换了个Flash芯片,只是容量不同、分区表变了。我愣是花了半天改代码、重新编译、烧录。当时我就想:要是能把硬件描述和内核代码分开,该多好?
嗯,设备树就是来解决这个问题的。
设备树的核心思想: 把硬件信息从内核源码中剥离出来,用一套独立的数据结构来描述。内核启动时,解析这个描述,动态获取硬件配置。
1.2 设备树的起源
设备树(Device Tree)最早源于PowerPC架构。2005年左右,PowerPC社区受够了板级文件泛滥的苦。他们借鉴了Open Firmware的FDT(Flattened Device Tree)概念,把硬件描述做成二进制格式,交给内核解析。
后来,ARM社区也遇到了同样的问题。2011年到2012年间,ARM Linux社区开始大规模采用设备树。到了2013年,新加入的ARM SoC基本都要求必须支持设备树。我那时候正好在做一个基于TI AM335x的项目,算是赶上了这波转型。
为什么会这样?因为ARM生态太碎片化了。每家芯片厂商都有自己的板级文件,内核维护者疲于应付。设备树一统江湖后,内核代码干净多了。
1.3 设备树在Linux内核中的位置
设备树在内核启动流程中,扮演着「硬件信息传递者」的角色。它的位置可以用下面这张图来说明:
从这张图可以看出,设备树贯穿了从Bootloader到驱动匹配的整个流程。我个人习惯把设备树比作「硬件说明书」——内核拿到它,就知道板上有什么、怎么用。
1.4 设备树相关的文件类型
实际工作中,你会接触到三种文件:
| 文件类型 | 扩展名 | 说明 |
|---|---|---|
| 设备树源文件 | .dts |
文本格式,开发者编写,类似C语言的语法 |
| 设备树头文件 | .dtsi |
可被多个.dts包含,用于SoC公共部分或板级公共部分 |
| 设备树二进制文件 | .dtb |
编译后的二进制,Bootloader加载并传递给内核 |
避坑指南: 我曾经犯过一个低级错误——直接修改了.dtb文件。记住,.dtb是二进制,不能直接编辑。要改就改.dts,然后用dtc编译器重新生成。命令是:dtc -I dts -O dtb -o xxx.dtb xxx.dts
1.5 一个简单的设备树示例
光说不练假把式。我们来看一个最简单的设备树片段:
/dts-v1/;
/ {
model = "MyBoard V1.0";
compatible = "vendor,myboard-v1", "vendor,myboard";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB */
};
uart0: serial@10000000 {
compatible = "vendor,uart-ip";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
status = "okay";
};
};
这段代码描述了什么?
- 根节点:板子型号和兼容性标识
- chosen节点:内核启动参数,相当于给内核传参
- memory节点:内存起始地址0x80000000,大小512MB
- uart0节点:串口控制器,地址0x10000000,中断号33
你想想看,如果没有设备树,这些信息就得写在arch/arm/mach-xxx/board-xxx.c里。换块板子,就得改C代码、重新编译。有了设备树,只需要换一个.dtb文件就行。
1.6 设备树与内核驱动匹配的关系
设备树最终要服务于驱动匹配。内核怎么知道哪个驱动对应哪个设备?靠的就是compatible属性。
驱动代码里会声明一个of_match_table:
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "vendor,uart-ip", },
{ /* sentinel */ }
};
static struct platform_driver my_uart_driver = {
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
.probe = my_uart_probe,
.remove = my_uart_remove,
};
内核启动时,遍历设备树中的所有节点。如果某个节点的compatible值与驱动的of_match_table匹配,就调用该驱动的probe函数。这就是设备树驱动匹配的核心机制。
注意: compatible属性的值有约定俗成的命名规则——「厂商,型号」。比如"ti,am335x-uart"、"fsl,imx6q-uart"。不要随便起名字,否则驱动匹配不上,板子就起不来了。
1.7 小结
设备树不是什么高深莫测的东西。说白了,它就是一份硬件配置清单。内核启动时照着清单干活,不用再硬编码在C代码里。
我个人觉得,理解设备树的关键就三点:
- 为什么需要:解耦硬件描述与内核代码
- 怎么工作:.dts → .dtb → 内核解析 → 驱动匹配
- 核心属性:compatible是匹配的关键,reg描述地址空间,interrupts描述中断
嗯,这一章就到这里。下一章我们会深入设备树的语法细节,包括节点、属性、标签、引用这些具体写法。到时候我会拿一个实际项目中的设备树文件来拆解,看看那些坑是怎么踩出来的。