3. 设备树基础:DTS语法、节点属性、compatible匹配机制
设备树,说白了就是一块「硬件描述板」。
我刚开始接触Linux驱动时,最头疼的就是搞不懂设备树到底在干嘛。后来有个老同事跟我说了一句话,我到现在还记得:「设备树就是给内核看的硬件说明书,告诉它你的板子上接了哪些外设,地址是多少,中断挂在哪根线上。」
嗯,这句话一下子就点醒了我。今天我们就来聊聊这个「说明书」怎么写。
3.1 为什么需要设备树?
在设备树出现之前,Linux内核是怎么知道硬件信息的?答案是——硬编码。每个板子都在arch/arm/mach-xxx目录下有一堆C文件,里面写死了GPIO、中断、时钟等信息。你想想看,换一个板子就得改内核代码,重新编译,这谁受得了?
设备树(Device Tree)就是为了解决这个问题。它把硬件描述从内核代码中剥离出来,变成一个独立的.dts文件。内核启动时解析这个文件,动态构建出设备模型。说白了,就是「硬件配置」和「驱动代码」解耦了。
核心思想:设备树描述硬件,驱动代码处理逻辑。两者通过compatible属性「握手」。
3.2 DTS语法基础
设备树源文件(.dts)的语法其实很简单。我个人的习惯是把它想象成一棵倒着长的树——根节点是根,下面挂着一堆子节点,每个节点代表一个设备或总线。
先看一个最简单的例子:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard", "vendor,common-board";
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
status = "okay";
};
};
嗯,这里有几个关键点:
- /dts-v1/; —— 版本声明,告诉编译器这是DTS v1格式。没有这行,编译器会报错。
- / { }; —— 根节点。所有设备都挂在这个根下面。
- model —— 板子名称,纯描述性,内核不依赖它做匹配。
- compatible —— 最重要的属性,后面细讲。
- reg —— 地址和大小信息。格式是 <地址 大小>。
- uart0: —— 标签(label),方便在其他地方引用这个节点。
我的小技巧:写设备树时,我习惯先写根节点,再写memory节点,最后写外设节点。这样结构清晰,不容易漏掉关键信息。
3.3 节点属性详解
设备树的节点属性,说白了就是键值对。值可以是字符串、32位整数、64位整数、字节数组,或者它们的组合。
我整理了一个常用属性表,方便你查阅:
| 属性名 | 含义 | 示例 |
|---|---|---|
| compatible | 设备兼容性标识,驱动匹配的关键 | "vendor,device" |
| reg | 寄存器地址和长度 | <0x10000000 0x1000> |
| interrupts | 中断号、触发类型 | <0 33 4> |
| status | 设备状态,"okay"或"disabled" | "okay" |
| clock-frequency | 时钟频率(单位Hz) | <24000000> |
| gpios | GPIO引脚描述 | <&gpio1 15 0> |
这里要注意一点:reg属性的格式取决于父节点的#address-cells和#size-cells。如果#address-cells=1,地址就是32位;如果=2,地址就是64位。size-cells同理。我在项目中就踩过这个坑——写错了address-cells,结果内核解析出来的地址完全不对,调试了一整天。
3.4 compatible匹配机制
compatible属性是设备树和驱动之间的「暗号」。内核驱动通过of_match_table声明自己支持哪些设备,设备树通过compatible告诉内核「我是谁」。两者对上号了,驱动就probe。
匹配规则是这样的:
- 内核遍历设备树节点,读取compatible属性。
- 遍历已注册驱动的of_match_table。
- 字符串逐条比较,匹配成功则调用驱动的probe函数。
- 如果compatible有多个值(如"vendor,myboard", "vendor,common-board"),按顺序匹配,第一个成功就停。
举个例子,驱动端这样写:
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "vendor,uart", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);
static struct platform_driver my_uart_driver = {
.probe = my_uart_probe,
.remove = my_uart_remove,
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
};
设备树节点里写:
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
这样,内核启动时就会自动匹配,调用my_uart_probe函数。
我曾经踩过的坑:compatible字符串的格式一定要是"厂商,设备名",比如"ti,am335x-uart"。不要随便写,否则驱动匹配不上。另外,MODULE_DEVICE_TABLE宏一定要加,否则模块热插拔时找不到匹配关系。
3.5 编写一个简单的设备树节点
好了,理论讲完了,我们来动手写一个。假设我有一个LED设备,挂在GPIO1的第15号引脚上,高电平点亮。
完整的设备树节点如下:
myled: led@0 {
compatible = "vendor,myled";
reg = <0>; /* 仅用于索引,无实际地址 */
gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
label = "myboard:green:status";
linux,default-trigger = "heartbeat";
status = "okay";
};
这里解释一下:
- compatible —— 驱动匹配用,写"vendor,myled"。
- gpios —— 引用gpio1控制器,第15号引脚,高电平有效。
- label —— 用户空间的设备名,比如/sys/class/leds/myboard:green:status。
- linux,default-trigger —— 默认触发模式,这里用心跳灯。
对应的驱动端,of_match_table里写:
{ .compatible = "vendor,myled", },
probe函数里通过of_get_named_gpio_flags获取GPIO号,然后注册LED设备。嗯,就是这么简单。
我的建议:刚开始写设备树时,多看看内核源码里arch/arm/boot/dts/目录下的.dts文件。那些都是官方写好的例子,照着抄不会错。我自己就是靠「抄」学会的——先抄一个能用的,再慢慢改出自己需要的。
3.6 设备树知识体系总览
为了让你对设备树有个整体认识,我画了一张图:
这张图展示了设备树的基本结构。根节点下面挂CPU、内存、外设,每个节点都有自己的属性。驱动通过compatible找到对应的节点,然后读取属性来配置硬件。
好了,设备树的基础知识就讲到这里。记住一句话:设备树是硬件的「身份证」,compatible是驱动的「钥匙」。把这两点搞明白,后面写platform驱动就顺了。