第1章:设备树基础回顾——语法、节点与属性、reg与compatible
各位同学,咱们今天聊聊设备树的基础。说实话,我刚开始接触设备树时,也觉得这东西有点玄乎。不就是一堆文本描述硬件吗?后来踩了几个坑才明白——设备树是Linux内核与硬件之间的“翻译官”,写不好它,驱动根本跑不起来。
1.1 设备树是什么?
设备树(Device Tree),说白了就是一个描述硬件拓扑结构的数据结构。它用文本文件(.dts)描述CPU、内存、外设、中断控制器、GPIO等所有硬件资源。内核启动时解析这个文件,动态构建硬件信息,而不是把硬件信息硬编码在代码里。
我个人习惯把设备树比作“硬件的身份证”。你想想看,一个SoC可能有几十个外设,每个外设的地址、中断号、时钟频率都不一样。如果没有设备树,内核就得为每块板子单独写一份板级文件,维护成本极高。设备树解决了这个问题——同一份内核镜像,换一块板子只需要换一个.dtb文件。
核心要点:设备树是硬件描述语言,不是驱动代码。它告诉内核“我有什么硬件”,而不是“怎么驱动硬件”。
1.2 设备树语法速览
设备树的语法其实很简单,就是一棵树。根节点是“/”,下面挂各种子节点。每个节点代表一个硬件设备或总线。来看一个最基本的例子:
/dts-v1/;
/ {
compatible = "vendor,board-name";
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
嗯,这里要注意几个关键点:
- 节点名称:格式是“名称@地址”,比如cpu@0、serial@10000000。地址部分通常对应设备的基地址。
- 属性:键值对形式,值可以是字符串、整数、数组等。比如compatible、reg、interrupts都是属性。
- 标签(label):比如uart0:,方便在其他地方引用这个节点。
1.3 节点与属性——硬件的“自我介绍”
每个节点都有一堆属性,这些属性就是硬件在向内核做自我介绍。我挑几个最常用的说说:
| 属性名 | 作用 | 示例 |
|---|---|---|
| compatible | 设备兼容性标识,驱动匹配的关键 | "vendor,device" |
| reg | 描述设备地址空间 | <基地址 长度> |
| interrupts | 中断号与触发方式 | <0 33 4> |
| clocks | 时钟源引用 | <&clkc 15> |
| status | 设备状态(okay/disabled) | "okay" |
我在项目中遇到过最坑的事:某次调试I2C设备,驱动死活不probe。查了两天,最后发现是compatible字符串写错了一个字母。内核匹配驱动时是严格字符串比较,多一个空格都不行。从那以后,我每次写compatible都会反复核对数据手册。
1.4 reg属性——地址空间的“地图”
reg属性是设备树里最重要的属性之一。它告诉内核:这个设备占用了哪段地址空间。格式是:
reg = <address1 length1 address2 length2 ...>;
address和length的位数由父节点的#address-cells和#size-cells决定。比如:
/ {
#address-cells = <1>; // 地址用32位表示
#size-cells = <1>; // 长度用32位表示
uart@10000000 {
reg = <0x10000000 0x1000>; // 基址0x10000000,长度0x1000
};
};
如果#address-cells = <2>,那地址就要写成两个32位数,比如<0x0 0x10000000>。64位地址空间常见这种写法。
避坑指南:我曾经在移植一个老驱动时,发现reg长度写错了。外设寄存器范围是0x1000,我写成了0x100。结果内核只映射了256字节,访问后面的寄存器直接触发异常。记住:reg长度一定要覆盖所有需要用到的寄存器。
1.5 compatible属性——驱动匹配的“钥匙”
compatible属性是设备树与驱动之间的桥梁。内核通过它找到对应的驱动程序。格式通常是"厂商,设备型号",比如:
compatible = "ti,omap3-uart", "ns16550";
这里有两个字符串,内核会按顺序尝试匹配。第一个是精确匹配,第二个是兼容匹配。为什么这么设计?因为有些IP核是通用的,比如16550 UART,很多厂商都在用。写"ns16550"可以让内核用通用驱动,写"ti,omap3-uart"则可以用TI的定制驱动。
驱动代码里怎么匹配?看这个:
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "ti,omap3-uart", },
{ .compatible = "ns16550", },
{ /* sentinel */ }
};
static struct platform_driver my_uart_driver = {
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
.probe = my_uart_probe,
};
内核解析设备树时,会遍历所有节点,找到compatible匹配的节点,然后调用对应的probe函数。说白了,compatible就是一把钥匙,设备树是锁,驱动是钥匙坯子,匹配上了才能开门。
1.6 知识体系总览
为了让大家更直观地理解设备树的核心逻辑,我画了一张图:
1.7 小结
这一章我们回顾了设备树的基础知识。说白了,设备树就是一套描述硬件的“语言”,节点是名词,属性是形容词。compatible是设备的名片,reg是设备的地址地图。掌握了这些,你就能看懂大部分设备树文件了。
嗯,这里要提醒一句:设备树语法虽然简单,但细节很多。比如#address-cells和#size-cells的继承关系、中断映射的复杂规则,这些后面章节会慢慢展开。别急,咱们一步步来。
重要提醒:设备树文件(.dts)编译成二进制(.dtb)后,大小通常只有几KB到几十KB。但一个语法错误就能让内核启动失败。我建议每次修改后都用dtc工具检查一下:dtc -I dts -O dtb -o test.dtb test.dts,养成习惯。
公众号:蓝海资料掘金营,微信deep3321