第一章:设备树入门——什么是设备树?为什么需要设备树?设备树在Linux内核中的角色
大家好,我是老李。做嵌入式Linux驱动开发有些年头了。今天咱们聊聊设备树。
说实话,我刚入行那会儿,还没有设备树这东西。那时候写驱动,板子信息都是硬编码在C文件里的。每次换一个板子,就得改一堆代码,重新编译内核。那叫一个痛苦。
后来设备树出现了。我第一次接触它时,心里想:这不就是个配置文件吗?有什么了不起的?
嗯,后来我发现,我错了。它远不止是个配置文件那么简单。
1.1 什么是设备树?
设备树,英文叫Device Tree,简称DT。它是一种描述硬件资源的数据结构。
说白了,它就是一张硬件清单。告诉内核:你的CPU是什么?内存多大?I2C控制器挂在哪里?GPIO引脚怎么分配的?
设备树的源文件通常以.dts结尾。编译后生成.dtb文件,这就是内核真正读取的二进制格式。
核心概念:设备树将硬件描述从内核代码中剥离出来,变成独立的数据文件。内核启动时解析这个文件,就知道自己跑在什么硬件上了。
来看一个最简单的例子:
/dts-v1/;
/ {
compatible = "acme,coyote";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
chosen {
bootargs = "console=ttyS0,115200";
};
};
这段代码描述了一个叫"coyote"的板子,内存从0x80000000开始,大小512MB。启动参数也写好了。
你可能会问:就这么简单?对,就是这么简单。但它的威力在后面。
1.2 为什么需要设备树?
这个问题,我当年也问过自己。为什么非要搞个设备树出来?
原因有三个:
- 解耦硬件描述和驱动代码——以前改个引脚配置就得重新编译内核,现在改个dts文件就行。
- 支持多平台——同一个内核镜像,换不同的dtb文件,就能跑在不同的板子上。
- 社区标准化——ARM架构之前乱得很,每家厂商都有自己的板级文件。设备树统一了描述方式。
我记得有一次,客户临时换了一个Flash芯片。引脚完全不一样。要是以前,我得改驱动代码,重新编译,测试,至少折腾两天。有了设备树,我只改了dts文件里的引脚配置,半小时搞定。
我的经验:设备树最大的价值不是技术本身,而是它改变了开发流程。硬件工程师改完原理图,直接更新dts文件。软件工程师拿到新dts,重新编译dtb,就能验证。两边不用互相等。
1.3 设备树在Linux内核中的角色
设备树在内核里扮演什么角色?我打个比方你就明白了。
想象一下,内核是一个操作系统,设备树就是它的"硬件说明书"。内核启动时,先读说明书,知道硬件长什么样,然后按图索骥,加载对应的驱动。
具体流程是这样的:
- Bootloader(比如U-Boot)加载内核镜像和dtb文件到内存
- 内核启动时,解析dtb,构建设备树结构
- 驱动通过OF(Open Firmware)API查询设备树,获取硬件信息
- 驱动根据这些信息初始化硬件
你看,设备树成了内核和硬件之间的桥梁。没有它,内核就不知道该怎么跟硬件打交道。
注意:设备树不是万能的。它只描述硬件"有什么",不描述硬件"怎么用"。怎么用,那是驱动代码的事。我见过不少新手把设备树当成驱动来写,结果越写越乱。
举个例子,设备树可以告诉你:这里有一个UART控制器,地址在0x1C090000,中断号是37。但它不会告诉你,这个UART怎么初始化,怎么收发数据。那些逻辑,都在驱动代码里。
1.4 设备树的基本语法
设备树的语法其实很简单。核心就几个概念:
| 概念 | 说明 | 示例 |
|---|---|---|
| 节点 | 用花括号包裹的硬件单元 | uart0: serial@1c090000 { ... }; |
| 属性 | 节点的特征描述 | compatible = "ns16550"; |
| 值 | 属性的具体内容 | reg = <0x1c090000 0x1000>; |
节点可以嵌套。比如一个I2C控制器下面,可以挂多个I2C设备:
&i2c0 {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
rtc@51 {
compatible = "ds1307";
reg = <0x51>;
};
};
这里&i2c0是引用之前定义好的I2C控制器节点。我们在它下面添加了两个子节点:一个EEPROM,一个RTC。
避坑指南:我曾经犯过一个低级错误——忘了写status = "okay";。结果设备树解析没问题,但驱动就是加载不了。查了两天才发现,默认status是"disabled"。嗯,从那以后,我每次写设备树都会检查status属性。
1.5 设备树与驱动的绑定
设备树怎么跟驱动关联起来?靠的是compatible属性。
驱动里会声明自己支持哪些设备:
static const struct of_device_id my_driver_of_match[] = {
{ .compatible = "acme,my-device" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);
static struct platform_driver my_driver = {
.probe = my_driver_probe,
.remove = my_driver_remove,
.driver = {
.name = "my_device",
.of_match_table = my_driver_of_match,
},
};
内核启动时,遍历设备树中的所有节点。如果某个节点的compatible属性跟驱动声明的匹配,内核就调用这个驱动的probe函数。
说白了,这就是一个"对暗号"的过程。设备树说"我是acme,my-device",驱动说"我支持acme,my-device",好,你们配对成功。
1.6 小结
设备树不是什么高深的技术。它就是一套描述硬件的规则。但它的出现,让嵌入式Linux开发变得灵活了很多。
我个人觉得,学习设备树最好的方法就是动手。找一个现成的开发板,看看它的dts文件,改几个参数,编译,烧录,看看效果。比看十遍文档都管用。
下一章,我们会深入设备树的语法细节,包括节点、属性、引用、覆盖等高级用法。到时候我会分享一些我在项目中踩过的坑,保证让你少走弯路。
今天就到这里。有问题欢迎交流。