2. 设备树(DTS)基础语法:节点、属性、标签、包含文件
设备树源文件(.dts)是一种树形结构的文本描述语言,用于向操作系统(尤其是Linux内核)描述不可枚举的硬件信息。在ARM64启动过程中,Bootloader(如U-Boot)会将编译后的设备树二进制(.dtb)传递给内核,内核解析该结构以完成内存映射、外设驱动匹配等关键初始化步骤。
本章将系统讲解DTS的核心语法要素:节点、属性、标签与包含文件,并给出严格的书写规则。
2.1 节点(Node):设备树的骨架
设备树由一系列节点组成,每个节点代表一个硬件设备、总线或控制器。节点从根节点 / 开始,形成层级结构。
语法规则:节点名称格式为
node-name@unit-address。node-name 描述设备类型(如 uart, i2c),unit-address 通常为设备在父总线上的起始地址(如寄存器基址),用于区分同名节点。
// 根节点
/ {
// 子节点:CPU
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a72";
reg = <0x0>;
};
cpu@1 {
compatible = "arm,cortex-a72";
reg = <0x1>;
};
};
// 子节点:内存
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>; // 2GB RAM
};
// 子节点:串口
uart@fe001000 {
compatible = "ns16550a";
reg = <0x0 0xfe001000 0x0 0x1000>;
interrupts = <0x0 0x1a 0x4>;
};
};
调试提示:在启动早期,内核通过
of_scan_flat_dt() 递归遍历节点树。如果节点名称中的 unit-address 与 reg 属性中的地址不匹配,可能导致设备无法被正确识别。
2.2 属性(Property):描述设备的元数据
属性是附着在节点上的键值对,用于描述设备的特性、地址、中断、时钟等信息。属性值可以是空、整数、字符串、字节数组或它们的组合。
| 属性类型 | 语法示例 | 说明 |
|---|---|---|
| 空属性 | status = "disabled"; |
布尔标志,存在即表示真 |
| 32位整数 | reg = <0xfe001000 0x1000>; |
尖括号包裹,空格分隔 |
| 64位整数 | reg = <0x0 0x80000000 0x0 0x80000000>; |
高32位在前,低32位在后 |
| 字符串 | compatible = "arm,cortex-a72"; |
双引号包裹 |
| 字符串列表 | compatible = "ns16550a", "ns16550"; |
逗号分隔多个字符串 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号包裹,十六进制字节 |
| 混合结构 | interrupts = <0x0 0x1a 0x4>; |
可包含整数、字符串组合 |
常见错误:在ARM64平台,
#address-cells 和 #size-cells 必须正确设置。对于64位地址空间,通常设置为 <2> 和 <2>,否则 reg 属性解析会截断高32位地址,导致内存映射错误。
2.3 标签(Label):节点的引用别名
标签为节点提供一个全局唯一的别名,便于在其他位置引用该节点。标签以冒号结尾,放在节点名称之前。
// 定义标签
uart0: uart@fe001000 {
compatible = "ns16550a";
reg = <0x0 0xfe001000 0x0 0x1000>;
interrupts = <0x0 0x1a 0x4>;
};
// 通过 & 引用标签,追加或修改属性
&uart0 {
clock-frequency = <1843200>;
status = "okay";
};
调试技巧:在启动日志中搜索
OF: fdt 或 of_node 可以查看内核解析节点时的标签匹配情况。如果标签引用失败,通常表现为 of_find_node_by_name() 返回 NULL。
2.4 包含文件(Include):模块化与复用
设备树支持使用 #include 预处理指令引入其他文件,实现硬件描述的模块化。常用包含文件包括:.dtsi(设备树包含文件)、.h(头文件,定义宏常量)。
// 主文件:board.dts
#include "soc.dtsi" // 引入SoC公共定义
#include "board-common.dtsi" // 引入板级公共定义
#include "pinmux.h" // 引入引脚复用宏
/ {
model = "ARM64 Development Board v2";
compatible = "arm,devboard-v2", "arm,devboard";
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>;
};
};
// 覆盖 soc.dtsi 中定义的节点
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
};
包含文件搜索路径:编译器(dtc)默认在以下路径查找包含文件:
- 当前文件所在目录
-i选项指定的目录- 系统预置的
include/dt-bindings/目录
2.5 设备树书写规则总结
| 规则类别 | 具体要求 | 违反后果 |
|---|---|---|
| 节点命名 | 通用名@地址,地址与reg一致 | 设备无法匹配驱动 |
| compatible | 优先使用"厂商,型号"格式 | 驱动probe失败 |
| reg 与地址单元 | #address-cells/#size-cells 必须匹配 | 内存映射偏移错误 |
| 标签唯一性 | 全局不可重复 | 编译报错或引用混乱 |
| 包含文件 | 使用 #include,避免重复定义 | 节点覆盖顺序不可控 |
| 缩进与格式 | 使用Tab缩进,分号结尾 | dtc编译警告或错误 |
2.6 实战:编写一个最小设备树
以下是一个可用于QEMU virt平台的ARM64最小设备树示例,包含必要的节点和属性:
/dts-v1/;
/ {
model = "QEMU ARM64 Virtual Platform";
compatible = "qemu,virt";
#address-cells = <2>;
#size-cells = <2>;
// 内存节点
memory@40000000 {
device_type = "memory";
reg = <0x0 0x40000000 0x0 0x40000000>; // 1GB
};
// CPU 节点
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a57";
reg = <0x0>;
device_type = "cpu";
};
};
// 系统级中断控制器
gic: interrupt-controller@8000000 {
compatible = "arm,gic-v3";
reg = <0x0 0x08000000 0x0 0x10000>,
<0x0 0x080a0000 0x0 0x20000>;
interrupt-controller;
#interrupt-cells = <3>;
};
// 串口
uart0: uart@9000000 {
compatible = "ns16550a";
reg = <0x0 0x09000000 0x0 0x1000>;
interrupts = <0x0 0x1 0x4>;
interrupt-parent = <&gic>;
clock-frequency = <3686400>;
};
chosen {
bootargs = "console=ttyAMA0 earlycon=uart,mmio,0x09000000";
stdout-path = &uart0;
};
};
编译命令:
dtc -I dts -O dtb -o board.dtb board.dtsdtc -I dtb -O dts -o board_decompiled.dts board.dtb (反编译验证)