2. 设备树基础:设备树(DTS)语法、节点、属性、标签、包含文件
设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点来定义CPU、内存、外设、中断控制器、时钟等硬件组件及其连接关系。在嵌入式Linux系统中,设备树源文件(.dts)经过编译生成设备树二进制文件(.dtb),由Bootloader传递给内核,内核解析后完成设备驱动与硬件资源的绑定。
2.1 设备树源文件结构
一个典型的设备树源文件由以下部分组成:
- 根节点:使用斜杠
/表示,是整个设备树的起点。 - 节点:描述一个硬件设备或总线,格式为
node-name@unit-address。 - 属性:键值对,描述节点的特性,如地址、中断号、时钟频率等。
- 标签:为节点或属性起别名,便于在其它位置引用。
- 包含文件:通过
#include引入公共头文件或其它设备树片段。
关键概念:设备树的核心思想是将硬件描述与内核代码分离,同一份内核镜像可以通过更换设备树文件来支持不同的硬件平台。
2.2 DTS语法详解
设备树源文件使用一种类C语言的语法,但更简洁。以下是一个完整的设备树示例:
/dts-v1/;
#include "soc-common.dtsi"
/ {
model = "Embedded Development Board v2.0";
compatible = "vendor,devboard-v2", "vendor,devboard";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; /* 1GB RAM */
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a72";
reg = <0x0>;
clock-frequency = <2000000000>;
};
cpu@1 {
compatible = "arm,cortex-a72";
reg = <0x1>;
clock-frequency = <2000000000>;
};
};
uart0: serial@1c090000 {
compatible = "vendor,uart";
reg = <0x1c090000 0x1000>;
interrupts = <0 33 4>;
clocks = <&uart_clk>;
status = "okay";
};
};
2.3 节点定义与命名规则
节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线或功能块。节点名称由两部分组成:
| 组成部分 | 说明 | 示例 |
|---|---|---|
| 节点名 | 描述设备功能的通用名称,应尽量使用标准名称 | serial, i2c, gpio |
| 单元地址 | 设备的基地址或片选号,用于区分同名节点 | 1c090000, 0 |
| 完整格式 | 节点名@单元地址 | serial@1c090000 |
最佳实践:节点名应使用小写字母,单词间用连字符分隔。单元地址使用十六进制小写,不带
0x 前缀。
2.4 属性类型与编码
属性是节点中描述硬件特性的键值对。设备树规范定义了多种属性类型:
| 属性类型 | 语法格式 | 示例 | 说明 |
|---|---|---|---|
| 字符串 | "string" |
compatible = "vendor,uart"; |
用于标识设备兼容性 |
| 32位整数 | <value> |
reg = <0x1c090000 0x1000>; |
地址、大小、中断号等 |
| 64位整数 | <high low> |
reg = <0x0 0x80000000 0x0 0x40000000>; |
64位地址空间 |
| 字节数组 | [hex bytes] |
local-mac-address = [00 11 22 33 44 55]; |
MAC地址、UUID等 |
| 布尔值 | 仅属性名,无值 | dma-coherent; |
表示属性为真 |
| 字符串列表 | "str1", "str2" |
compatible = "vendor,devboard-v2", "vendor,devboard"; |
按优先级排列的兼容列表 |
| phandle | &label |
clocks = <&uart_clk>; |
引用其它节点 |
2.5 标签(Label)与引用
标签为节点提供一个别名,可以在设备树其它位置通过 &label 引用该节点。标签通常用于:
- 中断连接:引用中断控制器节点
- 时钟引用:引用时钟源节点
- GPIO引用:引用GPIO控制器节点
- 覆盖(Overlay):在设备树覆盖文件中定位目标节点
/* 定义标签 */
uart0: serial@1c090000 {
compatible = "vendor,uart";
reg = <0x1c090000 0x1000>;
interrupts = <0 33 4>;
interrupt-parent = <&gic>;
clocks = <&uart_clk>;
};
/* 在其它位置引用标签 */
&uart0 {
status = "okay";
clock-frequency = <1843200>;
};
注意:标签名不能与节点名冲突,且在整个设备树中必须唯一。标签通常使用小写字母加下划线命名。
2.6 包含文件(#include)
设备树支持使用C预处理器指令 #include 来包含其它文件,实现代码复用。常见的包含文件类型:
| 文件类型 | 扩展名 | 用途 |
|---|---|---|
| 设备树包含文件 | .dtsi |
SoC公共定义、外设模板 |
| 头文件 | .h |
宏定义、中断号、时钟ID等常量 |
| 设备树覆盖 | .dtso |
运行时硬件扩展 |
/* soc-common.dtsi - SoC公共定义 */
/ {
gic: interrupt-controller@1c010000 {
compatible = "arm,gic-400";
reg = <0x1c010000 0x1000>,
<0x1c020000 0x2000>;
interrupt-controller;
#interrupt-cells = <3>;
};
uart_clk: clock@1c030000 {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>;
};
};
2.7 设备树节点关系图
下图展示了一个典型嵌入式系统的设备树节点层次结构:
2.8 标准属性详解
设备树规范定义了一系列标准属性,用于描述硬件资源的通用特性:
| 属性名 | 适用节点 | 说明 |
|---|---|---|
compatible |
所有设备节点 | 设备驱动匹配字符串,格式为 "厂商,设备型号" |
reg |
有地址空间的设备 | 描述设备地址范围,格式为 <基地址 长度> |
interrupts |
产生中断的设备 | 中断号、触发类型等,格式由中断控制器定义 |
interrupt-parent |
所有设备节点 | 指定中断控制器节点的phandle |
clocks |
需要时钟的设备 | 引用时钟源节点的phandle列表 |
status |
所有设备节点 | 设备状态:okay, disabled, reserved |
#address-cells |
总线节点 | 子节点地址字段占用的32位字数量 |
#size-cells |
总线节点 | 子节点大小字段占用的32位字数量 |
device_type |
memory、cpu等 | 设备类型标识,主要用于兼容IEEE 1275 |
model |
根节点 | 板级型号名称 |
2.9 地址编码与ranges属性
在总线节点中,#address-cells 和 #size-cells 定义了子节点地址空间的编码方式。ranges 属性用于将子节点的地址空间映射到父节点的地址空间:
/ {
#address-cells = <2>; /* 64位地址 */
#size-cells = <2>; /* 64位大小 */
soc@0 {
compatible = "simple-bus";
#address-cells = <1>; /* 32位地址 */
#size-cells = <1>; /* 32位大小 */
ranges = <0x0 0x0 0x1c000000 0x1000000>; /* 子地址0x0 -> 父地址0x1c000000 */
uart0: serial@1c090000 {
reg = <0x1c090000 0x1000>; /* 子地址空间中的地址 */
};
};
};
地址映射规则:
ranges = <子地址 父地址 长度>。当 ranges 属性为空时,表示子地址与父地址一一对应;当 ranges 属性不存在时,子节点不能直接使用父地址空间。
2.10 设备树编译与反编译
设备树源文件需要通过设备树编译器(dtc)转换为二进制格式:
| 操作 | 命令 | 说明 |
|---|---|---|
| 编译 | dtc -I dts -O dtb -o board.dtb board.dts |
将DTS编译为DTB |
| 反编译 | dtc -I dtb -O dts -o board.dts board.dtb |
将DTB反编译为DTS |
| 检查 | dtc -I dtb -O dts board.dtb | head -50 |
快速查看DTB内容 |
| 生成符号 | dtc -I dts -O dtb -@ -o board.dtb board.dts |
生成符号节点,支持覆盖 |
2.11 设备树覆盖(Overlay)
设备树覆盖允许在不修改原始设备树的情况下,动态添加或修改硬件描述。覆盖文件使用 .dtso 扩展名,通过 &label 语法定位目标节点:
/* overlay-example.dtso */
/dts-v1/;
/plugin/;
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
pinctrl-names = "default";
};
&i2c0 {
status = "okay";
clock-frequency = <400000>;
temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
覆盖应用场景:设备树覆盖常用于FPGA动态加载、扩展板支持、调试时临时修改硬件配置等场景,无需重新编译整个设备树。
2.12 常见错误与调试技巧
- 语法错误:缺少分号、括号不匹配、属性值格式错误。使用
dtc -I dts -O dtb编译时检查。 - 地址冲突:多个设备使用相同的地址范围。检查
reg属性是否重叠。 - 中断号错误:中断号与中断控制器不匹配。确认
#interrupt-cells定义。 - 标签未定义:引用了不存在的标签。检查标签名拼写和包含文件。
- 兼容字符串不匹配:驱动中的
of_match_table与设备树中的compatible不一致。
调试建议:在Linux内核启动时添加
earlycon 和 dtdebug 参数,可以打印设备树解析过程中的详细信息,帮助定位硬件描述问题。
2.13 本章小结
设备树是嵌入式Linux系统中硬件描述的核心机制,掌握DTS语法、节点定义、属性编码、标签引用和包含文件管理是进行设备驱动开发的基础。通过本章学习,你应该能够:
- 编写符合规范的设备树源文件
- 理解节点层次结构和地址映射关系
- 使用标签实现节点引用和覆盖
- 通过包含文件实现代码复用
- 使用dtc工具进行编译和调试