设备树子系统与驱动开发入门

文章目录

  • [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)
  • [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_defconfigBR2_LINUX_KERNEL_INTREE_DTS_NAME="rockchip/rk3588-orangepi-5-pro"
  • yocto 配置文件 orangepi-5-pro.confKERNEL_DEVICETREE = "rk3588s-orangepi-5-pro.dtb"
  • armbian 配置文件 orangepi5pro.cscBOOT_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:设备禁用,内核解析时会忽略该节点。

注意

  1. 若父节点(如总线控制器)的 statusdisabled,其所有子节点将被递归忽略,无论子节点自身配置如何。
  2. 父级 dtsi 文件中节点的 status 属性大都是 disabled,在引用设备节点时要显式声明 status = "okay",遵循按需启用的原则。
  3. 错误的拼写(如 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";
	};

注意

  1. interrupts 的具体编码(每个中断占用多少个 cell、各 cell 含义)由所引用的中断控制器节点的 #interrupt-cells 决定,该属性详细说明请参考下文 #interrupt-cells 小节内容。
  2. interrupt-parent 可显式指定中断控制器;若不写,内核会沿父节点查找可用的 interrupt-parent,但在使用 GPIO 中断或自定义中断控制器时最好显式指定以免歧义。
  3. 触发类型(上升沿/下降沿/电平等)是 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:电平极性标志,表明高电平为逻辑有效状态。

注意

  1. Linux GPIO 子系统在解析时会自动处理 -gpios 后缀。例如,设备树定义为 reset-gpios,驱动中调用 devm_gpiod_get() 时应传入 reset。若传入全名,将会导致查找失败。
  2. 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

功能

  1. 一个物理引脚可能既能做 GPIO,也能做 UART 的 TX,pinctrl 决定了它当前的"身份"。
  2. 通过 pinctrl 设置内部电阻(上拉/下拉),可以防止引脚在未驱动时处于浮空状态,提高系统稳定性。
  3. 在板级 .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() 来确保设备上电并正常工作。

功能

  1. Linux 内核默认遵循"按需开启"原则。如果设备树中未定义时钟或驱动未主动开启,硬件模块将处于断电/关断状态以节省功耗。
  2. 某些外设(如 UART 或 SPI)需要精确的时钟频率才能正常通信。通过设备树关联时钟树,内核可以自动计算并配置分频器。

4.8 cells

在设备树中,凡是涉及数值列表(如 reginterrupts)的属性,其数据的长度和含义均由父节点(或引用节点)中的 *-cells 属性决定。

4.8.1 #address-cells 与 #size-cells

#address-cells#size-cells 属性定义在父节点(通常是总线控制器,如 soci2cspi)中,用于规定其所有子节点在描述 reg 资源时应当遵循的语法规则。设备树中的数值以 32 位整型(u32)为基本单位,每一个单位被称为一个 Cell

  • #address-cells:指定子节点 reg 属性中起始地址部分占用多少个 Cell。
  • #size-cells:指定子节点 reg 属性中长度/空间大小部分占用多少个 Cell。

详细说明

  1. 64 位地址空间 :在 ARM64 架构中,外设的物理基地址和空间长度往往需要 64 位来表示。此时,父节点(如 axi 总线)会设置 #address-cells = <2>#size-cells = <2>。这意味着子节点的 reg 必须提供 4 个 u32 数值(前两个组合成 64 位地址,后两个组合成 64 位长度),例如 reg = <0x0 0xfb000000 0x0 0x200000>
  2. 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. 总结

设备树作为硬件描述语言,是连接硬件平台与操作系统内核的桥梁。确保 statuscompatiblereg 等关键属性的准确性,是驱动开发工作的基础。任何配置上的疏漏,都将导致内核忽略相关设备,使驱动程序无法执行初始操作。

相关推荐
小白同学_C6 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖6 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
不做无法实现的梦~7 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
默|笙9 小时前
【Linux】fd_重定向本质
linux·运维·服务器
陈苏同学10 小时前
[已解决] Solving environment: failed with repodata from current_repodata.json (python其实已经被AutoDL装好了!)
linux·python·conda
“αβ”10 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
不爱学习的老登11 小时前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
熊猫_豆豆12 小时前
同步整流 Buck 降压变换器
单片机·嵌入式硬件·matlab
小王C语言12 小时前
进程状态和进程优先级
linux·运维·服务器
xlp666hub12 小时前
【字符设备驱动】:从基础到实战(下)
linux·面试