3、设备树文件结构:DTS、DTSI、DTB、DTC工具的作用与关系,以及编译和反编译流程
设备树(Device Tree)是嵌入式Linux系统中描述硬件资源的核心机制,它将板级硬件细节从内核代码中剥离,以结构化数据的形式传递给内核。在实际工程中,开发者接触到的并非单一文件,而是一套由DTS源文件、DTSI公共包含文件、DTB二进制文件以及DTC编译工具组成的完整工具链。理解这四者之间的转换关系与编译流程,是进行设备树开发、调试与移植的第一道门槛。本章将系统拆解设备树的文件家族,剖析其作用边界,并通过完整的编译/反编译实战演示,帮助读者建立从源码到二进制再到内核解析的完整认知链路。
3.1 文件家族核心原理:DTS、DTSI、DTB、DTC的职责边界
设备树的文件体系设计遵循“公共抽象与板级特化分离”的原则。理解这一设计哲学,是掌握整个设备树开发流程的基础。
3.1.1 DTS(Device Tree Source):板级硬件描述源文件
DTS是设备树的顶层源文件,通常以board-name.dts命名(如imx6ull-myboard.dts)。它描述了一款具体开发板或产品的完整硬件拓扑,包括:
- CPU型号与数量:通过
cpus节点定义核心架构、频率、缓存信息。 - 内存布局:
memory节点指定DDR起始地址与大小。 - 外设实例化:使能或禁用SoC内部IP(如UART、I2C、SPI、GPIO、DMA等),并配置其寄存器地址、中断号、时钟频率、引脚复用。
- 板级特有资源:如外部PHY芯片型号、LCD面板参数、WiFi模组接口、按键/ LED对应的GPIO等。
一个典型的DTS文件结构如下:
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "MyBoard i.MX6ULL Board";
compatible = "myboard,imx6ull", "fsl,imx6ull";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB DDR */
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
};
3.1.2 DTSI(Device Tree Source Include):SoC级公共定义文件
DTSI是被DTS包含的公共头文件,通常以soc-name.dtsi命名(如imx6ull.dtsi)。它由芯片原厂(如NXP、Rockchip、Allwinner)提供,描述了同一颗SoC所有型号共有的硬件资源:
- CPU集群、GIC中断控制器、定时器、看门狗等基础IP。
- 所有外设控制器的默认状态(通常为
disabled),仅定义寄存器地址、中断号、时钟ID等静态属性。 - SoC级引脚复用(pinctrl)的默认分组。
DTSI通过C预处理器(CPP)的#include机制被DTS引用。这种设计避免了每个板级文件重复编写数百行SoC公共代码,同时允许板级文件通过&节点名语法对DTSI中的节点进行增量修改(如追加属性、覆盖状态)。
3.1.3 DTB(Device Tree Blob):内核实际消费的二进制格式
DTB是DTS经过DTC编译后生成的二进制扁平设备树。Bootloader(如U-Boot)在启动内核前,会将DTB从存储介质(eMMC、SD卡、网络)加载到内存中,并通过寄存器(如ARM的r2)将DTB物理地址传递给内核。内核在启动早期解析DTB,构建出struct device_node树,进而驱动设备模型。
DTB格式具有以下特点:
- 紧凑性:采用大端序、对齐的二进制结构,去除了注释与空白,体积通常比DTS小5-10倍。
- 可校验性:头部包含
magic(0xd00dfeed)、totalsize、off_dt_struct等字段,内核启动时会进行完整性校验。 - 平台无关性:同一份DTB可用于不同Bootloader,只要遵循FDT(Flattened Device Tree)规范。
3.1.4 DTC(Device Tree Compiler):编译与反编译的瑞士军刀
DTC是设备树工具链的核心可执行程序,通常位于Linux内核源码的scripts/dtc/目录下,或作为独立软件包(device-tree-compiler)安装。它支持三种输入输出格式:
| 格式标识 | 说明 | 典型扩展名 |
|---|---|---|
dts | 设备树源文件(文本) | .dts |
dtb | 设备树二进制(编译产物) | .dtb |
dts(输出) | 反编译后的源文件(带/dts-v1/;头) | .dts |
DTC还支持fs格式(用于与SysFS交互)和yaml格式(用于验证)。其核心能力在于:词法/语法分析、符号解析、节点合并、属性覆盖、二进制打包。
3.2 编译与反编译操作流程:从源码到二进制再回溯
掌握DTC的调用参数与Makefile集成方式,是设备树开发的基本功。以下展示标准编译流程与调试反编译流程。
3.2.1 标准编译流程(DTS → DTB)
在Linux内核源码树中,设备树的编译通常由Kbuild系统自动完成。但手动编译有助于理解底层机制。基本命令如下:
# 1. 预处理(展开#include和宏)
cpp -nostdinc -I <内核源码目录>/include \
-I <内核源码目录>/arch/arm/boot/dts \
-undef -D__DTS__ -x assembler-with-cpp \
myboard.dts myboard.dts.preprocessed
# 2. 编译(生成DTB)
dtc -I dts -O dtb -o myboard.dtb myboard.dts.preprocessed
# 3. 一步到位(推荐,跳过中间文件)
dtc -I dts -O dtb -o myboard.dtb myboard.dts
实际工程中,更常使用内核的make dtbs命令:
make ARCH=arm myboard.dtb # 在arch/arm/boot/dts/目录下生成
该命令会自动处理#include依赖、DTC_CFLAGS以及CONFIG_*宏的传递。
3.2.2 反编译流程(DTB → DTS)
当拿到一个未提供源码的DTB(如从开发板中提取),或需要确认内核实际解析的硬件配置时,反编译是逆向分析的关键手段:
# 反编译为可读的DTS
dtc -I dtb -O dts -o decompiled.dts myboard.dtb
# 同时输出符号表(可选)
dtc -I dtb -O dts -@ -o decompiled_with_symbols.dts myboard.dtb
反编译产物与原始DTS的差异:
- 所有
#include已被展开,节点层级扁平化。 - 属性值以十六进制数组形式呈现(如
reg = <0x80000000 0x20000000>变为reg = <0x80000000 0x20000000>,但phandle引用会保留)。 - 节点名称可能被重命名(如
uart1变为serial@02020000),因为编译时丢失了label信息。
3.2.3 编译选项与调试技巧
| DTC选项 | 作用 | 典型场景 |
|---|---|---|
-@ | 生成符号节点(__symbols__) | 允许运行时通过of_platform_populate动态修改 |
-H | 生成人类可读的十六进制输出 | 调试属性值 |
-V | 指定DTB版本(如-V 17) | 兼容旧Bootloader |
-Wno-unit_address_vs_reg | 忽略地址格式警告 | 处理历史遗留DTS |
-O asm | 生成汇编文件 | 深度调试FDT布局 |
scripts/dtc/下的DTC版本最可靠。系统自带的dtc可能因版本过旧而无法解析新语法(如#include支持、delete-node操作)。
3.3 实战案例:从零编译与反编译i.MX6ULL开发板设备树
本案例以NXP i.MX6ULL EVK开发板为背景,演示完整的编译-反编译-验证闭环。
3.3.1 案例背景与初始目标
某项目需要在一块第三方设计的i.MX6ULL核心板上添加一个I2C接口的触摸屏控制器(GT911)。原厂BSP提供了imx6ull-evk.dts,但未包含GT911节点。开发者的目标是:修改DTS,编译生成新DTB,并验证触摸屏能否被内核识别。
3.3.2 操作步骤与关键参数
步骤1:定位并复制源文件
cd <kernel>/arch/arm/boot/dts/
cp imx6ull-evk.dts myboard.dts
cp imx6ull-14x14-evk.dtsi myboard.dtsi # 假设使用14x14封装
步骤2:修改DTS,添加GT911节点
# 在myboard.dtsi中追加:
&i2c2 {
status = "okay";
clock-frequency = <100000>;
gt911@5d {
compatible = "goodix,gt911";
reg = <0x5d>;
interrupt-parent = <&gpio1>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
irq-gpios = <&gpio1 13 GPIO_ACTIVE_HIGH>;
touchscreen-inverted-x;
touchscreen-inverted-y;
};
};
步骤3:编译DTB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard.dtb
编译输出:
DTC arch/arm/boot/dts/myboard.dtb
CHK arch/arm/boot/dts/myboard.dtb
步骤4:反编译验证
dtc -I dtb -O dts -o verify.dts arch/arm/boot/dts/myboard.dtb
grep -A 15 "gt911" verify.dts
反编译输出片段:
gt911@5d {
compatible = "goodix,gt911";
reg = <0x5d>;
interrupt-parent = <0x2>; /* phandle引用 */
interrupts = <0xd 0x8>;
reset-gpios = <0x3 0x7 0x0>;
irq-gpios = <0x2 0xd 0x1>;
touchscreen-inverted-x;
touchscreen-inverted-y;
};
步骤5:烧录与内核日志验证
# 将myboard.dtb烧录到SD卡boot分区,启动后查看:
dmesg | grep -i goodix
# 预期输出:
# input: Goodix Capacitive TouchScreen as /devices/platform/soc/21a4000.i2c/i2c-1/1-005d/input/input0
3.3.3 案例复盘与风险提示
- 引脚冲突风险:GT911的IRQ引脚(GPIO1_IO13)可能与LCD复位或SDIO复用冲突,需查阅原理图确认。
- 中断触发类型:GT911支持上升沿/下降沿,需与驱动匹配。若内核日志显示
irq 32: nobody cared,则需检查interrupts属性。 - phandle变化:反编译后
interrupt-parent变为数字0x2,这是编译时生成的phandle索引,不影响功能,但不利于人工阅读。
3.4 常见误区与总结复盘
在设备树文件管理实践中,以下误区高频出现,需要特别警惕:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 将DTSI当作板级文件直接修改 | 污染SoC公共定义,影响同系列所有板卡 | DTSI只由原厂维护,板级修改通过DTS中的&节点覆盖 |
忽略#include的路径依赖 | 编译报错找不到头文件 | 使用cpp -I指定include路径,或依赖内核Kbuild自动处理 |
| 使用系统自带dtc编译内核DTS | 语法不兼容或生成错误DTB | 优先使用内核scripts/dtc/dtc,或安装匹配版本 |
| 反编译后直接回编 | 丢失label导致&uart1引用失效 | 反编译仅用于查看,修改必须回到原始DTS |
忘记/dts-v1/;版本头 | DTC报错或生成旧格式DTB | 所有DTS第一行必须包含/dts-v1/; |
3.4.1 编译错误排查清单
- Error: memory reservation 0x... too large:检查
/memreserve/语法或DTS中reg属性是否越界。 - Warning: unit_address_vs_reg:节点名包含地址但
reg缺失,或反之。需统一命名规范。 - Error: phandle 0x... already exists:手动指定
phandle冲突,应删除显式phandle,让DTC自动分配。 - Error: label 'xxx' already exists:DTSI与DTS中定义了同名label,需重命名。
3.4.2 工程管理最佳实践
- 版本控制:DTS/DTSI必须纳入Git管理,每次硬件改版对应一次提交。
- 命名规范:板级DTS采用
vendor-board-rev.dts格式,如myboard-rev2.dts。 - 构建集成:在Makefile中通过
dtb-$(CONFIG_SOC_IMX6ULL) += myboard.dtb添加目标。 - 验证闭环:每次编译后执行
dtc -I dtb -O dts反编译,与预期配置比对,确保无意外属性丢失。