从设备树实例到驱动配置:嵌入式Linux实战 - 第4节

4.1 设备树包含机制:#include 与 /include/

设备树源文件(.dts / .dtsi)的编译预处理由 DTC(Device Tree Compiler) 完成。DTC 支持两种包含语法,但推荐统一使用 C 预处理器风格的 #include,以便同时获得宏定义和头文件展开能力。

语法 预处理器 宏支持 推荐场景
#include "xxx.dtsi" CPP(C 预处理器) 支持 #define / #ifdef 主流方案,配合 dtc -E 调试
/include/ "xxx.dtsi" DTC 内置 不支持宏 仅用于无宏需求的极简场景
实战建议: 在 Linux 内核源码中,arch/arm/boot/dts/ 和 arch/arm64/boot/dts/ 下的所有设备树均使用 #include。统一使用 CPP 预处理可以避免“宏定义在 /include/ 中无效”的隐蔽错误。

4.1.1 包含展开流程

DTC 在编译 .dts 文件时,先调用 CPP 展开所有 #include 和宏,生成一个完整的临时 .dts 文件,再交给 DTC 解析。这一过程可以通过 dtc -E 查看展开后的中间结果。

# 查看展开后的完整设备树(包含所有 .dtsi 和宏替换)
dtc -E -I dts -O dts arch/arm64/boot/dts/ti/k3-am625-beagleplay.dts
关键点: 展开后的文件可能达到数千行,但最终编译出的 .dtb 二进制大小通常只有几十 KB。包含机制只影响源码组织,不影响运行时效率。

4.2 .dtsi 文件设计原则

.dtsi(Device Tree Source Include)是设备树的“头文件”,用于存放可复用的 SoC 公共定义、外设节点模板、时钟/Pinctrl/中断等资源描述。设计良好的 .dtsi 能大幅减少多板级设备树的重复代码。

4.2.1 分层设计模型

.dtsi 分层设计模型 SoC 级 .dtsi(如 k3-am625.dtsi) CPU 核心、GIC、总线、UART/I2C/SPI 控制器、时钟树、DMA SoC 变体级 .dtsi(如 k3-am625-pinctrl.dtsi) Pinmux 配置、特定外设的默认状态、电源域划分 板级公共 .dtsi(如 k3-am625-beagleplay-common.dtsi) 板载 eMMC、PMIC、以太网 PHY、LED 定义 具体板级 .dts(如 k3-am625-beagleplay.dts) 板级差异化配置、GPIO 按键、自定义外设

4.2.2 设计原则清单

  • 单一职责: 每个 .dtsi 只描述一个逻辑层次(SoC / 变体 / 板级公共),不跨层混合。
  • 状态默认化: 外设节点在 .dtsi 中默认 status = "disabled",板级 .dts 按需 status = "okay"。
  • 标签(label)统一: 为所有关键节点定义标签(如 &uart0),方便板级文件通过 &label 引用覆盖。
  • 避免硬编码: 板级特有的 GPIO 编号、电源电压等参数不在 .dtsi 中写死,留空或使用宏占位。
  • 头文件分离: 宏定义、GPIO 编号、时钟索引等放入 .h 头文件,.dtsi 通过 #include 引用。
常见陷阱: 不要在 .dtsi 中直接写 status = "okay",除非该外设在所有使用该 .dtsi 的板级上都必须启用。否则会导致某些板级无法关闭不需要的外设,增加功耗和驱动加载时间。

4.3 多板级设备树组织策略

一个 SoC 通常对应多个开发板或产品。Linux 内核通过 Makefile 和 Kbuild 系统管理多板级设备树的编译。合理的目录组织能提升可维护性。

4.3.1 目录结构示例(以 TI AM62x 为例)

arch/arm64/boot/dts/ti/
├── k3-am62.dtsi                  # SoC 级基础定义
├── k3-am62-pinctrl.dtsi          # Pinmux 配置
├── k3-am62-clk.dtsi              # 时钟树
├── k3-am62-main.dtsi             # 主域外设
├── k3-am62-mcu.dtsi              # MCU 域外设
├── k3-am62-wakeup.dtsi           # 唤醒域外设
├── k3-am625-sk.dts               # AM625 SK 开发板
├── k3-am625-sk-lpm.dtsi          # SK 板低功耗公共配置
├── k3-am625-beagleplay.dts       # BeaglePlay 开发板
├── k3-am625-beagleplay-csi.dtsi  # BeaglePlay CSI 摄像头扩展
└── Makefile                      # 编译目标列表

4.3.2 多板级包含关系图

多板级设备树包含关系(TI AM62x 系列) k3-am62.dtsi k3-am62-pinctrl.dtsi + k3-am62-clk.dtsi k3-am625-sk-lpm.dtsi(低功耗公共) k3-am625-sk.dts SK 开发板 k3-am625-beagleplay.dts BeaglePlay 板

4.3.3 Makefile 编译管理

在 Makefile 中,每个板级 .dts 对应一个 .dtb 目标。Kbuild 系统自动处理依赖关系,当 .dtsi 文件变更时,所有依赖它的 .dtb 都会重新编译。

# arch/arm64/boot/dts/ti/Makefile 片段
dtb-$(CONFIG_ARCH_K3) += k3-am625-sk.dtb
dtb-$(CONFIG_ARCH_K3) += k3-am625-beagleplay.dtb
dtb-$(CONFIG_ARCH_K3) += k3-am642-evm.dtb
dtb-$(CONFIG_ARCH_K3) += k3-am642-sk.dtb
编译验证: 执行 make dtbs 即可编译所有使能的设备树。使用 make dtbs_check 可运行设备树绑定检查,验证节点是否符合 schema 规范。

4.4 实战案例:从 SoC .dtsi 到板级 .dts 的复用

以下展示一个简化的 UART 节点如何在 .dtsi 中定义,并在板级 .dts 中启用和配置。

4.4.1 SoC 级 .dtsi 定义

// soc.dtsi — SoC 公共定义
/ {
    soc {
        uart0: serial@2800000 {
            compatible = "ti,am62-uart", "ti,am64-uart";
            reg = <0x02800000 0x100>;
            interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&k3_clks 147 0>;
            clock-names = "fclk";
            status = "disabled";   // 默认关闭
        };

        uart1: serial@2810000 {
            compatible = "ti,am62-uart", "ti,am64-uart";
            reg = <0x02810000 0x100>;
            interrupts = <GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&k3_clks 148 0>;
            clock-names = "fclk";
            status = "disabled";
        };
    };
};

4.4.2 板级 .dts 启用与覆盖

// board.dts — 具体板级配置
#include "soc.dtsi"
#include "board-pinctrl.dtsi"

/ {
    model = "My AM62x Board";
    compatible = "mycompany,am62x-board", "ti,am625";
};

&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
    current-speed = <115200>;
    status = "okay";              // 启用 UART0
};

&uart1 {
    status = "disabled";          // 明确禁用 UART1(省电)
};
复用要点: 通过 &label 引用节点时,只需写出需要覆盖的属性,未提及的属性继承自 .dtsi。这种“增量覆盖”机制是设备树复用的核心。

4.5 本章小结

知识点 核心要点
#include 机制 统一使用 CPP 预处理,支持宏定义;/include/ 仅用于无宏场景
.dtsi 设计原则 分层单一职责、默认 disabled、标签化、避免硬编码
多板级组织 SoC → 变体 → 板级公共 → 具体板级,Makefile 管理编译目标
增量覆盖 板级 .dts 通过 &label 只覆盖差异属性,继承其余定义
课后自查: 检查你的板级 .dts 中是否有重复定义 .dtsi 已包含的属性?如果有,考虑使用 &label 覆盖替代复制粘贴,避免维护不一致。