1. 设备树基础:什么是设备树?为什么需要设备树?设备树在Linux内核中的角色。
大家好,我是你们的老朋友。今天咱们开始聊设备树。说实话,我刚入行那会儿,设备树这个概念让我头疼了好一阵子。但搞明白之后,你会发现它其实没那么玄乎。
1.1 什么是设备树?
设备树,英文叫 Device Tree,简称 DT。说白了,它就是一个描述硬件的数据结构。嗯,你可以把它想象成一张硬件的「身份证」。
这张身份证上写了什么?写了你的板子上有哪些硬件设备,它们怎么连接的,地址是多少,中断号是多少,等等。内核启动的时候,就靠读这个文件来认识硬件。
核心定义:设备树是一种用于描述硬件资源的数据结构,它以树形结构组织,每个节点代表一个硬件设备或总线。
举个例子,一个简单的设备树节点长这样:
/ {
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x00>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
uart@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
你看,每个节点都有名字、地址,还有 compatible 属性。这个 compatible 特别重要,它告诉内核:这个设备用哪个驱动来管。
1.2 为什么需要设备树?
这个问题问得好。我刚开始做嵌入式的时候,还没有设备树。那时候怎么搞?硬编码!
什么意思呢?就是每个板子都要在 Linux 内核源码里写一堆 board-xxx.c 文件。里面全是硬件的地址、中断号、时钟配置。你想想看,一个芯片支持几十种板子,就得写几十个这样的文件。维护起来简直是噩梦。
我记得有一次,客户换了一个内存颗粒,只是改了容量和时序参数。结果呢?我得重新编译整个内核,就为了改一个 board 文件里的几个数字。你说崩溃不崩溃?
设备树解决了这个问题。它把硬件描述从内核源码里剥离出来,变成了一个独立的数据文件。这样,换硬件只需要改设备树文件,内核源码不用动。
我的经验:设备树最大的好处是「硬件与驱动解耦」。驱动写一次,设备树配不同板子。这在项目迭代快的公司里,能省下大量时间。
具体来说,设备树带来了几个关键好处:
- 硬件描述标准化:用统一的语法描述硬件,不再依赖特定架构的板级文件
- 驱动通用化:驱动只认 compatible 字符串,不关心具体板子
- 启动参数灵活:内存大小、时钟频率、引脚复用,全在设备树里配
- 多平台支持:同一份内核镜像,换设备树就能跑不同板子
1.3 设备树在Linux内核中的角色
设备树在内核里扮演什么角色?我打个比方你就明白了。
内核就像一个操作系统,设备树就是它的「硬件地图」。没有这张地图,内核就是个瞎子,不知道内存在哪,不知道串口在哪,更不知道怎么跟外设通信。
具体流程是这样的:
- Bootloader(比如 U-Boot)把设备树二进制文件(DTB)加载到内存
- 内核启动时,从指定地址读取 DTB
- 内核解析 DTB,构建出设备树结构体(device_node 树)
- 驱动通过设备树 API 查找节点、读取属性
- 驱动根据设备树信息初始化硬件
你看,设备树贯穿了整个硬件初始化过程。没有它,内核连内存大小都不知道,更别提挂载根文件系统了。
注意:设备树不是万能的。它只描述「硬件有什么」,不描述「硬件怎么用」。怎么用是驱动的事。我曾经见过有人把驱动逻辑写进设备树,这是大忌!
1.4 设备树的文件格式
设备树有三种文件格式,我简单列一下:
| 格式 | 扩展名 | 说明 |
|---|---|---|
| DTS | .dts | 设备树源文件,文本格式,人类可读 |
| DTSI | .dtsi | 设备树头文件,用于包含公共定义 |
| DTB | .dtb | 设备树二进制文件,内核实际使用的格式 |
DTS 编译成 DTB 的工具叫 dtc(Device Tree Compiler)。这个工具在 Linux 内核源码的 scripts/dtc 目录下。我习惯用内核自带的 dtc,版本匹配,不容易出问题。
1.5 一个完整的例子
咱们来看一个实际项目中用到的设备树片段。这是我之前做的一个 i.MX6ULL 板子的配置:
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "MyCustomBoard";
compatible = "mycompany,imx6ull-board", "fsl,imx6ull";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256MB */
};
chosen {
stdout-path = &uart1;
};
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
&i2c1 {
clock-frequency = <100000>;
status = "okay";
touch@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <9 2>;
};
};
这里有几个关键点:
/dts-v1/;表示使用设备树 v1 语法#include引入芯片级公共定义model和compatible标识板子型号chosen节点指定控制台输出设备&uart1这种写法叫「引用」,用于覆盖或追加节点属性
避坑指南:我曾经犯过一个低级错误——忘了在 DTS 里加 /dts-v1/; 声明。结果 dtc 编译出来的 DTB 内核不认,启动直接挂掉。排查了半天才发现。所以,第一行一定要写!
1.6 设备树与驱动的绑定
设备树怎么跟驱动对上号?靠的就是 compatible 属性。
驱动里会定义一个 of_device_id 表:
static const struct of_device_id my_driver_of_match[] = {
{ .compatible = "mycompany,imx6ull-board", },
{ .compatible = "edt,edt-ft5x06", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);
内核启动时,会遍历设备树的所有节点。每个节点都带着 compatible 字符串。内核拿这个字符串去匹配驱动的 of_device_id 表。匹配上了,就调用驱动的 probe 函数。
说白了,这就是一个「对暗号」的过程。设备树说「我是 edt,edt-ft5x06」,驱动说「我认识 edt,edt-ft5x06」,好,你们俩配对成功。
1.7 小结
好了,这一章的内容就这么多。咱们回顾一下:
- 设备树是硬件的「身份证」,用树形结构描述硬件资源
- 它解决了板级文件泛滥的问题,实现了硬件与驱动的解耦
- 在内核中,设备树是硬件初始化的「地图」,驱动靠它找到设备
- DTS 编译成 DTB,内核解析 DTB 构建设备树结构
下一章,咱们会深入设备树的语法细节。到时候我会手把手教你写一个完整的 DTS 文件。嗯,敬请期待。