设备树语法精讲:节点与属性
设备树这东西,说白了就是一块描述硬件的数据结构。我刚开始接触时也觉得它挺玄乎,后来发现它就是个“硬件说明书”——告诉内核你的板子上接了哪些外设、地址是多少、中断怎么连。今天咱们就把它的语法掰开揉碎了讲。
节点(Node)与属性(Property)
设备树的基本单位是节点。每个节点代表一个硬件设备或总线。节点里可以嵌套子节点,形成树状结构。属性则是节点的“特征描述”,比如地址、中断号、时钟频率等。
看个最简单的例子:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard";
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x00>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
根节点 / 下面挂了两个子节点:cpu@0 和 memory@80000000。每个节点都有 compatible 属性,告诉内核该用哪个驱动。节点名后面的 @ 跟的是地址,用来区分同类型设备。
标准属性详解
设备树里有一组标准属性,几乎所有设备都会用到。我挑几个最关键的讲。
compatible:驱动匹配的关键
compatible 是设备树里最重要的属性。内核通过它来匹配驱动。格式是 "厂商,设备型号",比如 "ti,am335x-uart"。可以写多个值,内核会按顺序尝试匹配。
uart0: serial@44e09000 {
compatible = "ti,am335x-uart", "ns16550";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这里写了两个兼容值。内核会先尝试匹配 ti,am335x-uart,如果没找到驱动,再试 ns16550。我在项目中经常用这个技巧来做驱动兼容——新板子用新驱动,老板子用通用驱动。
reg:地址与大小
reg 描述设备的地址空间。格式是 <地址 大小>,可以有多组。比如:
reg = <0x44e09000 0x2000>;
表示设备从 0x44e09000 开始,占用 0x2000 字节。如果设备有多个地址段,可以这样写:
reg = <0x44e09000 0x2000>, <0x44e0b000 0x1000>;
#address-cells 和 #size-cells 决定。默认是 1 个 cell(32 位)。如果地址超过 4GB,需要设置 #address-cells = <2>。
interrupts:中断配置
中断属性描述设备的中断号、触发方式等。格式取决于中断控制器。常见写法:
interrupts = <0 72 4>;
这里三个值分别表示:中断类型(0=SPI,1=PPI)、中断号、触发方式(1=上升沿,2=下降沿,4=高电平,8=低电平)。
我记得有一次调试网卡驱动,死活收不到中断。查了半天发现是中断号写错了——设备树里写的是 72,但芯片手册上写的是 73。嗯,这种低级错误最容易坑人。
其他常用标准属性
| 属性名 | 作用 | 示例 |
|---|---|---|
device_type |
设备类型(主要用于内存节点) | device_type = "memory"; |
status |
设备状态(okay/disabled) | status = "disabled"; |
clock-frequency |
时钟频率 | clock-frequency = <24000000>; |
interrupt-parent |
指定中断控制器 | interrupt-parent = <&gic>; |
特殊节点:aliases、chosen、memory
除了普通设备节点,设备树里还有几个特殊节点,它们不描述具体硬件,而是提供系统级配置。
aliases:给节点起别名
aliases 节点用来给其他节点起短名字。方便在代码里引用。比如:
aliases {
serial0 = &uart0;
serial1 = &uart1;
ethernet0 = &mac;
};
这样在 U-Boot 或内核代码里,就可以直接用 serial0 来引用 uart0 节点。我个人习惯把所有关键设备都加个别名,调试时方便很多。
chosen:传递运行时参数
chosen 节点不描述硬件,而是传递系统启动时的参数。最常用的是 bootargs 和 stdout-path:
chosen {
bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = &uart0;
};
U-Boot 会把内核启动参数填到 bootargs 里。内核启动时从 chosen 节点读取这些参数。我曾经遇到过一个问题:改了 bootargs 但系统没生效,后来发现是 U-Boot 覆盖了设备树里的 chosen 节点——嗯,U-Boot 的优先级比设备树高。
memory:告诉内核内存布局
memory 节点描述系统的物理内存。格式很简单:
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
表示从 0x80000000 开始,有 1GB(0x40000000)内存。如果内存不连续,可以写多组 reg:
memory {
device_type = "memory";
reg = <0x80000000 0x40000000>, <0xC0000000 0x40000000>;
};
device_type = "memory" 属性,否则内核不会把它当作内存。这是设备树规范里少数几个必须写 device_type 的地方。
设备树语法结构图
下面这张图帮你理清设备树的整体结构:
实战经验总结
讲几个我在项目中踩过的坑:
- reg 地址写错:有一次 SPI 控制器不工作,查了两天才发现 reg 里的地址少写了一个 0。0x44e09000 写成了 0x44e9000——少了一位,地址完全不对。
- interrupts 触发方式不对:某个 GPIO 中断设备,设备树里写的是上升沿触发,但实际硬件是下降沿。结果中断触发一次,内核收到两次。
- compatible 字符串拼写:大小写敏感!
"ti,am335x-uart"和"TI,AM335X-UART"是两个完全不同的字符串。我见过有人把am335x写成am335,驱动死活匹配不上。
dtc -I dtb -O dts 把编译好的 dtb 反编译成 dts,检查属性值是否正确。再用 ls /proc/device-tree/ 查看内核实际解析的设备树。这两个命令能解决 90% 的设备树问题。
设备树语法其实不复杂,核心就是节点和属性。你只要记住:每个节点代表一个设备,每个属性描述设备的一个特征。剩下的就是多写多练,踩过几次坑就熟了。