第2章:设备树基础语法——节点、属性、标签与别名
设备树说白了就是一套描述硬件的“数据结构”。你给它一个树形结构,它告诉内核:这里有个I2C控制器,那里有个GPIO引脚,时钟频率是多少。我刚开始接触设备树时,觉得它跟XML差不多,后来踩了几个坑才发现——语法细节才是魔鬼。
这一章我们就把最基础的四个概念讲透:节点、属性、标签、别名。你把这四个东西搞明白了,后面写设备树就跟写配置文件一样顺手。
2.1 节点(Node)——设备树的骨架
节点是设备树的基本组成单元。每个节点代表一个硬件设备或总线。节点之间是父子关系,形成一棵树。
节点的写法很简单:
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
cpu {
compatible = "arm,cortex-a7";
reg = <0x00 0x100>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
看到没?根节点是 /,里面挂了一个 cpu 节点和一个 memory@80000000 节点。节点名后面可以跟 @地址,用来区分同一层级下相同名字的节点。
节点命名规则:
- 节点名只能包含小写字母、数字、连字符(-)和逗号(,)
- 不能以数字开头
- 如果节点有地址,用
@分隔,地址通常是十六进制 - 同一父节点下,节点名必须唯一
我在项目中遇到过一个问题:两个外设用了相同的节点名,结果内核只识别了最后一个。嗯,从那以后我养成了给节点加地址后缀的习惯,哪怕地址是0。
2.2 属性(Property)——节点的“身份证”
属性就是节点的配置信息。每个属性都有一个名字和一个值。值的类型可以是字符串、整数、字节数组、布尔值等。
常见的属性写法:
uart0: serial@10000000 {
compatible = "fsl,imx6-uart", "ns16550";
reg = <0x10000000 0x4000>;
interrupts = <0 59 4>;
clocks = <&clks IMX6QDL_CLK_UART>;
status = "okay";
dma-names = "rx", "tx";
};
这里我列几个最常用的属性类型:
| 属性类型 | 写法 | 说明 |
|---|---|---|
| 字符串 | "string" |
用双引号包裹 |
| 32位整数 | <0x12345678> |
尖括号包裹,十六进制或十进制 |
| 64位整数 | <0x00000000 0x12345678> |
两个32位拼一个64位 |
| 字节数组 | [01 02 03 04] |
方括号包裹,空格分隔 |
| 布尔值 | 属性名本身 | 存在即为true,不存在为false |
| 字符串列表 | "str1", "str2" |
逗号分隔多个字符串 |
避坑指南:我曾经在 compatible 属性里把厂商名写反了——应该是 "厂商,设备",我写成了 "设备,厂商"。结果内核匹配不到驱动,查了两天才发现。记住:厂商在前,设备在后,这是约定俗成的规矩。
2.3 标签(Label)——给节点起个“外号”
标签是节点的引用名。你可以在节点名前加一个标签,然后在其他地方用 &标签 来引用这个节点。
看个例子:
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x4000>;
};
// 在其他地方引用
&uart0 {
status = "okay";
};
这里 uart0 就是标签。后面用 &uart0 就能找到这个节点,往里面追加属性或修改属性。
标签的命名规则:
- 只能包含字母、数字、下划线
- 不能以数字开头
- 不能是关键字(比如
compatible、reg这些) - 整个设备树里标签必须唯一
注意:标签不是必须的。但如果你需要在其他地方修改这个节点,就必须给它加标签。我习惯给所有外设节点都加标签,哪怕暂时用不到——因为你永远不知道后面会不会需要覆盖某个属性。
2.4 别名(Aliases)——给节点一个“短路径”
别名放在根节点的 aliases 节点里,作用是给一些常用节点起一个简短的名字。内核启动时,有些驱动会通过别名来查找设备。
写法如下:
/ {
aliases {
serial0 = &uart0;
serial1 = &uart1;
mmc0 = &sdhci0;
i2c0 = &i2c1;
};
uart0: serial@10000000 {
// ...
};
uart1: serial@10001000 {
// ...
};
};
别名和标签的区别:
| 对比项 | 标签(Label) | 别名(Aliases) |
|---|---|---|
| 定义位置 | 节点名前 | 根节点下的 aliases 节点 |
| 引用方式 | &标签 |
&别名 或字符串查找 |
| 主要用途 | 在dts文件中引用和修改节点 | 给内核提供固定名称的设备路径 |
| 作用域 | 整个设备树编译单元 | 全局可见,常用于bootloader传递 |
说白了,标签是给开发者用的,别名是给内核用的。你想想看,如果内核启动时想找串口0,它不可能去遍历整个设备树——直接查 aliases 里的 serial0 就搞定了。
2.5 知识体系总览
这一章的内容其实就围绕一个核心:如何用设备树描述硬件。下面这张图帮你理清四个概念之间的关系:
2.6 实战中的常见坑
讲完理论,我分享几个实际项目中踩过的坑:
坑1:标签重复
我曾经在一个包含多个dtsi文件的工程里,两个文件都定义了 uart0 标签。编译时没报错,但运行时串口0死活不工作。后来发现第二个定义覆盖了第一个。记住:标签在整个编译单元里必须唯一,包括所有include进来的文件。
坑2:属性值类型搞错
有一次我把 reg 属性的地址写成了字符串:reg = "0x10000000"。内核解析时直接崩溃。属性值类型必须严格匹配——地址用 <>,字符串用 "",不能混用。
坑3:别名和标签混淆
新手容易把 aliases 当成标签来用。其实别名是给内核用的固定名称,标签是给开发者用的引用名。我建议:所有外设节点都加标签,只在需要给内核提供固定路径时才加别名。
2.7 本章小结
这一章我们讲了四个核心概念:
- 节点:设备树的骨架,每个硬件对应一个节点
- 属性:节点的配置信息,类型要严格匹配
- 标签:给节点起外号,方便在其他地方引用
- 别名:给内核提供固定路径,常用于串口、MMC等标准设备
这些语法看起来简单,但实际项目中80%的设备树问题都出在语法细节上。我建议你动手写几个简单的dts文件,用dtc编译一下,看看报错信息——这是最快的学习方式。