设备树语法基础:DTS文件结构、节点和属性、数据类型、标签和引用、包含文件
设备树(Device Tree)说白了就是一块「硬件描述板」。你写进去什么外设、什么地址、什么中断,内核启动时就读什么。我刚开始接触Yocto时,总觉得设备树是内核的事,跟构建系统没关系。后来被坑了几次才明白——设备树写不对,整个系统都起不来。
这一章我们就把设备树的语法基础掰开揉碎。你掌握了这些,后面做Yocto集成才不会手忙脚乱。
4.1 DTS文件结构:从根节点往下看
一个标准的DTS文件,结构其实很简单。它就是一个树形结构,从根节点 / 开始,往下挂各种子节点。每个节点描述一个硬件设备或总线。
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
leds {
pinctrl-names = "default";
pinctrl-0 = <&led_pins>;
led@0 {
label = "beaglebone:green:usr0";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
你看,根节点下面有 chosen、memory、leds 这些子节点。每个节点用花括号包裹,末尾加分号。节点名后面可以跟 @地址,比如 memory@80000000,表示这个内存的基地址是0x80000000。
4.2 节点和属性:设备树的「名词」和「形容词」
节点就是硬件设备,属性就是描述这个设备的参数。你可以把节点想象成「名词」,属性就是「形容词」。
每个节点必须有一个 compatible 属性,告诉内核这个设备用什么驱动。格式是 "厂商,型号"。比如 "ti,am335x-bone-black",内核匹配驱动时,会从最具体的开始试,不行再试通用的。
常用的属性我列个表,你写设备树时基本都会用到:
| 属性名 | 作用 | 示例 |
|---|---|---|
compatible |
驱动匹配字符串 | "ti,am335x-uart" |
reg |
寄存器地址和大小 | <0x44e09000 0x2000> |
interrupts |
中断号与触发方式 | <0 56 4> |
clocks |
时钟引用 | <&clk_uart0> |
status |
设备启用/禁用 | "okay" 或 "disabled" |
pinctrl-0 |
引脚复用配置 | <&uart0_pins> |
status = "okay"。内核默认是 disabled,结果SPI设备死活不工作。查了两天才发现是这个问题。所以,新加的节点一定要显式设置 status。
4.3 数据类型:u32、u64、字符串和数组
设备树里的数据类型不多,但每个都有讲究。我直接给你总结:
- 整数(u32):用尖括号包裹,比如
<0x1000>。多个整数用空格隔开:<0x1000 0x2000>。 - 64位整数(u64):用两个u32表示,比如
<0x00000000 0x80000000>表示0x80000000。 - 字符串:用双引号包裹,比如
"okay"。 - 字符串列表:用逗号分隔,比如
"ti,am335x-bone", "ti,am33xx"。 - 字节数组:用方括号包裹,比如
[00 01 02 03]。 - 混合类型:用
<>和[]组合,比如<0x1000> [00 01]。
你可能会问:为什么64位地址要用两个u32?因为设备树编译器(DTC)默认按32位处理。如果你用 #address-cells = <2>,那地址就是64位的。这个后面讲地址映射时会细说。
4.4 标签和引用:别写死,用「指针」
设备树里最让我觉得舒服的设计就是标签(label)和引用(phandle)。你想想看,如果每个节点都写全路径,比如 /soc/spi@48030000,那改个名字或层级,所有引用都得改。多麻烦。
标签的写法很简单,在节点名前加个名字和冒号:
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这里 uart0 就是标签。其他地方想引用这个节点,用 &uart0 就行:
chosen {
stdout-path = &uart0;
};
编译器会把 &uart0 自动替换成对应的phandle(一个整数指针)。这样既清晰又灵活。
uart0、i2c0、spi0、mmc0。命名规则统一用「外设名+编号」。这样在Yocto的bbappend文件里覆盖节点时,直接 &uart0 { ... }; 就搞定了,不用写一长串路径。
4.5 包含文件:dtsi和dts的分工
一个完整的设备树,不可能全写在一个文件里。那样太乱了。实际项目中,我们会把通用的部分放在 .dtsi 文件里,把板级特有的部分放在 .dts 文件里。
.dtsi 就像C语言的头文件。它定义SoC通用的外设、时钟、中断控制器等。.dts 则包含具体的板级配置——内存大小、引脚复用、外设使能等。
包含的语法和C语言一样:
// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
};
&uart0 {
status = "okay";
};
&mmc1 {
status = "okay";
vmmc-supply = <&vmmcsd_fixed>;
};
你看,am33xx.dtsi 里定义了UART、MMC、I2C等外设节点,但状态都是 disabled。板级文件里用 &uart0 { status = "okay"; }; 来启用。这就是设备树的「增量覆盖」机制。
.dtsi,板级厂商提供 .dts。你在Yocto里做定制,通常只需要修改或追加 .dts 文件,不要动 .dtsi。这样升级内核时,SoC的通用配置自动更新,你的板级改动不受影响。
4.6 知识体系总览
说了这么多,我画张图帮你把整个知识体系串起来。你一看就明白设备树语法各部分之间的关系了。
这张图把设备树语法的五个核心模块串起来了。你写设备树时,脑子里要有这个结构:先搭文件框架(dtsi/dts),再定义节点和属性,用标签做引用,用正确的数据类型填值。每一步都有章可循。
arch/arm/boot/dts/ 目录,找一个你熟悉的板子,一行一行读。读懂了再改,改通了再自己写。我当年就是这么过来的。