3、设备树基础语法:节点、属性、值、标签、引用、路径、别名等基本元素
设备树(Device Tree)是一种描述硬件资源的数据结构,它通过节点(node)和属性(property)的层级组合,将板级硬件信息从内核代码中剥离出来。理解设备树的基本语法,是编写、修改和调试设备树源文件(.dts)的前提。本章将围绕节点、属性、值、标签、引用、路径和别名这七个核心元素展开,建立对设备树“数据描述语言”的完整认知。
设备树的核心思想是“用数据描述硬件”,而非“用代码初始化硬件”。在Linux内核中,设备树源文件(.dts)经过设备树编译器(dtc)编译为二进制设备树块(.dtb),内核在启动时解析该二进制文件,从而获知系统中有哪些设备、设备位于何处、如何配置驱动。这种机制使得同一份内核镜像可以支持多种硬件平台,只需更换对应的.dtb文件即可。
3.1 节点:设备树的骨架
节点是设备树的基本组成单元,用于表示一个硬件设备、总线、控制器或简单的逻辑分组。节点采用树形层级结构,从根节点“/”开始,向下延伸出子节点。每个节点可以包含属性(描述自身特征)和子节点(描述其下的从属设备)。
核心原理:节点的命名与层级
节点名称由“节点名”和“单元地址”两部分组成,格式为node-name@unit-address。其中,node-name用于标识节点类型,通常使用通用名称(如uart、i2c、ethernet);unit-address用于区分同一类型的不同实例,通常对应设备在总线上的寄存器地址或片选编号。例如:
/ {
soc {
uart@1a000000 {
compatible = "ns16550a";
reg = <0x1a000000 0x1000>;
};
uart@1a001000 {
compatible = "ns16550a";
reg = <0x1a001000 0x1000>;
};
};
};
上述代码中,uart@1a000000和uart@1a001000是两个不同的串口节点,通过单元地址区分。节点名的选择应遵循“设备类型优先”原则,避免使用具体芯片型号作为节点名(如uart@...优于ns16550@...),因为节点名是给内核和调试工具识别的逻辑标识,而具体芯片型号由compatible属性描述。
判断方法:节点是否合法
判断一个节点是否合法,可以从以下三个维度检查:
- 命名规范:节点名只能包含小写字母、数字、逗号和连字符,且必须以字母开头;单元地址为十六进制数字,可省略。
- 层级关系:每个节点必须有且仅有一个父节点(根节点除外);子节点之间不能重名(包括单元地址)。
- 标签唯一性:如果节点定义了标签(label),标签名在整个设备树中必须唯一。
案例拆解:CPU节点的组织方式
以双核ARM处理器为例,设备树中通常将CPU核组织在cpus节点下:
/ {
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a9";
reg = <1>;
};
};
};
这里#address-cells和#size-cells是总线节点的重要属性,它们决定了子节点中reg属性的地址和长度字段的位数(详见3.3节)。device_type属性是CPU节点的特殊标识,用于兼容旧版内核的启动代码。
3.2 属性与值:描述设备特征
属性是节点的“键值对”数据,用于描述设备的硬件特征。属性名是字符串,属性值可以是空、字符串、数值、数组或布尔值。设备树规范(ePAPR)定义了一系列标准属性,同时也允许自定义属性。
核心原理:属性值的编码规则
属性值在设备树源文件中的表示方式如下:
| 值类型 | 语法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "arm,cortex-a9"; |
用双引号包裹,可包含多个字符串(以空格分隔) |
| 32位整数 | reg = <0x1a000000 0x1000>; |
用尖括号包裹,多个数值以空格分隔 |
| 64位整数 | reg = <0x0 0x40000000 0x0 0x1000>; |
由两个32位整数组合表示 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
用方括号包裹,每字节两位十六进制数 |
| 布尔值 | status = "disabled"; |
通常用字符串"okay"或"disabled"表示 |
其中,compatible属性是最关键的匹配属性。内核通过它查找设备对应的驱动,其值是一个字符串列表,按优先级从高到低排列。例如:
compatible = "fsl,imx6ull-uart", "ns16550a";
内核首先尝试匹配fsl,imx6ull-uart,如果失败则尝试ns16550a。这种机制允许同一驱动兼容多个硬件版本。
操作流程:如何定义自定义属性
自定义属性用于传递板级特有信息(如GPIO编号、时钟频率、电源域标识)。定义时需注意:
- 属性名建议使用“厂商前缀,属性名”格式(如
fsl,pinmux),避免与标准属性冲突。 - 属性值类型需与驱动解析代码保持一致,否则会导致数据错位。
- 在驱动中通过
of_property_read_u32()、of_property_read_string()等API读取。
常见误区:属性值类型混淆
初学者常将字符串和整数混淆。例如,将clock-frequency = <24000000>;写成clock-frequency = "24000000";,这会导致驱动读取到的是字符串指针而非整数,引发时钟配置错误。另一个常见错误是忘记#address-cells和#size-cells的设置,导致reg属性解析异常。
3.3 标签、引用与路径:定位节点的三种方式
在设备树中,经常需要从一个节点引用另一个节点(例如中断控制器、时钟源、GPIO控制器)。设备树提供了三种定位节点的方式:标签(label)、路径(path)和别名(alias)。
核心原理:三种定位机制对比
| 定位方式 | 语法 | 特点 | 典型用途 |
|---|---|---|---|
| 标签引用 | &label_name |
编译期解析,效率高;标签名需唯一 | 在节点中引用中断控制器、时钟等 |
| 路径引用 | &/path/to/node |
直接使用路径,无需预先定义标签 | 在头文件(.dtsi)中覆盖或修改节点 |
| 别名引用 | &alias_name |
通过/aliases节点定义,语义化强 |
在bootargs或驱动中引用固定设备 |
标签的定义方式是在节点名前加上“标签名:”前缀:
/ {
soc {
uart0: uart@1a000000 {
compatible = "ns16550a";
reg = <0x1a000000 0x1000>;
};
};
};
之后在其它节点中可以通过&uart0引用该节点。引用操作通常用于添加或修改属性:
&uart0 {
status = "okay";
clock-frequency = <24000000>;
};
这段代码在编译时会被展开为对uart@1a000000节点的属性追加。
操作流程:使用路径引用修改节点
当设备树源文件被拆分为多个.dtsi头文件时,路径引用非常有用。例如,在imx6ull.dtsi中定义了uart1节点,在板级.dts文件中可以这样覆盖:
&/soc/uart@1a000000 {
pinctrl-0 = &pinctrl_uart1;
};
路径引用要求开发者准确掌握节点在树中的位置,一旦父节点改名或层级调整,路径引用就会失效。因此,在大型项目中优先使用标签引用。
案例拆解:别名在串口调试中的应用
在Linux内核启动参数中,经常需要指定控制台串口,例如console=ttymxc0。这里的ttymxc0对应设备树中的/aliases节点:
/ {
aliases {
serial0 = &uart1;
serial1 = &uart2;
};
};
内核通过serial0别名找到uart1节点,再根据其compatible属性匹配驱动。使用别名的好处是:即使硬件上更换了串口引脚,只需修改aliases节点,无需改动内核命令行参数。
3.4 综合案例:从零构建一个最小设备树
为了将上述语法元素串联起来,我们构建一个包含内存、串口和GPIO控制器的简化设备树。假设硬件平台为:单核ARM处理器,256MB内存起始地址0x40000000,一个NS16550串口位于0x1a000000,一个GPIO控制器位于0x1b000000。
/dts-v1/;
/ {
model = "Example Board";
compatible = "example,board-v1";
#address-cells = <1>;
#size-cells = <1>;
aliases {
serial0 = &uart0;
};
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x10000000>; // 256MB
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges; // 子节点地址与父节点地址空间相同
uart0: uart@1a000000 {
compatible = "ns16550a";
reg = <0x1a000000 0x1000>;
interrupts = <0 27 4>; // SPI中断,编号27,高电平触发
clock-frequency = <24000000>;
status = "okay";
};
gpio0: gpio@1b000000 {
compatible = "example,gpio";
reg = <0x1b000000 0x1000>;
#gpio-cells = <2>;
gpio-controller;
};
};
};
该设备树展示了以下关键点:
- 根节点:包含
model和compatible属性,用于识别板卡型号。 - 地址单元:根节点和
soc节点均声明#address-cells = <1>和#size-cells = <1>,表示子节点reg属性中地址和长度各占一个32位整数。 - ranges属性:空
ranges表示子节点地址与父节点地址空间直接映射,无需地址转换。 - 中断描述:
interrupts = <0 27 4>中的第一个字段0表示SPI中断类型,27为中断号,4为触发方式(高电平触发)。 - GPIO控制器:
gpio-controller属性标记该节点为GPIO控制器,#gpio-cells = <2>表示引用该控制器时需要两个参数(GPIO编号和标志)。
编译与验证流程
使用设备树编译器检查语法正确性:
dtc -I dts -O dtb -o example.dtb example.dts
dtc -I dtb -O dts -o example_verify.dts example.dtb # 反编译验证
反编译后的example_verify.dts应与源文件内容一致(除注释和格式外)。如果编译报错,通常是因为括号不匹配、属性值类型错误或标签重复。
常见误区与复盘
- 误区1:忘记声明
#address-cells和#size-cells。这会导致reg属性被错误解析,驱动读取到的地址和长度完全错误。 - 误区2:在节点中重复定义同名属性。设备树规范不允许同一节点内出现两个同名属性,编译器会报错。
- 误区3:引用未定义的标签。使用
&uart0前必须确保uart0标签已定义,否则编译失败。 - 误区4:忽略
status属性。默认情况下,节点状态为"okay",但如果硬件上未焊接某个外设,应显式设置为"disabled",避免驱动加载失败。