设备树驱动开发从设备节点到probe函数调用 - 第1节

1.1 设备树的诞生背景:从“硬编码”到“动态描述”

在早期的 Linux 内核(2.6 及更早版本)中,板级细节(如内存基址、外设中断号、GPIO 映射)被直接硬编码在 arch/arm/mach-xxx 的 C 语言文件中。这种“代码即配置”的方式带来了严重的维护灾难:

  • 代码膨胀:每增加一款开发板,就需要新增或修改大量 board-*.c 文件。
  • 耦合度高:驱动代码与板级配置深度绑定,更换外设或调整引脚时,必须重新编译内核。
  • 碎片化严重:ARM 架构下,不同厂商的 SoC 和开发板各自维护一套板级文件,导致主线内核难以统一支持。
核心痛点:内核无法在运行时感知硬件拓扑,所有硬件信息必须在编译前确定。

1.1.1 从 PowerPC 到 ARM 的迁移

设备树(Device Tree)最初源于 Open Firmware(IEEE 1275),被 PowerPC 架构成功用于描述硬件。2011 年左右,Linux ARM 社区决定全面引入设备树,以解决上述痛点。设备树将硬件描述从内核源码中剥离,变成一个独立的数据结构,由 bootloader 传递给内核。

1.2 设备树的核心作用

设备树本质上是一个描述硬件资源的树形数据结构,其核心作用可归纳为以下三点:

作用 说明 传统方式(无设备树) 设备树方式
硬件描述 描述 CPU、内存、外设、中断控制器、GPIO 控制器等硬件节点及其属性 硬编码在 board-*.c 中 以 .dts 文本文件描述
驱动与硬件解耦 驱动通过 compatible 字符串匹配设备节点,不再依赖板级代码 驱动直接调用板级 API 驱动只关心 compatible 匹配
多板支持 同一内核镜像通过更换 .dtb 文件即可支持不同开发板 需编译多个内核镜像 只需更换 .dtb 文件
关键理解:设备树是“硬件配置数据”,而不是“驱动代码”。它告诉内核“有什么硬件”,但不负责“如何驱动”。

1.3 设备树的基本概念与术语

在深入设备树驱动开发之前,必须掌握以下核心术语:

  • DTS (Device Tree Source):设备树源文件,文本格式,后缀为 .dts 或 .dtsi。
  • DTC (Device Tree Compiler):将 .dts 编译为 .dtb 的工具。
  • DTB (Device Tree Blob):二进制格式的设备树,由 bootloader 加载并传递给内核。
  • Node (节点):描述一个硬件设备或总线,格式为 node-name@unit-address。
  • Property (属性):节点的具体参数,如 reg、interrupts、compatible。
  • compatible:最重要的属性,用于驱动匹配,格式为 "厂商,设备型号"。

1.3.1 设备树结构示意图

下图展示了一个简化的设备树结构,包含根节点、CPU 节点、内存节点和两个外设节点:

设备树基本结构示意图 根节点 / cpus { } memory { } soc { } uart@... { } i2c@... { } 设备树节点说明 • 每个节点可以包含子节点,形成树形结构。 • 节点通过 compatible 属性与驱动匹配。 • reg 属性描述地址空间,interrupts 属性描述中断资源。

1.4 为什么 Linux 内核引入设备树?

引入设备树并非偶然,而是嵌入式 Linux 生态发展的必然选择。以下从四个维度分析其必要性:

维度 引入设备树之前 引入设备树之后
内核维护 每个 ARM 开发板都需要维护 board-*.c 文件,主线内核难以统一 板级代码大幅减少,硬件描述统一为 .dts 文件
驱动开发 驱动需要包含板级头文件,调用平台特定的 API 驱动只通过设备树 API 获取资源,与板级无关
启动流程 bootloader 传递 ATAG 列表,信息有限且不灵活 bootloader 传递 .dtb 文件,信息丰富且可扩展
多板兼容 一个内核镜像只能支持一种开发板 一个内核镜像通过更换 .dtb 支持多种开发板
注意:设备树并不能解决所有硬件差异问题。对于运行时动态变化的硬件(如 USB 热插拔),设备树只提供控制器描述,具体设备由总线枚举发现。

1.4.1 设备树与内核启动流程

设备树在内核启动过程中的位置至关重要,下图展示了从 bootloader 到驱动 probe 的完整链路:

设备树与内核启动流程 Bootloader 加载 .dtb 到内存 内核早期初始化 解析 DTB,创建 device_node 驱动匹配 compatible 匹配 → probe 关键流程说明 1. Bootloader 将 .dtb 文件加载到内存,并将地址通过寄存器 r2 传递给内核。 2. 内核在 start_kernel() 之前调用 setup_machine_fdt() 解析 DTB。 3. 内核为每个设备节点创建 platform_device,驱动通过 of_match_table 匹配。 4. 匹配成功后,内核调用驱动的 probe 函数,驱动通过设备树 API 获取资源。

1.5 设备树文件体系:.dts / .dtsi / .dtb

设备树源文件采用类似 C 语言的 #include 机制,形成层次化的文件体系:

  • .dtsi (Device Tree Source Include):公共的 SoC 级描述文件,类似 C 头文件,可被多个 .dts 包含。
  • .dts (Device Tree Source):具体的板级描述文件,包含或覆盖 .dtsi 中的节点。
  • .dtb (Device Tree Blob):编译后的二进制文件,是内核实际使用的格式。
最佳实践:SoC 厂商提供 .dtsi 文件,板卡厂商提供 .dts 文件,通过 #include 和 &node 引用机制实现复用与覆盖。

1.5.1 一个简单的 .dts 示例

/dts-v1/;

#include "imx6ul.dtsi"

/ {
    model = "Freescale i.MX6 UltraLite EVK Board";
    compatible = "fsl,imx6ul-evk", "fsl,imx6ul";

    memory {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };

    &uart1 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};

上述代码中:

  • /dts-v1/; 声明 DTS 版本。
  • #include "imx6ul.dtsi" 引入 SoC 级描述。
  • / { } 定义根节点,包含 model、compatible、memory 等属性。
  • &uart1 { } 通过引用标签覆盖 .dtsi 中 uart1 节点的属性。

1.6 本章小结

设备树的引入是 Linux 内核嵌入式生态的一次重要重构。它将硬件描述从内核代码中分离,实现了驱动与板级配置的解耦,极大地提升了内核的可维护性和可移植性。理解设备树的起源、作用与基本概念,是掌握后续设备树驱动开发的基础。

关键概念 一句话总结
设备树 描述硬件资源的树形数据结构,独立于内核源码
DTS/DTB 源文件与二进制文件,通过 DTC 编译转换
compatible 驱动匹配的核心属性,格式为 "厂商,设备型号"
节点与属性 节点描述设备,属性描述设备的参数