文章目录
- [1. 引言](#1. 引言)
- [2. 设备树文件定位](#2. 设备树文件定位)
-
- [2.1 源码目录与文件层级](#2.1 源码目录与文件层级)
- [2.2 确定目标文件](#2.2 确定目标文件)
-
- [2.2.1 运行时查询](#2.2.1 运行时查询)
- [2.2.2 查阅 Bootloader 环境变量](#2.2.2 查阅 Bootloader 环境变量)
- [2.2.3 检查编译配置](#2.2.3 检查编译配置)
- [2.3 修改原则](#2.3 修改原则)
- [3. 驱动匹配与加载机制](#3. 驱动匹配与加载机制)
- [4. 核心属性](#4. 核心属性)
-
- [4.1 status](#4.1 status)
- [4.2 compatible](#4.2 compatible)
- [4.3 reg](#4.3 reg)
- [4.4 interrupts](#4.4 interrupts)
- [4.5 gpios](#4.5 gpios)
- [4.6 pinctrl](#4.6 pinctrl)
- [4.7 clocks](#4.7 clocks)
- [4.8 cells](#4.8 cells)
-
- [4.8.1 #address-cells 与 #size-cells](#address-cells 与 #size-cells)
- 4.8.2 #interrupt-cells
- [5. 调试与验证](#5. 调试与验证)
- [6. 总结](#6. 总结)
1. 引言
在嵌入式 Linux 系统开发中,设备树(Device Tree)是描述硬件拓扑结构与资源属性的核心数据结构。它将硬件的具体描述与内核源代码分离,旨在解决内核中存在大量冗余板级代码的问题。
对于驱动开发工程师而言,设备树配置的正确性直接决定了驱动程序能否被内核正确加载。据统计,绝大多数驱动初始化失效(如 probe 函数未执行、GPIO 控制异常、中断未响应)的原因,并非源于驱动代码逻辑错误,而是由于设备树节点描述与驱动期望不符。
本文将首先介绍如何在内核源码中定位目标设备树文件,随后深入剖析设备树核心属性的配置规范与常见误区。
2. 设备树文件定位
在修改设备树配置之前,必须准确识别当前硬件平台所使用的设备树源码文件。设备树文件没找对将直接导致配置不生效,如修改了其他平台的设备树文件,或修改了未被编译的设备树文件。
2.1 源码目录与文件层级
Linux 内核中的设备树源码通常位于以下路径:
- ARM32 :
arch/arm/boot/dts/ - ARM64 :
arch/arm64/boot/dts/
文件主要分为两类:
.dtsi(Device Tree Source Include):通常描述 SoC 内部通用的硬件资源(如 CPU 核、I2C/SPI 控制器等)。这是父级文件,被多个板级文件共享。.dts(Device Tree Source):描述具体开发板的硬件信息(如板载传感器、LED、接口配置)。这是最终编译为.dtb的入口文件。
2.2 确定目标文件
确定我们要修改的设备树源码文件有以下常见的三种方法。
2.2.1 运行时查询
在开发板的 Linux 系统终端中执行以下命令,获取当前设备树的 model 属性:
bash
$ cat /sys/firmware/devicetree/base/model
Orange Pi 5 Pro
# 或
$ cat /proc/device-tree/model
Orange Pi 5 Pro
获取字符串后,在内核源码的 dts 目录下使用 grep 搜索该字符串,即可定位到具体的 .dts 文件。
例如,在 arch/arm64/boot/dts/rockchip/rk3588s-orangepi-5-pro.dts 文件中搜到匹配的 model 属性:
dts
/ {
// 匹配 model 属性
model = "Orange Pi 5 Pro";
compatible = "rockchip,rk3588s-orangepi-5-pro", "rockchip,rk3588";
/delete-node/ chosen;
...
};
2.2.2 查阅 Bootloader 环境变量
在 U-Boot 命令行中检查 fdtfile 环境变量:
bash
printenv fdtfile
执行后获取到的环境变量值:rockchip/rk3588s-orangepi-5-pro.dtb。该变量的值即为系统启动时加载的 .dtb 文件名,对应的 .dts 源码通常与之同名。
2.2.3 检查编译配置
查看构建系统(如 Yocto、Buildroot 和 Armbian 等)的配置文件,确认相关变量的设置。以下是常见构建系统的典型设置:
buildroot配置文件orangepi_5_pro_defconfig:BR2_LINUX_KERNEL_INTREE_DTS_NAME="rockchip/rk3588-orangepi-5-pro"yocto配置文件orangepi-5-pro.conf:KERNEL_DEVICETREE = "rk3588s-orangepi-5-pro.dtb"armbian配置文件orangepi5pro.csc:BOOT_FDT_FILE="rockchip/rk3588s-orangepi-5-pro.dtb"
2.3 修改原则
设备树的修改原则是尽量使用引用覆盖已有配置,而非直接修改原配置。
工程最佳实践 :尽量避免直接修改 SoC 厂商提供的 .dtsi 文件。应在板级 .dts 文件中通过引用标签的方式对节点属性进行覆盖。
例如,i2c0 节点在 .dtsi 中默认是禁用的,应在 .dts 中如下修改:
dts
// 在板级 dts 文件的根节点引用
&i2c0 {
status = "okay"; // 覆盖父级配置
// 添加特定设备节点
pinctrl-names = "default";
pinctrl-0 = <&i2c0m2_xfer>;
};
3. 驱动匹配与加载机制
理解设备树的关键在于掌握内核如何解析 .dtb(设备树二进制文件)并将设备节点与驱动程序进行绑定。以下流程图展示了从设备树描述到驱动 probe 函数调用的完整逻辑链路。
驱动程序
设备树子系统
设备树描述(DTS)
disabled
okay
匹配失败
匹配成功
设备节点定义
compatible 属性
status 属性
reg 属性
解析 DTB 二进制文件
检查 status 属性
忽略该节点
注册 platform_device / i2c_client
匹配 compatible 字符串
静默失败(无报错)
of_match_table 定义
执行 probe() 回调
内核通过 of_match_table 与设备树中的 compatible 属性进行字符串匹配。若匹配成功且 status 为可用状态,内核才会调用驱动的入口函数。
4. 核心属性
4.1 status
status 属性用于定义设备的启用状态,是内核决定是否为该节点创建设备结构体的首要依据。
status 属性值:
okay:设备启用,内核将尝试绑定驱动。disabled:设备禁用,内核解析时会忽略该节点。
注意:
- 若父节点(如总线控制器)的
status为disabled,其所有子节点将被递归忽略,无论子节点自身配置如何。 - 父级
dtsi文件中节点的status属性大都是disabled,在引用设备节点时要显式声明status = "okay",遵循按需启用的原则。 - 错误的拼写(如
statuss)将导致属性失效,且编译器通常不会报错。
4.2 compatible
compatible 属性是设备与驱动程序绑定的关键标识符,驱动和设备通过这个标识进行匹配。
设备树中的字符串必须与驱动代码 of_device_id 表中的定义完全一致。若不匹配,内核将静默忽略该设备,不会产生错误日志。
语法示例:
dts
/ {
gpu_panthor: gpu-panthor@fb000000 {
compatible = "rockchip,rk3588-mali", "arm,mali-valhall-csf";
}
};
工作原理:
内核总线子系统会将设备树中的 compatible 字符串列表与驱动程序中 of_device_id 结构体的 compatible 字段逐一比对(包括标点符号)。该属性支持字符串数组。若驱动不支持第一个特定型号,内核会尝试匹配后续的通用型号定义,如果都不匹配,内核将静默忽略该设备,不会产生错误日志,这往往是驱动无法加载的主要原因。
4.3 reg
reg 属性用于定义设备的地址资源,其具体含义取决于设备所属的总线类型。
设备树节点名 @ 后面的地址应与 reg 的地址数值保持一致,支持写 64 位长度(由 reg 指定的两个数值组成),表示某设备在某个地址,如 gpu_panthor: gpu-panthor@fb000000。
应用场景:
- Platform 设备 :表示
MMIO地址,物理内存基地址和长度资源索引。例如reg = <0x0 0xfb000000 0x0 0x200000>,出现 4 个值是由于父节点指定了#address-cells和#size-cells均为 2,分别组合成 64 位地址和 64 位长度。 - I2C 设备 :表示 7 位从机地址(如
reg = <0x40>),I2C 地址必须是 7 位有效地址,不应包含读写位。 - SPI 设备 :表示片选编号(如
reg = <0>)。
4.4 interrupts
中断配置通常涉及中断源(Source)与中断控制器(Controller)的关联。
语法示例:
dts
hym8563: hym8563@51 {
compatible = "haoyu,hym8563";
reg = <0x51>;
...
interrupt-parent = <&gpio0>;
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
status = "okay";
};
注意:
interrupts的具体编码(每个中断占用多少个 cell、各 cell 含义)由所引用的中断控制器节点的#interrupt-cells决定,该属性详细说明请参考下文#interrupt-cells小节内容。interrupt-parent可显式指定中断控制器;若不写,内核会沿父节点查找可用的interrupt-parent,但在使用 GPIO 中断或自定义中断控制器时最好显式指定以免歧义。- 触发类型(上升沿/下降沿/电平等)是
interrupts中的一项 cell,其位置取决于#interrupt-cells。不要假定参数的含义,应以控制器的#interrupt-cells属性为准。
存在多个中断时,分别定义各个中断,每个中断的描述方法和只有一个中断的情况相同。建议定义 interrupt-names 为中断起名,方便驱动使用。
多中断示例:
dts
gpu_panthor: gpu-panthor@fb000000 {
compatible = "rockchip,rk3588-mali", "arm,mali-valhall-csf";
reg = <0x0 0xfb000000 0x0 0x200000>;
interrupts = <GIC_SPI 92 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 93 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 94 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "job", "mmu", "gpu";
status = "disabled";
}
4.5 gpios
该属性用于描述设备引用的 GPIO 资源及其初始状态。
语法示例:
dts
&pcie2x1l2 {
reset-gpios = <&gpio3 RK_PD1 GPIO_ACTIVE_HIGH>;
status = "okay";
};
属性解析:
&gpio3:表示 GPIO 控制器句柄。RK_PD1:该控制器下的引脚编号。GPIO_ACTIVE_HIGH:电平极性标志,表明高电平为逻辑有效状态。
注意:
- Linux GPIO 子系统在解析时会自动处理
-gpios后缀。例如,设备树定义为reset-gpios,驱动中调用devm_gpiod_get()时应传入reset。若传入全名,将会导致查找失败。 GPIO_ACTIVE_HIGH标志位直接影响驱动层的逻辑电平。若配置为低电平有效,驱动层设置逻辑1时,物理引脚将输出低电平。
4.6 pinctrl
pinctrl 属性用于配置引脚的复用功能(如将引脚配置为 GPIO、I2C 或 SPI)以及电气特性(如上下拉、驱动能力)。
语法示例:
dts
i2c0: i2c@fd880000 {
compatible = "rockchip,rk3588-i2c", "rockchip,rk3399-i2c";
reg = <0x0 0xfd880000 0x0 0x1000>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0m0_xfer>;
};
属性解析:
pinctrl-names:定义引脚状态列表,最常用的是default。内核在加载驱动时,会自动将引脚切换到default对应的配置。pinctrl-0:引用具体的引脚配置节点。这里的&i2c0m0_xfer通常定义在 Soc 级pinctrl.dtsi文件中,比如arch/arm64/boot/dts/rockchip/rk3588s-pinctrl.dtsi。
功能:
- 一个物理引脚可能既能做 GPIO,也能做 UART 的 TX,
pinctrl决定了它当前的"身份"。 - 通过
pinctrl设置内部电阻(上拉/下拉),可以防止引脚在未驱动时处于浮空状态,提高系统稳定性。 - 在板级
.dts中,我们通常只需引用(使用&符号)厂商已经在.dtsi中定义好的pinctrl配置,而不需要手动计算复杂的寄存器数值。
4.7 clocks
clocks 属性用于定义设备工作所需的时钟源,内核的时钟子系统会根据该描述为设备分配并使能时钟信号。
语法示例:
dts
i2c0: i2c@fd880000 {
compatible = "rockchip,rk3588-i2c", "rockchip,rk3399-i2c";
reg = <0x0 0xfd880000 0x0 0x1000>;
clocks = <&cru CLK_I2C0>, <&cru PCLK_I2C0>;
// 为多个时钟资源命名
clock-names = "i2c", "pclk";
}
属性解析:
clocks:引用时钟控制单元(如&cru)提供的具体时钟节点,通常包含时钟控制器的句柄和时钟 ID。clock-names:为时钟资源命名,方便驱动程序通过字符串获取特定的时钟。- 在驱动中,开发者通常会调用
devm_clk_get()和clk_prepare_enable()来确保设备上电并正常工作。
功能:
- Linux 内核默认遵循"按需开启"原则。如果设备树中未定义时钟或驱动未主动开启,硬件模块将处于断电/关断状态以节省功耗。
- 某些外设(如 UART 或 SPI)需要精确的时钟频率才能正常通信。通过设备树关联时钟树,内核可以自动计算并配置分频器。
4.8 cells
在设备树中,凡是涉及数值列表(如 reg 和 interrupts)的属性,其数据的长度和含义均由父节点(或引用节点)中的 *-cells 属性决定。
4.8.1 #address-cells 与 #size-cells
#address-cells 与 #size-cells 属性定义在父节点(通常是总线控制器,如 soc、i2c 或 spi)中,用于规定其所有子节点在描述 reg 资源时应当遵循的语法规则。设备树中的数值以 32 位整型(u32)为基本单位,每一个单位被称为一个 Cell。
#address-cells:指定子节点reg属性中起始地址部分占用多少个 Cell。#size-cells:指定子节点reg属性中长度/空间大小部分占用多少个 Cell。
详细说明:
- 64 位地址空间 :在 ARM64 架构中,外设的物理基地址和空间长度往往需要 64 位来表示。此时,父节点(如
axi总线)会设置#address-cells = <2>和#size-cells = <2>。这意味着子节点的reg必须提供 4 个 u32 数值(前两个组合成 64 位地址,后两个组合成 64 位长度),例如reg = <0x0 0xfb000000 0x0 0x200000>。 - I2C/SPI 总线 :由于 I2C 从机地址通常只有 7 位或 10 位,且这类设备访问不涉及内存映射的长度,因此 I2C 控制器节点通常设置
#address-cells = <1>而#size-cells = <0>。这使得子设备(如传感器)的reg属性只需填写一个数值作为从机地址(如reg = <0x51>)。
语法示例:
dts
/ {
compatible = "rockchip,rk3588";
#address-cells = <2>;
#size-cells = <2>;
gpu_panthor: gpu-panthor@fb000000 {
compatible = "rockchip,rk3588-mali", "arm,mali-valhall-csf";
// 父节点(根节点)指定了 cells 为 2-2
reg = <0x0 0xfb000000 0x0 0x200000>;
}
i2c0: i2c@fd880000 {
compatible = "rockchip,rk3588-i2c", "rockchip,rk3399-i2c";
reg = <0x0 0xfd880000 0x0 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
};
};
&i2c0 {
vdd_cpu_big0_s0: vdd_cpu_big0_mem_s0: rk8602@42 {
compatible = "rockchip,rk8602";
// 父节点 i2c0 指定了 cells 为 1-0
reg = <0x42>;
};
};
4.8.2 #interrupt-cells
#interrupt-cells 属性定义在中断控制器节点(如 GIC 全局中断控制器、GPIO 控制器)中,用于告知内核描述该控制器下的一个具体中断需要占用多少个 Cell。
当设备节点通过 interrupt-parent 引用某个中断控制器时,其 interrupts 属性所包含的参数个数,必须严格等于该控制器节点中 #interrupt-cells 的声明值。
常见标准配置:
- GIC (Generic Interrupt Controller) :通常设为
3。其内部 Cell 含义通常固定为:<中断类型 中断号 触发电平标志>。 - GPIO 控制器 :通常设为
2。其内部 Cell 含义通常为:<引脚编号 触发电平标志>,例如interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>刚好对应 2 个单元。
语法示例:
dts
gic: interrupt-controller@fe600000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
interrupt-controller;
}
gpio0: gpio@fd8a0000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfd8a0000 0x0 0x100>;
gpio-controller;
interrupt-controller;
#interrupt-cells = <2>;
};
hym8563: hym8563@51 {
compatible = "haoyu,hym8563";
reg = <0x51>;
interrupt-parent = <&gpio0>;
// 父节点 gpio0 指定了 cells 为 2
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
};
若单元数量配置不一致,内核在启动阶段解析二进制设备树(DTB)时,会因为属性长度检查失败而无法正确识别中断资源。这将直接导致驱动程序中 platform_get_irq() 或 devm_request_irq() 函数调用失败。
5. 调试与验证
为确保设备树配置正确生效,建议采用以下标准验证流程:
1. 运行时结构检查:
利用 Linux 的 /proc 文件系统查看内核实际加载的设备树结构,确认字段值是否符合预期,示例如下:
bash
# 由于字符串之间以空字符分隔,直接 cat 可能导致多个字符串显示时粘连在一起
$ cat /proc/device-tree/gpu-panthor@fb000000/compatible
rockchip,rk3588-maliarm,mali-valhall-csf
# 配合 tr 命令查看
$ cat /proc/device-tree/gpu-panthor@fb000000/compatible | tr '\0' '\n'
rockchip,rk3588-mali
arm,mali-valhall-csf
2. 反编译验证:
使用 dtc 工具将系统启动时加载的二进制文件(例如 /boot/dtb/rockchip/rk3588s-orangepi-5-pro.dtb)反编译为文本格式,以排查编译过程中宏展开或文件覆盖产生的问题,示例如下:
bash
# 将 dtb 反编译为 dts 文本
dtc -I dtb -O dts -o orangepi-5-pro.dts /boot/dtb/rockchip/rk3588s-orangepi-5-pro.dtb
3. 驱动加载追踪:
在驱动的 probe() 函数入口处添加内核日志打印(如 dev_info),这是判断驱动与设备树匹配是否成功的最直接手段。
6. 总结
设备树作为硬件描述语言,是连接硬件平台与操作系统内核的桥梁。确保 status、compatible、reg 等关键属性的准确性,是驱动开发工作的基础。任何配置上的疏漏,都将导致内核忽略相关设备,使驱动程序无法执行初始操作。