第二课:设备树基础语法——节点、属性、标签、别名、包含与宏
各位同学,欢迎来到设备树学习的第二站。上一章我们聊了设备树是干什么的,今天咱们就动手拆解它的语法结构。说白了,设备树就是一个描述硬件信息的文本文件,但它的语法规则很严谨,写错了内核就不认账。
我个人习惯把设备树比作一张「硬件清单」。你想想看,你要给一个嵌入式板子写驱动,总得告诉内核:CPU 是什么、内存多大、I2C 控制器挂在哪、GPIO 引脚怎么接……这些信息,全写在设备树里。而节点和属性,就是这张清单上的「条目」和「具体参数」。
一、节点(Node)与属性(Property)
节点是设备树的基本组成单元。每个节点代表一个硬件设备或总线,用花括号 {} 包裹。属性则是节点的具体描述,比如地址、中断号、时钟频率等。
来看一个最简单的例子:
/dts-v1/;
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard";
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
};
这里 / 是根节点,代表整个板子。根节点下面有两个子节点:cpu@0 和 memory@80000000。每个节点里都有属性,比如 compatible、reg。属性值可以是字符串、数字、数组等。
核心要点:
- 节点名格式:
名称@地址(地址可选,用于区分同类型设备) - 属性名小写,值用双引号或尖括号包裹
reg属性描述地址和大小,格式为<起始地址 长度>
我在项目中遇到过一个问题:有同事把 reg 的地址写成了十进制,结果内核解析时直接崩溃。嗯,这里要注意,reg 里的数字默认是十六进制,别写成 0x80000000 以外的格式。
二、标签(Label)与别名(Alias)
标签和别名,说白了就是给节点起个「外号」。为什么要这么做?因为设备树里节点名可能很长,比如 soc/spi@ff110000,每次引用都写全路径太痛苦了。
标签的写法很简单,在节点名前加个冒号:
spi0: spi@ff110000 {
compatible = "vendor,spi-controller";
reg = <0xff110000 0x1000>;
};
这样,spi0 就成了这个节点的标签。在其他地方引用时,可以用 &spi0 来指向它。比如在板级文件里覆盖属性:
&spi0 {
status = "okay";
clock-frequency = <1000000>;
};
别名则是在根节点下的 aliases 节点里定义的:
/ {
aliases {
spi0 = &spi0;
serial0 = &uart0;
};
};
我个人习惯用标签来引用节点,因为更直观。别名主要用于一些历史遗留的驱动,或者需要给用户空间提供固定名称的场景。
避坑指南:我曾经在写设备树时,给两个不同的节点起了相同的标签,结果编译报错说「重复定义」。标签在整个设备树里必须是唯一的,这点和 C 语言的变量名一样。
三、包含文件(#include)与 dtsi
一个复杂的板子,设备树可能有上千行。如果所有内容都写在一个文件里,维护起来简直是噩梦。所以设备树支持 #include 预处理指令,和 C 语言一样。
.dtsi 文件是设备树的「头文件」,通常存放 SoC 公共部分,比如 CPU 核心、中断控制器、时钟等。而 .dts 文件是具体的板级文件,只写板子特有的内容。
举个例子:
// myboard.dts
#include "soc.dtsi"
#include "myboard-pinctrl.dtsi"
/ {
model = "MyBoard v2.0";
compatible = "vendor,myboard";
&uart0 {
status = "okay";
baud-rate = <115200>;
};
};
而 soc.dtsi 里定义了 SoC 的公共节点:
// soc.dtsi
/ {
uart0: uart@ff100000 {
compatible = "vendor,uart";
reg = <0xff100000 0x1000>;
interrupts = <0 33 4>;
status = "disabled";
};
};
这样,不同板子只需要包含同一个 soc.dtsi,然后覆盖自己需要的节点即可。你想想看,如果每个板子都重复写一遍 UART 的定义,那得多累?
最佳实践:
- SoC 公共部分放
.dtsi,板级特有部分放.dts - 用
#include而不是/include/(后者是旧语法,不推荐) - 覆盖节点时用
&标签语法,不要重新定义整个节点
四、注释与宏定义
设备树的注释和 C 语言一样,支持两种风格:
// 单行注释/* 多行注释 */
我建议在设备树里多写注释,尤其是那些「为什么这么配」的地方。比如:
// 这个 GPIO 必须拉高,否则 PMIC 不启动
gpio-hog {
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
output-high;
};
宏定义则依赖 C 预处理器。设备树文件经过 cpp(C 预处理器)处理后才被编译,所以你可以用 #define 定义常量:
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1
/ {
led {
gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>;
};
};
不过要注意,宏定义只在 .dts 文件里有效,.dtsi 里如果用了宏,必须在包含之前定义好。我曾经踩过这个坑:在 soc.dtsi 里用了 IRQ_TYPE_EDGE_RISING,但忘记在板级文件里 #define,结果编译报错说宏未定义。
警告:不要滥用宏定义。设备树是描述硬件的,不是写算法的。如果宏定义太多,反而降低了可读性。我个人只在 GPIO 电平、中断类型等少量场景使用宏。
知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
好了,这一章的内容就到这里。设备树的语法其实不复杂,关键是多写多练。你可以在自己的开发板上试着写一个简单的设备树,从根节点开始,慢慢添加 CPU、内存、串口等节点。遇到问题不要怕,编译器的报错信息会告诉你哪里写错了。
记住,设备树是给内核看的「硬件说明书」,写清楚、写规范,后面调试驱动时会省很多事。
小练习:试着写一个包含两个 LED 节点的设备树,每个 LED 占用一个 GPIO,并用标签和别名分别引用它们。编译通过后,你就算入门了。