设备树基础:DTS、DTC、DTB 三者的关系与编译流程

说实话,我刚接触嵌入式Linux那会儿,被设备树这三个缩写搞得一头雾水。DTS、DTC、DTB,长得像三胞胎,但干的活完全不一样。今天咱们就把它们掰开揉碎了讲清楚。

先搞懂它们各自是干什么的

我习惯用一个比喻来理解这三者的关系:DTS是源代码,DTB是编译后的二进制,DTC就是那个编译器。就像你写C程序,.c文件经过gcc编译成.o文件一样。

  • DTS(Device Tree Source):设备树源文件,文本格式,人能看懂
  • DTB(Device Tree Blob):设备树二进制文件,机器能看懂
  • DTC(Device Tree Compiler):把DTS变成DTB的工具

嗯,这里要注意一点:DTS文件里还可以通过#include包含.dtsi文件。.dtsi是什么?说白了就是设备树的头文件,通常放一些SoC通用的定义。我在项目中经常把GPIO控制器、时钟控制器这些公共部分放到.dtsi里,省得每个板子都重复写一遍。

编译流程:从文本到二进制

整个流程其实就三步,我画了个图帮你理解:

.dts 源文件 .dtsi 头文件 DTC 编译器 .dtb 二进制 文本格式,人类可读 dtc 工具 二进制格式,内核解析 编译命令:dtc -I dts -O dtb -o output.dtb input.dts

实际编译的时候,你可能会看到这样的命令:

# 最基本的编译
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 带调试信息的编译(我调试时常用)
dtc -I dts -O dtb -o myboard.dtb myboard.dts -@

# 反编译:把DTB还原成DTS(查问题利器)
dtc -I dtb -O dts -o myboard.dts myboard.dtb

我曾经遇到过一个坑:板子死活起不来,查了半天发现是DTS里一个GPIO的极性写反了。后来我养成了习惯,每次改完DTS先用dtc -@编译,再用fdtdump看看生成的DTB结构对不对。

DTS文件长什么样?

给你看个实际项目中用到的DTS片段,我稍微简化了一下:

/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "MyBoard i.MX6ULL";
    compatible = "mycompany,imx6ull-myboard", "fsl,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

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

    leds {
        compatible = "gpio-leds";
        led0 {
            label = "heartbeat";
            gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            linux,default-trigger = "heartbeat";
        };
    };
};

&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

看到/dts-v1/;这行了吗?这是版本声明,告诉DTC用v1语法解析。我见过新手忘了写这行,编译直接报错。

DTC编译器的那些事儿

DTC其实是个很轻量的工具,源码在Linux内核的scripts/dtc/目录下。你编译内核的时候,它会被自动编译出来。我个人习惯单独编译一个dtc放系统里,方便调试:

# 从内核源码编译dtc
make scripts/dtc
cp scripts/dtc/dtc /usr/local/bin/

DTC支持一些有用的选项,我列个表给你参考:

选项 作用 我什么时候用
-@ 生成符号节点(phandle) 需要动态加载设备树插件时
-H 指定phandle格式(epapr或legacy) 兼容老旧bootloader时
-Wno-unit_address_vs_reg 忽略某些警告 处理第三方DTS时经常用
-O dtb 输出DTB格式 默认行为,但显式写出更安全
小技巧:dtc -I dtb -O dts反编译的时候,加上-H epapr可以生成更易读的phandle格式。我反编译别人的DTB时必加这个选项。

DTB在内核中的加载流程

你想想看,DTB编译好了,bootloader怎么把它传给内核的?

以U-Boot为例,流程是这样的:

  1. U-Boot从存储介质(NAND、SD卡、网络)加载DTB到内存
  2. U-Boot把DTB的物理地址通过寄存器r2传给内核(ARM架构)
  3. 内核启动时,在setup_arch()中解析DTB
  4. 内核遍历DTB中的节点,创建platform_device
  5. 驱动匹配后,probe函数被调用

这里有个关键点:DTB必须放在内核能访问到的内存区域。我曾经在调试时把DTB放到了某个保留内存区域,结果内核解压时直接覆盖了它,板子就卡死了。嗯,从那以后我都在U-Boot里用fdt addr命令确认DTB的加载地址。

常见的编译错误与避坑

注意:DTS对语法非常敏感,少个分号或者括号不匹配,DTC直接报错退出。我建议你在编辑DTS时用支持语法高亮的编辑器,比如VSCode装个Device Tree插件。

我整理了几个常见的编译错误,你遇到了别慌:

  • "syntax error":多半是少了分号或大括号不匹配。检查每个节点的结尾
  • "FATAL: Input tree has errors":DTC发现结构问题,比如reg属性长度不对
  • "WARNING: unit_address_vs_reg":节点名带地址但reg属性不匹配,可以忽略但最好修正
  • "Error: phandle 0x... not found":引用了不存在的节点,检查&引用是否正确

我曾经在项目交付前夜遇到一个诡异问题:同样的DTS,在A开发板上正常,在B板子上就编译不过。查了半天发现是B板子的DTC版本太老,不支持某些新语法。所以我的建议是:统一DTC版本,最好用内核源码自带的

总结一下

DTS、DTC、DTB这三者的关系,说白了就是源码、编译器、目标文件。你写DTS的时候,心里要清楚它最终会被编译成DTB,然后被内核解析。调试的时候,反编译DTB看看实际内容,往往能发现一些隐蔽的问题。

下一节我们会深入DTS的语法细节,包括节点、属性、中断映射这些实战中天天要打交道的东西。到时候我会拿几个我踩过的坑出来分享,保证让你少走弯路。


公众号:蓝海资料掘金营,微信deep3321