第四节 设备树语法入门:节点、属性、值、字符串、整数、数组、引用等基本语法元素
设备树(Device Tree)是一种描述硬件资源的数据结构,它通过一种树状的层级关系,将CPU、内存、外设控制器、总线、板级器件等硬件信息以节点(Node)和属性(Property)的形式组织起来。对于嵌入式Linux开发者而言,掌握设备树的基本语法是进行驱动开发、内核移植和板级调试的前提。本节将围绕设备树的语法核心,深入剖析节点、属性、值、字符串、整数、数组以及引用等基本元素,帮助读者建立扎实的语法基础。
上图展示了设备树的基本层级结构。根节点“/”是所有节点的祖先,其下可以挂载多个子节点,如 cpus、memory、soc 和板级设备节点。每个节点又可以包含子节点,从而形成一棵描述完整硬件拓扑的树。设备树的核心价值在于将硬件配置信息从内核代码中剥离出来,通过 Bootloader(如 U-Boot)传递给内核,实现“硬件描述与内核代码分离”的设计思想。
4.1 节点(Node)与属性(Property)的核心原理
节点是设备树的基本组成单元,用于表示一个硬件设备、总线、控制器或简单的逻辑分组。节点通过路径唯一标识,例如 /soc/i2c@1000 表示根节点下的 soc 节点中的 i2c@1000 节点。节点名称通常包含设备类型和单元地址(如 i2c@1000),其中 @1000 用于区分同一类型的不同实例。
属性是附着在节点上的键值对,用于描述节点的具体特征。属性值可以是空、字符串、整数、字节串、数组等。常见的属性包括 compatible(兼容性标识)、reg(寄存器地址和长度)、interrupts(中断信息)、status(设备状态)等。内核通过解析这些属性来初始化对应的驱动程序。
核心原理:设备树语法遵循“节点-属性”模型。节点代表硬件对象,属性代表对象的参数。内核在启动时通过 of_* API(如 of_find_node_by_path、of_property_read_u32)遍历设备树,获取硬件资源信息并绑定驱动。这种机制使得同一份内核镜像可以支持多种硬件平台,只需更换设备树源文件(.dts)即可。
4.2 值类型与编码规则
设备树中的属性值有多种数据类型,理解这些类型是正确编写和解析设备树的关键。下表总结了常见的数据类型及其编码方式:
| 数据类型 | 语法表示 | 说明 | 示例 |
|---|---|---|---|
| 字符串 | "string" | 以双引号括起的文本,用于描述设备名称、兼容性等 | compatible = "arm,cortex-a9"; |
| 整数(32位) | <0x1234> | 尖括号内为32位无符号整数,支持十进制或十六进制 | reg = <0x1000 0x100>; |
| 64位整数 | <0x00000000 0x80000000> | 使用两个32位整数拼接表示64位值 | reg = <0x0 0x80000000 0x0 0x10000000>; |
| 字节串 | [0x01 0x02 0x03] | 方括号内为十六进制字节序列,用于MAC地址等 | local-mac-address = [00 11 22 33 44 55]; |
| 数组 | <0x1 0x2 0x3> | 尖括号内多个整数,表示一组值 | clock-frequency = <1000000 2000000>; |
| 布尔值 | 空属性 | 属性存在即为真,不存在为假 | status = "disabled"; 或省略该属性 |
| 引用(phandle) | <&node> | 引用其他节点的句柄,用于建立节点间关联 | interrupt-parent = <&intc>; |
在编写设备树时,需要注意整数默认是32位,若需要64位地址或大小,必须拆分为两个32位整数。此外,#address-cells 和 #size-cells 属性用于定义 reg 属性中地址和长度的单元长度,这是解析 reg 的关键。
4.3 引用(Reference)与标签(Label)
设备树中的引用机制允许一个节点通过 &label 语法引用另一个节点,从而实现节点间的关联。标签(Label)是在节点定义时附加的标识符,例如:
/ {
intc: interrupt-controller@1000 {
compatible = "arm,cortex-a9-gic";
reg = <0x1000 0x1000>;
interrupt-controller;
#interrupt-cells = <3>;
};
uart0: serial@2000 {
compatible = "ns16550a";
reg = <0x2000 0x100>;
interrupts = <0 5 4>;
interrupt-parent = <&intc>;
};
};
在上述代码中,intc: 和 uart0: 是标签,&intc 是对中断控制器节点的引用。这种机制在中断映射、时钟引用、GPIO控制等场景中极为常见。引用不仅简化了设备树的书写,还避免了硬编码路径带来的维护困难。
经验提醒:在编写设备树时,建议为所有重要节点添加有意义的标签(如 uart0、i2c1),这样在驱动代码中可以通过 of_find_node_by_label() 快速定位节点,提高代码可读性。
4.4 案例拆解:从零构建一个简单的设备树
为了将上述语法知识融会贯通,我们以一个典型的嵌入式板卡为例,构建一个包含CPU、内存、UART和GPIO控制器的设备树片段。假设硬件环境如下:
- CPU:ARM Cortex-A9 双核,主频1GHz
- 内存:512MB DDR3,起始地址0x80000000
- UART0:地址0x10000000,中断号29(SPI类型)
- GPIO控制器:地址0x10001000,支持32个GPIO
对应的设备树源文件(.dts)片段如下:
/dts-v1/;
/ {
model = "ExampleBoard Rev.1";
compatible = "vendor,example-board", "arm,cortex-a9";
#address-cells = <1>;
#size-cells = <1>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
reg = <0>;
clock-frequency = <1000000000>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB */
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x100>;
interrupts = <0 29 4>;
interrupt-parent = <&intc>;
clock-frequency = <24000000>;
status = "okay";
};
intc: interrupt-controller@10002000 {
compatible = "arm,cortex-a9-gic";
reg = <0x10002000 0x1000>;
interrupt-controller;
#interrupt-cells = <3>;
};
gpio0: gpio@10001000 {
compatible = "vendor,gpio-controller";
reg = <0x10001000 0x200>;
gpio-controller;
#gpio-cells = <2>;
};
};
};
在这个案例中,我们定义了根节点及其子节点。关键点包括:
- 根节点使用
model和compatible描述板卡型号和兼容性。 - CPU节点通过
reg = <0>表示CPU0,clock-frequency描述主频。 - 内存节点使用
reg描述起始地址和大小,注意#address-cells和#size-cells均为1,因此地址和长度各占一个32位整数。 - UART节点通过
interrupt-parent引用中断控制器,interrupts描述中断类型(0表示SPI)、中断号和触发方式(4表示高电平触发)。 - GPIO节点定义了
gpio-controller属性和#gpio-cells,为其他设备引用GPIO提供接口。
通过这个案例,读者可以直观地看到节点、属性、值、引用等语法元素如何协同工作,构建出内核可识别的硬件描述。
4.5 常见误区与总结复盘
在设备树语法学习过程中,初学者容易陷入以下误区:
风险提示:
- 忽略
#address-cells和#size-cells:这两个属性决定了reg中地址和长度的位数。若父节点和子节点设置不一致,会导致地址解析错误,驱动无法正确获取资源。 - 字符串与整数混淆:例如将
compatible写成<"vendor,device">,这是错误的,字符串应直接使用双引号。 - 引用未定义标签:使用
&label时,若标签未定义,编译会报错。务必确保标签在设备树中唯一且已声明。 - 忘记
status = "disabled"的语义:当属性值为"disabled"时,内核不会激活该设备;若省略该属性,默认视为"okay"。但某些驱动可能要求显式声明。
总结复盘:设备树语法虽然简单,但细节决定成败。掌握节点、属性、值类型和引用机制,是编写高质量设备树的基础。建议读者在实战中多阅读内核自带的 .dts 文件(如 arch/arm/boot/dts/ 目录下的文件),模仿其写法,并利用 dtc 工具(Device Tree Compiler)进行编译和反编译,验证语法的正确性。通过不断实践,逐步建立起对设备树语法的直觉和熟练度。