设备树语法精讲:节点与属性、reg与ranges、中断控制器、时钟与GPIO绑定
设备树(Device Tree)这东西,说白了就是一块描述硬件的数据结构。我刚开始接触U-Boot移植时,总觉得它就是个配置文件,后来踩了几个坑才明白——它其实是操作系统和硬件之间的翻译官。你想想看,没有它,内核怎么知道你的CPU连了哪些外设?地址怎么映射?中断怎么路由?
这一章,咱们就把设备树的核心语法掰开揉碎了讲。我会结合自己实际调试过的案例,把节点、属性、reg、ranges、中断、时钟、GPIO这些关键点一一说清楚。
2.1 节点与属性:设备树的基本单元
设备树里最基本的单位就是节点(node)。每个节点代表一个硬件设备或总线。节点里可以嵌套子节点,形成树形结构。每个节点又有一堆属性(property),用来描述设备的特性。
来看个最简单的例子:
/dts-v1/;
/ {
compatible = "myboard,imx6ull";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
uart1: serial@02020000 {
compatible = "fsl,imx6ull-uart", "fsl,imx-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 4>;
clocks = <&clks IMX6ULL_CLK_UART1>;
status = "okay";
};
};
这里有几个关键点:
- 根节点:用
/表示,是整个设备树的起点 - 节点名称:格式是
名称@地址,比如memory@80000000 - compatible:最重要的属性之一,告诉内核这个设备兼容哪个驱动。我习惯把具体型号写在前面,通用型号写在后面,这样驱动匹配时优先级更明确
- status:控制设备是否启用,
"okay"表示启用,"disabled"表示禁用
2.2 reg 与 ranges:地址映射的核心
说到地址映射,reg 和 ranges 是绕不开的两个属性。我见过不少新手在这上面栽跟头,包括我自己。
2.2.1 reg 属性
reg 描述设备的地址空间。它的格式由父节点的 #address-cells 和 #size-cells 决定:
#address-cells:地址占几个32位单元#size-cells:长度占几个32位单元
举个例子:
spi@02018000 {
#address-cells = <1>;
#size-cells = <0>;
reg = <0x02018000 0x4000>;
flash@0 {
reg = <0>; // 片选0
};
};
这里 reg = <0x02018000 0x4000> 表示:基地址 0x02018000,长度 0x4000 字节。子节点的 reg = <0> 表示片选0,因为 #size-cells = <0>,所以不需要长度。
2.2.2 ranges 属性
ranges 用于地址转换,把子节点的地址映射到父节点的地址空间。它的格式是:
ranges = <子地址 父地址 长度>;
看个实际例子:
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x20000000 0x10000000>;
aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
uart1: serial@02020000 {
reg = <0x02020000 0x4000>;
};
};
};
这里 ranges 是空的,表示子地址和父地址一一对应。而 soc 节点的 ranges = <0x0 0x20000000 0x10000000> 表示:子地址 0x0 映射到父地址 0x20000000,长度 0x10000000。
ranges;(空值)或者干脆不写。但如果不写,子节点的地址就无法被父节点解析,驱动里用 of_iomap 会失败。
2.3 中断控制器绑定
中断是嵌入式系统里最头疼的部分之一。设备树里中断的描述涉及三个关键属性:
| 属性 | 作用 | 说明 |
|---|---|---|
interrupt-parent |
指定中断控制器 | 指向中断控制器的 phandle |
interrupts |
描述中断号 | 格式由中断控制器决定 |
interrupt-controller |
声明自己是中断控制器 | 空属性,有就行 |
#interrupt-cells |
中断描述符的单元数 | 通常为 1、2 或 3 |
来看一个典型的中断控制器和中断消费者的例子:
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
uart1: serial@02020000 {
compatible = "fsl,imx6ull-uart", "fsl,imx-uart";
reg = <0x02020000 0x4000>;
interrupt-parent = <&intc>;
interrupts = <0 26 4>;
};
这里 interrupts = <0 26 4> 的三个值分别表示:
- 0:中断类型,0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)
- 26:中断号,对于 SPI 来说就是 32 + 26 = 58
- 4:触发方式,4 表示高电平触发
cat /proc/interrupts 看看中断有没有注册上。如果注册了但不触发,八成是中断号或者触发方式写错了。我会对照芯片手册,把 interrupts 的三个值一个一个核对。
2.4 时钟绑定
时钟系统在设备树里也是个容易出幺蛾子的地方。时钟提供者(clock provider)和时钟消费者(clock consumer)通过 clocks 和 clock-names 关联。
先看时钟提供者:
clks: clock-controller@020c4000 {
compatible = "fsl,imx6ull-ccm";
reg = <0x020c4000 0x4000>;
#clock-cells = <1>;
clocks = <&osc 0>, <&rtc 0>;
clock-names = "osc", "rtc";
};
再看时钟消费者:
uart1: serial@02020000 {
compatible = "fsl,imx6ull-uart", "fsl,imx-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 4>;
clocks = <&clks IMX6ULL_CLK_UART1>;
clock-names = "uart";
};
这里 #clock-cells = <1> 表示时钟提供者需要 1 个参数来指定具体的时钟输出。IMX6ULL_CLK_UART1 就是在头文件里定义的宏,对应 UART1 的时钟索引。
2.5 GPIO 绑定
GPIO 在设备树里的描述方式,跟时钟和中断类似,也是提供者-消费者模式。GPIO 控制器用 gpio-controller 声明自己,用 #gpio-cells 指定描述一个 GPIO 需要几个参数。
来看 GPIO 控制器的定义:
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ull-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <0 66 4>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
GPIO 消费者的写法:
led {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led0 {
label = "heartbeat";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
linux,default-trigger = "heartbeat";
};
};
这里 gpios = <&gpio1 3 GPIO_ACTIVE_LOW> 的三个参数:
- &gpio1:指向 GPIO 控制器的 phandle
- 3:GPIO 引脚号,这里是 GPIO1_IO03
- GPIO_ACTIVE_LOW:低电平有效,这个宏定义在
dt-bindings/gpio/gpio.h里
2.6 知识结构总览
说了这么多,咱们用一张图把设备树的核心语法串起来:
2.7 实战经验总结
说了这么多语法,最后分享几个我在实际项目中总结的经验:
- 先看芯片手册,再写设备树:我见过太多人直接抄其他板子的设备树,结果地址不对、中断号不对,调试起来更费时间。芯片手册里的内存映射表、中断号表才是第一手资料。
- 善用 dtc 工具:写完设备树后,用
dtc -I dts -O dtb -o test.dtb test.dts编译,再用dtc -I dtb -O dts test.dtb反编译,看看有没有语法错误。这个习惯帮我省了不少时间。 - 从简单开始:新板子第一次跑,我通常只保留 UART 和内存节点,其他全部 disabled。等串口能打印了,再逐个打开外设。这样出了问题,范围很小,好定位。
- 注意 phandle 的引用:
&intc、&clks这种引用,一定要确保被引用的节点存在,而且 phandle 标签没有冲突。我曾经在两个头文件里定义了相同的标签,编译不报错,但运行时引用指向了错误的节点。
fdt list 查看设备树,用 fdt get value 读取某个属性的值。我经常在 U-Boot 命令行里先验证设备树是否正确,再启动内核。这样能避免内核启动后才发现设备树有问题。
嗯,设备树的语法其实不难,难的是把每个属性跟芯片手册里的硬件特性对应起来。多写几次,多踩几个坑,自然就熟练了。