从设备树实例到驱动配置:嵌入式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 分层设计模型
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 多板级包含关系图
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 覆盖替代复制粘贴,避免维护不一致。