设备树语法详解:根节点、别名节点、chosen节点、memory节点、cpus节点、reg属性、interrupts属性、compatible属性
设备树(Device Tree)说白了就是一块描述硬件的数据结构。我刚开始接触Linux内核时,总觉得这玩意儿有点多余——硬件信息不都在代码里写死了吗?后来做了一次板级移植,才发现没有设备树,改一个GPIO都得重新编译整个内核。嗯,从那以后我就老老实实学设备树语法了。
这一章我们聚焦设备树的核心语法。你不需要一次性记住所有细节,但根节点、几个关键子节点和常用属性,必须烂熟于心。
核心观点:设备树是硬件描述与内核代码的解耦层。你改硬件,只改.dts文件,不用动C代码。
根节点(/)
每个设备树文件有且只有一个根节点,用斜杠 / 表示。它就像文件系统的根目录,所有其他节点都是它的孩子或孙子。
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
};
根节点里一般放什么?model 描述板子型号,compatible 给内核做板级匹配。我在项目里见过有人把根节点写得乱七八糟,外设节点直接挂在根下面——这其实不违规,但可读性很差。我建议根节点只放全局属性,外设都放到 soc 或 amba 总线节点下。
别名节点(aliases)
别名节点是个很实用的东西。它给其他节点起个短名字,方便引用。
aliases {
serial0 = &uart0;
serial1 = &uart1;
mmc0 = &mmc1;
i2c0 = &i2c0;
};
为什么要用别名?举个例子,U-Boot 启动时需要通过 serial0 找到控制台串口。如果没有别名,你得写一长串路径 /soc/serial@44e09000。有了别名,直接写 serial0 就行。
我的习惯:每个板级.dts里我都会把串口、MMC、I2C、SPI这些常用外设都加上别名。后期调试时少敲很多字。
chosen节点
chosen 节点不描述硬件,它传递的是运行时参数。最典型的用法是指定内核启动参数和控制台。
chosen {
stdout-path = &serial0;
bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rw";
};
stdout-path 告诉内核把日志输出到哪个串口。bootargs 就是内核启动参数。我记得有一次调试,板子死活不打印启动信息,查了半天发现 stdout-path 写成了 serial0 而不是 &serial0——少了个 & 符号,内核根本找不到设备。
注意:chosen节点里的内容由bootloader填充。U-Boot会覆盖.dts里写的bootargs,所以你在.dts里写bootargs可能不生效。调试时记得先检查U-Boot的环境变量。
memory节点
内存节点告诉内核物理内存的起始地址和大小。格式很简单:
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 起始0x80000000,大小512MB
};
这里 reg 的第一个值是基地址,第二个值是长度。为什么是 0x20000000?512MB 嘛。我见过有人把长度写成 0x80000000(2GB),但板子实际只有512MB——内核启动后直接访问不存在的地址,死机。
另外,device_type = "memory" 是历史遗留,新设备树里其实可以省略。但为了兼容性,我建议还是写上。
cpus节点
CPU 节点描述处理器核心。单核、双核、大小核架构,都在这里定义。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
device_type = "cpu";
reg = <0>;
clock-frequency = <800000000>;
};
cpu@1 {
compatible = "arm,cortex-a9";
device_type = "cpu";
reg = <1>;
clock-frequency = <800000000>;
};
};
每个 CPU 核心是一个子节点,reg 的值就是核心编号。#address-cells = <1> 表示地址用 1 个 32 位整数表示,#size-cells = <0> 表示没有长度字段——因为 CPU 编号不需要长度。
为什么 #size-cells 是 0?你想想看,CPU 编号就是一个数字,不像内存地址那样有范围。所以长度字段是多余的。
reg属性——地址描述的核心
reg 属性是设备树里最常用的属性之一。它描述设备的地址空间:基地址和范围。
uart0: serial@44e09000 {
compatible = "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这里 reg = <0x44e09000 0x2000> 表示 UART 寄存器基地址是 0x44e09000,占用 0x2000(8KB)空间。内核驱动通过这个信息做 ioremap。
reg 的格式由父节点的 #address-cells 和 #size-cells 决定。比如:
#address-cells |
#size-cells |
reg 含义 |
|---|---|---|
| 1 | 1 | 基地址(32位) + 长度(32位) |
| 2 | 1 | 基地址(64位) + 长度(32位) |
| 1 | 0 | 只有基地址,没有长度 |
我曾经在一个 64 位 ARM 板子上踩过坑:DDR 地址在 0x800000000(超过 32 位),但 #address-cells 写成了 1,结果高 32 位被截断,内核只认到 0x80000000。嗯,这种 bug 很难查。
interrupts属性
中断属性描述设备连接到哪个中断控制器、使用哪个中断号、触发方式是什么。
gpio0: gpio@44e07000 {
compatible = "ti,omap4-gpio";
reg = <0x44e07000 0x1000>;
interrupts = <96>;
#gpio-cells = <2>;
};
如果系统有多个中断控制器,需要用 interrupt-parent 指定父控制器:
device {
interrupt-parent = &gic;
interrupts = <0 29 4>;
};
这里 <0 29 4> 的含义取决于中断控制器的 #interrupt-cells。对于 ARM GIC:
- 第一个值:中断类型(0=SPI,1=PPI)
- 第二个值:中断号
- 第三个值:触发方式(1=上升沿,4=高电平)
避坑指南:我曾经把 SPI 中断的触发方式写成了 0,结果中断只触发一次就不再响应。查了两天,才发现 GIC 的触发方式不能为 0。电平触发用 4,边沿触发用 1,记住这两个就够了。
compatible属性——驱动匹配的关键
compatible 是设备树里最重要的属性。它把硬件设备和内核驱动联系起来。
compatible = "ti,omap3-uart", "ns16550";
内核驱动通过 of_match_table 匹配这个字符串。匹配顺序是从左到右:先试 ti,omap3-uart,如果没找到驱动,再试 ns16550。
命名规范是 厂商,设备型号。为什么要有厂商前缀?避免命名冲突。比如两家公司都叫 uart,加上厂商名就区分开了。
我建议 compatible 至少写两个值:第一个是精确的芯片型号,第二个是兼容的通用型号。这样即使内核没有你的专用驱动,也能用通用驱动跑起来。
总结一下:设备树语法不复杂,但细节很多。根节点是骨架,aliases 是快捷方式,chosen 传参数,memory 和 cpus 描述核心硬件,reg、interrupts、compatible 是驱动匹配的三板斧。把这些搞明白,大部分设备树你都能看懂、能改。