驱动移植实战:外设内存映射与设备树适配方法详解 - 第3节

3.1 设备树的起源与设计动机

在早期的嵌入式 Linux 系统中,板级硬件信息(如内存基地址、中断号、GPIO 映射)被硬编码在 arch/arm/mach-xxx 下的 C 语言板文件中。每更换一款 SoC 或外设,就必须重新编译内核,导致内核源码中充斥着大量平台相关的“垃圾代码”。

为了解决这一痛点,PowerPC 社区率先引入了 设备树(Device Tree) 概念,随后被 ARM、RISC-V 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化文本形式独立存在,内核在启动时解析该描述动态生成设备节点。

关键收益:同一份内核镜像可以支持多个硬件平台,只需更换设备树文件即可完成适配,极大降低了内核维护成本。

3.1.1 设备树在嵌入式系统中的位置

设备树位于 Bootloader(如 U-Boot)与 Linux 内核之间,充当硬件信息的“桥梁”。

设备树在系统启动流程中的位置 硬件外设 Bootloader 设备树(DTB) Linux 内核 Bootloader 将 DTB 加载到内存,内核启动时解析 DTB 并动态创建设备

3.2 DTS / DTC / DTB 文件格式

设备树涉及三种文件形态,理解它们的转换关系是入门的第一步。

文件类型 扩展名 格式 作用
DTS .dts 文本(ASCII) 设备树源文件,开发者编写硬件描述
DTC — 编译器工具 将 DTS 编译为 DTB
DTB .dtb 二进制(FDT 格式) 内核启动时直接解析的二进制设备树
DTSI .dtsi 文本(ASCII) 设备树包含文件,用于 SoC 级公共描述

3.2.1 编译流程

DTS 通过 DTC 工具编译为 DTB,也可以反编译 DTB 为 DTS 进行调试。

DTS → DTC → DTB 编译流程 .dts 源文件 DTC 编译器 .dtb 二进制 常用命令:dtc -I dts -O dtb -o output.dtb input.dts
实战提示:在开发板上调试时,可以使用 dtc -I dtb -O dts -o dump.dts /sys/firmware/fdt 反编译当前运行的 DTB,快速验证设备树是否被正确加载。

3.3 设备树语法入门

设备树语法基于节点(node)和属性(property),采用树形层级结构描述硬件拓扑。以下是最核心的语法规则。

3.3.1 基本结构

一个完整的 DTS 文件包含一个根节点 /,其下挂载若干子节点,每个节点可以包含属性和子节点。

/dts-v1/;

/ {
    compatible = "vendor,board-name";
    #address-cells = <1>;
    #size-cells = <1>;

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x00 0x100>;
    };

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

    uart0: serial@10000000 {
        compatible = "ns16550a";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
    };
};

3.3.2 节点命名规则

节点名称格式为 node-name@unit-address,其中 @unit-address 通常对应设备在总线上的起始地址。

示例 说明
cpu@0 CPU 核心 0,地址为 0
serial@10000000 串口外设,基地址 0x10000000
i2c@20000000 I2C 控制器,基地址 0x20000000

3.3.3 常用属性

  • compatible:驱动匹配的关键字符串,格式为 "厂商,设备型号",内核通过该属性查找对应驱动。
  • reg:描述设备地址空间,格式为 <address length>,地址和长度由 #address-cells 和 #size-cells 决定。
  • interrupts:描述中断号、触发类型,格式依赖 interrupt-parent 和 #interrupt-cells。
  • status:设备状态,常用值 "okay"(启用)或 "disabled"(禁用)。

3.3.4 节点引用与标签

使用 标签名: 节点名@地址 定义标签,后续通过 &标签名 引用节点,实现追加或修改属性。

// 定义标签
uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x10000000 0x1000>;
    status = "disabled";
};

// 在其他地方引用并修改
&uart0 {
    status = "okay";
    clock-frequency = <24000000>;
};
常见错误:忘记在 DTS 文件头部声明 /dts-v1/;,会导致 DTC 编译失败。另外,reg 属性的地址和长度必须与 #address-cells / #size-cells 严格对齐,否则内核解析会出错。

3.4 设备树层级关系与地址映射

设备树的树形结构天然反映了 SoC 内部的总线拓扑。根节点代表整个板级系统,子节点代表挂载在总线上的外设。

设备树典型层级结构 / (根节点) cpus memory@80000000 soc cpu@0 serial@10000000 i2c@20000000 gpio@30000000 每个节点通过 reg 属性描述其在父总线地址空间中的映射范围

3.5 设备树与驱动的绑定机制

内核通过 compatible 属性将设备树节点与驱动进行匹配。当驱动注册时,内核遍历设备树,查找 compatible 值匹配的节点,并调用驱动的 probe 函数。

3.5.1 匹配流程

  1. 内核启动时,解析 DTB 构建 device_node 树。
  2. 总线驱动(如 platform bus)扫描设备树,为每个节点创建 platform_device。
  3. 驱动注册时,内核比较驱动的 of_match_table 与节点的 compatible 值。
  4. 匹配成功则调用驱动的 probe 函数,并将设备树中的资源(reg、interrupts 等)传递给驱动。
// 驱动中的匹配表示例
static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "vendor,my-device" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .of_match_table = my_driver_of_match,
    },
};
module_platform_driver(my_driver);
核心要点:设备树中的 compatible 字符串必须与驱动中 of_match_table 的字符串完全一致(包括大小写和厂商前缀),否则驱动无法绑定。

3.6 本章小结

设备树是嵌入式 Linux 驱动开发中不可或缺的硬件描述语言。通过本章的学习,你应该掌握:

  • 设备树解决了板级代码硬编码问题,实现了内核与硬件描述的分离。
  • DTS 是文本源文件,DTC 是编译器,DTB 是内核解析的二进制文件。
  • 设备树语法以节点和属性为核心,compatible、reg、interrupts 是最常用的属性。
  • 驱动通过 of_match_table 与设备树节点进行匹配绑定。