Linux platform驱动开发与设备树资源适配 - 第3节

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

在早期的 Linux 内核中,板级硬件描述信息(如内存基址、中断号、GPIO 映射)被直接硬编码在 arch/arm/mach-xxx 下的 C 代码中。这种“代码即配置”的方式导致每次更换硬件平台都需要重新编译内核,且不同厂商的代码高度耦合,难以维护。

核心痛点: 内核源码中充斥着大量板级 platform_device 静态定义,每增加一款开发板就要修改内核源码,违背了“内核与硬件描述分离”的设计原则。

设备树(Device Tree)正是为解决此问题而生。它由 PowerPC 架构率先采用,后被 ARM、RISC-V 等架构广泛接纳,成为描述硬件拓扑结构的标准机制。

阶段 描述方式 缺点
早期(Linux 2.6 ~ 3.x) C 结构体硬编码 代码冗余、平台耦合、升级困难
过渡期 部分板级文件 + 设备树 兼容性差,维护成本高
现代(Linux 4.x+) 纯设备树 + 通用驱动 一次编译,多板运行
设备树设计动机:从硬编码到描述分离 旧方式(硬编码) arch/arm/mach-xxx platform_device 数组 修改硬件 → 改内核源码 重新编译 → 烧录 新方式(设备树) .dts 文本描述 修改硬件 → 改 .dts 编译 .dtb → 替换文件 内核无需重新编译 关键收益 硬件描述与驱动分离 同一内核镜像支持多板 社区维护成本大幅降低

3.2 DTS / DTC / DTB 文件格式

设备树体系由三种文件形态构成,分别对应人类可读的源文件、编译器、以及内核可解析的二进制文件。

文件后缀 全称 格式 作用
.dts Device Tree Source 文本(类 C 语法) 开发者编写硬件描述
.dtsi Device Tree Source Include 文本 公共头文件,被 .dts 引用
.dtb Device Tree Blob 二进制 内核 Bootloader 加载的最终产物
.dtbo Device Tree Blob Overlay 二进制 运行时叠加片段(热插拔场景)
编译流程: .dts + .dtsi → DTC(Device Tree Compiler) → .dtb。DTC 工具位于内核源码 scripts/dtc/ 目录下,也可通过 dtc 命令单独安装。
DTS → DTC → DTB 编译流水线 .dts 源文件 (文本描述) .dtsi 头文件 DTC 编译器 dtc -I dts -O dtb .dtb 二进制 (内核加载) Bootloader 传递给内核

3.3 设备树语法详解

3.3.1 节点(Node)

设备树的基本组成单元是节点,每个节点代表一个硬件设备或总线。节点采用树形层级结构,根节点为 /。

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            reg = <0x0>;
        };
    };

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

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "okay";
    };
};
节点命名规则: 节点名@单元地址。其中“单元地址”通常对应设备在总线上的基址,用于区分同一名称下的多个实例(如 cpu@0、cpu@1)。

3.3.2 属性(Property)

属性是节点的键值对,用于描述设备的配置参数。常见属性类型如下:

属性类型 语法示例 说明
字符串 compatible = "ti,am3352-uart"; 驱动匹配标识,格式为 厂商,型号
32 位整数 reg = <0x44e09000 0x2000>; 地址 + 长度,可包含多个 <>
64 位整数 reg = <0x00000000 0x80000000 0x00000000 0x20000000>; 高 32 位 + 低 32 位组合
布尔值 status = "okay"; 或 status = "disabled"; 字符串形式表示开关
字节数组 local-mac-address = [00 11 22 33 44 55]; 十六进制字节序列
字符串列表 compatible = "ti,am3352-uart", "ti,omap3-uart"; 逗号分隔,优先级从高到低

3.3.3 标签(Label)与引用

标签为节点提供一个全局别名,方便在其他位置引用。标签定义在节点名之前,使用 : 分隔。

// 定义标签
uart0: serial@44e09000 {
    compatible = "ti,am3352-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;
};

// 引用标签(追加属性)
&uart0 {
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
};
标签的典型用途: 在 .dtsi 头文件中定义基础节点,在 .dts 文件中通过 &标签 引用并追加板级特有属性(如引脚复用、时钟频率)。这种机制实现了 SoC 公共描述与板级定制描述的分离。
设备树节点、属性、标签关系图 根节点 / cpus 节点 memory 节点 标签: uart0 serial@44e09000 属性示例(节点内) compatible = "arm,cortex-a8"; reg = <0x80000000 0x20000000>; 标签引用(外部追加) &uart0 { pinctrl-0 = <&uart0_pins>; };

3.4 关键属性详解

3.4.1 compatible —— 驱动匹配的核心

compatible 是设备树中最重要的属性,它建立了硬件节点与内核驱动之间的桥梁。内核通过 of_match_table 匹配该属性。

// 设备树节点
i2c0: i2c@44e0b000 {
    compatible = "ti,am3352-i2c", "ti,omap4-i2c";
    reg = <0x44e0b000 0x1000>;
    interrupts = <70>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "okay";
};

// 内核驱动匹配表
static const struct of_device_id i2c_omap_of_match[] = {
    { .compatible = "ti,omap4-i2c", },
    { .compatible = "ti,am3352-i2c", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, i2c_omap_of_match);
匹配优先级: 内核遍历 compatible 字符串列表时,从左到右依次匹配。通常将最具体的型号放在最左侧,通用型号放在右侧作为 fallback。

3.4.2 reg —— 地址与内存映射

reg 属性描述设备的地址空间,由 #address-cells 和 #size-cells 决定每个地址和长度的单元数。

父节点属性 含义 reg 示例 解析结果
#address-cells = <1>; #size-cells = <1>; 32 位地址 + 32 位长度 reg = <0x44e09000 0x2000>; 基址 0x44e09000,长度 0x2000
#address-cells = <2>; #size-cells = <1>; 64 位地址 + 32 位长度 reg = <0x00000000 0x80000000 0x20000000>; 基址 0x80000000,长度 0x20000000
#address-cells = <1>; #size-cells = <0>; 仅地址,无长度 reg = <0x0>; CPU 核心编号 0

3.4.3 interrupts —— 中断描述

中断属性需要配合 interrupt-parent 和 #interrupt-cells 使用。典型的中断控制器(如 GIC)使用 3 个 cell:中断类型、中断号、触发方式。

// 中断控制器定义
gic: interrupt-controller@48241000 {
    compatible = "arm,cortex-a15-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x48241000 0x1000>;
};

// 外设节点引用中断
uart0: serial@44e09000 {
    compatible = "ti,am3352-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <0 72 4>;  // GIC_SPI 72 IRQ_TYPE_LEVEL_HIGH
    interrupt-parent = <&gic>;
};
中断 cell 含义(ARM GIC 标准): 第一个 cell 为 0(SPI 共享外设中断)或 1(PPI 私有外设中断);第二个 cell 为中断号;第三个 cell 为触发类型(1=上升沿,2=下降沿,4=高电平,8=低电平)。

3.5 设备树实战要点

  • 复用 .dtsi: SoC 厂商提供 .dtsi 文件定义公共 IP 核,板级 .dts 通过 #include 引入并覆盖或追加属性。
  • status 属性: 默认设为 "disabled",板级文件中通过 &节点 { status = "okay"; }; 启用。
  • pinctrl 绑定: 引脚复用通过 pinctrl-0 和 pinctrl-names 属性关联,引用 pin controller 节点中的配置。
  • 编译验证: 使用 dtc -I dts -O dtb -o test.dtb test.dts 编译,用 dtc -I dtb -O dts test.dtb 反编译检查。
设备树实战流程:从 .dtsi 到内核加载 SoC .dtsi 公共 IP 描述 板级 .dts #include + 追加 DTC 编译 生成 .dtb Bootloader 加载 传递给内核解析 内核启动时解析 .dtb → 创建 platform_device → 匹配驱动 → 完成硬件初始化
学习建议: 从内核源码 arch/arm/boot/dts/ 目录下找一个实际 .dts 文件(如 am335x-boneblack.dts),对照本节的语法规则逐行分析,是掌握设备树最有效的方法。