2. 设备树语法精讲
各位同学,今天我们来啃设备树语法这块硬骨头。说实话,我刚接触设备树那会儿,看着满屏的.dts文件,心里直犯嘀咕:这不就是个配置文件吗?怎么搞得这么复杂?后来踩过几次坑才明白——设备树就是嵌入式世界的"宪法",它规定了硬件长什么样、驱动该怎么找它。
1. 节点与属性:设备树的基本单元
设备树的结构,说白了就是一棵树。根节点是/,下面挂着一堆子节点。每个节点代表一个硬件设备或总线,属性就是描述这个设备的参数。
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
reg = <0x0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
};
你看,每个节点用节点名@地址来标识。地址部分不是必须的,但如果有reg属性,我建议你加上——这样调试时一眼就能看出设备映射到哪个地址空间。
核心要点:
- 节点名:尽量用通用名称,比如
serial@...而不是my_uart@... - 属性值:字符串用双引号,32位整数用尖括号,字节数组用方括号
- 标签(label):可以在节点前加
&标签名,方便在其他地方引用
2. 标准属性:compatible、reg、interrupts
这三个属性是设备树的"三驾马车"。我在项目中见过有人把compatible写错,结果驱动死活加载不上——嗯,这种低级错误我也犯过。
2.1 compatible:驱动匹配的钥匙
compatible属性决定了哪个驱动来接管这个设备。格式是"厂商,型号",可以有多个值,内核会按顺序匹配。
i2c@44e0b000 {
compatible = "ti,omap4-i2c", "ti,omap3-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
};
我个人习惯把最具体的型号写在最前面,通用的放后面。这样如果新硬件有特殊处理,驱动能优先匹配到。
2.2 reg:地址空间的"身份证"
reg描述设备占用的地址范围。它由#address-cells和#size-cells决定格式。比如#address-cells = <1>表示地址用1个32位整数,#size-cells = <1>表示长度也用1个32位整数。
避坑指南:我曾经在移植一个网卡驱动时,发现reg里的地址长度写错了。父节点定义的是#size-cells = <1>,但我写成了<0x10000000 0x10000 0x2000>——多了一个值。内核解析时直接崩溃。记住:reg的格式必须和父节点严格一致。
2.3 interrupts:中断的"接线图"
中断属性描述设备的中断号、触发类型等信息。它依赖interrupt-parent指定中断控制器。
gpio_keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
button@0 {
label = "user-button";
linux,code = <KEY_ENTER>;
gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
};
};
你想想看,如果中断号写错了,驱动注册中断时就会失败。我调试过一个触摸屏驱动,中断一直触发不了,最后发现是interrupts里的IRQ_TYPE写成了IRQ_TYPE_EDGE_RISING,而硬件实际是低电平触发。
3. 特殊节点:chosen 与 aliases
这两个节点是设备树的"基础设施",虽然不直接对应硬件,但内核启动时离不开它们。
3.1 chosen:内核启动参数
chosen节点不描述硬件,而是传递启动参数。比如bootargs、stdout-path等。
chosen {
bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = &uart0;
};
我记得有一次调试,内核启动后串口没输出,查了半天发现是stdout-path指向了错误的UART节点。说白了,这就是个"默认控制台"的配置。
3.2 aliases:节点的"快捷方式"
aliases给节点起别名,方便在代码中引用。比如serial0 = &uart0;,这样驱动里可以直接用of_alias_get_id()获取序号。
aliases {
serial0 = &uart0;
serial1 = &uart1;
mmc0 = &mmc1;
mmc1 = &mmc2;
};
注意:别名不要乱起。我见过有人把mmc0和mmc1搞反,结果系统从eMMC启动时,内核却去SD卡找根文件系统——直接启动失败。别名最好和硬件设计文档保持一致。
4. 宏定义与include:让设备树"活"起来
设备树支持C语言的预处理,这意味着我们可以用#include、#define来组织代码。这招在大型项目中特别有用。
// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
model = "TI AM335x BeagleBone Black";
&i2c0 {
eeprom@50 {
compatible = "atmel,24c256";
reg = <0x50>;
pagesize = <64>;
};
};
};
你看,am33xx.dtsi定义了SoC的公共部分,am335x-bone-common.dtsi定义了板级公共部分,最后am335x-boneblack.dts只添加差异部分。这样维护起来轻松多了。
我的经验:
- 把SoC级定义放在
.dtsi文件中,板级差异放在.dts中 - 用
&标签语法覆盖或追加节点属性,不要直接修改原始文件 - 头文件中的宏定义(如
GPIO_ACTIVE_HIGH)能提高可读性,建议多用
5. 知识体系总览
下面这张图总结了设备树语法的核心结构,我建议你把它打印出来贴在工位上。
这张图把设备树的核心要素都串起来了。你从根节点出发,往下走是各种设备节点,每个节点挂载标准属性;特殊节点chosen和aliases提供启动参数和别名;预处理机制让代码可以模块化组织。
最后说一句:设备树语法不难,难的是理解硬件和驱动之间的映射关系。我建议你拿到一块新板子时,先读它的.dts文件,对照原理图把每个节点的reg和interrupts过一遍。这样调试驱动时,心里就有底了。