2. 设备树语法入门:节点、属性、标签、包含文件与宏定义
设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点(node)和键值对属性(property)来描述系统中的CPU、内存、外设、中断控制器等硬件信息。本章将系统讲解设备树源文件(.dts)的核心语法要素,为后续使用ftrace追踪解析函数调用打下坚实的语法基础。
学习提示:设备树语法是嵌入式Linux开发的“硬件语言”,掌握节点、属性、标签、包含与宏定义这五大要素,就能读懂并编写绝大多数设备树文件。
2.1 节点(Node):设备树的骨架
节点是设备树的基本组成单元,每个节点代表一个硬件设备或总线。节点通过路径(path)唯一标识,格式为 /node-name@unit-address。
2.1.1 节点定义语法
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x0 0x100>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
serial@101f1000 {
compatible = "ns16550";
reg = <0x101f1000 0x1000>;
interrupts = <0 12 4>;
};
};
2.1.2 节点命名规则
| 组成部分 | 说明 | 示例 |
|---|---|---|
| 节点名称 | 描述设备类型,使用小写字母、数字和连字符 | cpu, serial, i2c-bus |
| 单元地址 | 可选,通常为设备基地址或编号,用十六进制表示 | @0, @101f1000 |
| 路径分隔 | 使用斜杠 / 表示层级关系 |
/soc/serial@101f1000 |
关键点:节点名称应尽量使用标准设备类型名称(如
serial、ethernet、gpio),避免使用厂商特定名称,以保证设备树的可移植性。
2.2 属性(Property):描述设备特征
属性是附加在节点上的键值对,用于描述设备的配置参数、兼容性、地址范围、中断信息等。属性值可以是空、整数、整数数组、字符串、字符串数组或二进制数据。
2.2.1 常用属性类型
| 属性类型 | 语法格式 | 示例 | 说明 |
|---|---|---|---|
| 空属性 | property-name; |
status = "disabled"; |
仅表示存在,无值 |
| 32位整数 | property-name = <value>; |
#address-cells = <1>; |
单个32位值 |
| 整数数组 | property-name = <val1 val2 ...>; |
reg = <0x101f1000 0x1000>; |
多个32位值,空格分隔 |
| 字符串 | property-name = "string"; |
compatible = "ns16550"; |
双引号包裹 |
| 字符串数组 | property-name = "str1", "str2"; |
compatible = "ti,am335x-uart", "ns16550"; |
逗号分隔多个字符串 |
| 二进制数据 | property-name = [hex bytes]; |
local-mac-address = [00 11 22 33 44 55]; |
十六进制字节序列 |
2.2.2 标准属性详解
- compatible:设备驱动匹配的关键属性,格式为
"厂商,设备型号",如"ti,am335x-uart"。内核通过该属性查找对应的驱动。 - reg:描述设备地址空间,格式为
<基地址 长度>,由#address-cells和#size-cells决定单元数。 - interrupts:描述中断信息,格式依赖
interrupt-parent和#interrupt-cells。 - status:设备状态,常用值
"okay"(启用)、"disabled"(禁用)。 - #address-cells / #size-cells:定义
reg属性中地址和长度的单元数(32位字长)。
常见错误:
compatible 属性值顺序很重要!内核优先匹配列表中的第一个字符串。如果厂商驱动未加载,会尝试匹配通用驱动。务必把最具体的兼容字符串放在最前面。
2.3 标签(Label):节点引用与复用
标签(label)是节点的别名,用于在设备树的其他位置引用该节点。标签以冒号结尾,放在节点名称之前。
2.3.1 标签定义与引用
/dts-v1/;
/ {
soc {
uart0: serial@101f1000 {
compatible = "ns16550";
reg = <0x101f1000 0x1000>;
interrupts = <0 12 4>;
};
};
};
// 在其他位置通过 &uart0 引用该节点
&uart0 {
status = "okay";
clock-frequency = <24000000>;
};
2.3.2 标签的典型用途
| 用途 | 说明 | 示例 |
|---|---|---|
| 覆盖属性 | 在板级.dts中修改SoC级.dtsi中的节点属性 | &uart0 { baud = <115200>; }; |
| 引用节点 | 在 interrupt-parent 或 clocks 中引用其他节点 |
interrupt-parent = &gic; |
| 添加子节点 | 向已存在的节点添加新的子节点 | &i2c0 { eeprom@50 { ... }; }; |
| 条件编译 | 配合 #ifdef 实现不同配置 |
&usb0 { status = "okay"; }; |
调试技巧:使用ftrace追踪设备树解析时,可以关注
of_find_node_by_path 和 of_find_node_by_name 函数,它们通过节点路径或名称查找节点。标签在编译后会被展开为完整路径,因此使用标签引用的节点在运行时会被正确解析。
2.4 包含文件(#include):模块化设计
设备树支持C语言风格的 #include 预处理指令,用于引入头文件(.h)或其他设备树包含文件(.dtsi)。这是实现设备树模块化、复用SoC级定义的关键机制。
2.4.1 包含文件的使用
// am335x.dtsi - SoC级定义
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/gpio.h>
/ {
compatible = "ti,am335x";
#address-cells = <1>;
#size-cells = <1>;
ocp: ocp {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@44e09000 {
compatible = "ti,am335x-uart", "ns16550";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
};
};
// am335x-boneblack.dts - 板级定义
#include "am335x.dtsi"
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-boneblack", "ti,am335x-bone", "ti,am335x";
chosen {
stdout-path = &uart0;
};
};
&uart0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
};
2.4.2 .dtsi 与 .dts 的分工
| 文件类型 | 扩展名 | 内容范围 | 复用层级 |
|---|---|---|---|
| SoC包含文件 | .dtsi |
CPU、总线、外设IP核、中断控制器、时钟等 | 同一SoC系列所有板卡共享 |
| 板级文件 | .dts |
板卡型号、内存大小、外设使能、引脚复用、时钟频率 | 单板卡独有 |
| 头文件 | .h |
宏定义、中断号、GPIO编号、时钟索引等常量 | 跨SoC/板卡共享 |
最佳实践:将SoC级通用定义放在
.dtsi 中,板级差异化配置放在 .dts 中。头文件中的宏定义可以大幅提高代码可读性,例如 IRQ_TYPE_EDGE_RISING 比直接写数字 4 更清晰。
2.5 宏定义:提升可读性与可维护性
设备树源文件支持C预处理器宏定义,通过 #define 定义常量,在头文件中集中管理硬件参数。
2.5.1 宏定义示例
// dt-bindings/interrupt-controller/irq.h
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_LEVEL_HIGH 3
#define IRQ_TYPE_LEVEL_LOW 4
// dt-bindings/gpio/gpio.h
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1
// 在设备树中使用宏
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/gpio.h>
gpio_keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
button@0 {
label = "user-button";
gpios = <&gpio0 7 GPIO_ACTIVE_LOW>;
interrupts = <7 IRQ_TYPE_EDGE_FALLING>;
};
};
2.5.2 宏定义的优势
- 可读性:
IRQ_TYPE_EDGE_FALLING比2更直观,一眼就能看出中断触发类型。 - 可维护性:修改宏定义一处,所有使用该宏的地方自动更新。
- 跨平台一致性:不同SoC厂商可以使用相同的宏定义规范,降低学习成本。
- 编译时检查:预处理器在编译阶段展开宏,错误会在编译时暴露。
注意事项:宏定义只在预处理阶段生效,编译后的
.dtb 文件中所有宏已被展开为数值。因此,使用ftrace追踪设备树解析时,你看到的是展开后的数值,而不是宏名称。调试时需要对照头文件中的宏定义来理解数值含义。
2.6 综合示例:完整设备树文件
以下是一个综合了节点、属性、标签、包含文件和宏定义的完整设备树示例,展示了各语法要素如何协同工作。
// imx6ull.dtsi - SoC级定义
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/clock/imx6ull-clock.h>
/ {
#address-cells = <1>;
#size-cells = <1>;
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
gic: interrupt-controller@a01000 {
compatible = "arm,cortex-a7-gic";
reg = <0xa01000 0x1000>,
<0xa02000 0x100>;
interrupt-controller;
#interrupt-cells = <3>;
};
uart1: serial@2020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
reg = <0x2020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1>;
status = "disabled";
};
i2c1: i2c@21a0000 {
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x21a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};
};
};
// myboard.dts - 板级定义
#include "imx6ull.dtsi"
/ {
model = "My i.MX6ULL Board";
compatible = "my,imx6ull-board", "fsl,imx6ull";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB RAM
};
leds {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&led_pins>;
led0: led-0 {
label = "heartbeat";
gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart1_pins>;
};
&i2c1 {
status = "okay";
clock-frequency = <100000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <16>;
};
};
实战要点:在后续章节使用ftrace追踪设备树解析时,你将看到内核如何递归遍历节点树、解析
compatible 属性匹配驱动、处理 reg 地址资源、解析 interrupts 中断信息。理解本章的语法要素,是读懂ftrace输出日志的前提。