3、设备树文件结构:DTS、DTSI、DTB、DTC工具的作用与关系,以及编译和反编译流程

设备树(Device Tree)是嵌入式Linux系统中描述硬件资源的核心机制,它将板级硬件细节从内核代码中剥离,以结构化数据的形式传递给内核。在实际工程中,开发者接触到的并非单一文件,而是一套由DTS源文件、DTSI公共包含文件、DTB二进制文件以及DTC编译工具组成的完整工具链。理解这四者之间的转换关系与编译流程,是进行设备树开发、调试与移植的第一道门槛。本章将系统拆解设备树的文件家族,剖析其作用边界,并通过完整的编译/反编译实战演示,帮助读者建立从源码到二进制再到内核解析的完整认知链路。

设备树文件家族与编译链路 源文件(文本) .dts(板级源文件) .dtsi(SoC公共包含) DTC 编译器 dtc -I dts -O dtb 二进制产物 .dtb(编译产物) .dts(反编译产物) #include 编译 DTC 反编译器 dtc -I dtb -O dts 反编译 生成 关键路径: ① 开发时:.dts + .dtsi --(DTC)--> .dtb --(Bootloader加载)--> 内核解析 ② 调试时:.dtb --(DTC -I dtb -O dts)--> .dts(反编译用于逆向分析)

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格式(用于验证)。其核心能力在于:词法/语法分析、符号解析、节点合并、属性覆盖、二进制打包。

核心原理总结:DTSI是“芯片手册”,DTS是“板卡原理图”,DTB是“烧录到板上的固件”,DTC是“编译工具”。开发者的日常工作就是编写/修改DTS,通过DTC生成DTB,再交由Bootloader加载。

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布局
经验提醒:在编译DTS时,务必使用与目标内核版本匹配的DTC。内核源码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索引,不影响功能,但不利于人工阅读。
风险警告:不要直接修改反编译生成的DTS作为正式源码。反编译产物丢失了label、注释和宏定义,且节点顺序可能变化,直接使用会导致后续维护困难。正确做法是始终维护原始DTS/DTSI,将反编译仅用于调试比对。

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 编译错误排查清单

  1. Error: memory reservation 0x... too large:检查/memreserve/语法或DTS中reg属性是否越界。
  2. Warning: unit_address_vs_reg:节点名包含地址但reg缺失,或反之。需统一命名规范。
  3. Error: phandle 0x... already exists:手动指定phandle冲突,应删除显式phandle,让DTC自动分配。
  4. 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反编译,与预期配置比对,确保无意外属性丢失。
总结复盘:设备树文件体系的核心是“分层描述、增量覆盖、二进制交付”。DTSI定义SoC共性,DTS定义板级特性,DTC完成从文本到二进制的转换,DTB成为内核与Bootloader之间的硬件契约。掌握编译/反编译流程,不仅是为了生成DTB,更是为了在调试时能够逆向验证内核实际看到的硬件视图。建议开发者在每次硬件改动后,都执行一次“修改DTS → 编译 → 反编译比对 → 烧录验证”的完整闭环,这是避免低级配置错误的最有效手段。