一、设备树基础回顾:DTS、DTC、DTB 文件格式与编译流程

说实话,很多嵌入式工程师一开始接触设备树,都觉得它挺玄乎的。我当年刚转做 Linux 驱动时,也被这玩意儿绕晕过。但说白了,设备树就是一套描述硬件信息的“说明书”。它告诉内核:你有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么配。

今天咱们就把 DTS、DTC、DTB 这三兄弟彻底捋清楚。嗯,先从文件格式说起。

1.1 DTS:人类可读的硬件描述

DTS 是 Device Tree Source 的缩写。它是文本文件,用树形结构描述硬件。我习惯把 DTS 比作一张“硬件清单”,每个节点就是一个设备。

来看一个最简单的例子:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a8";
            reg = <0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  /* 256 MB */
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "okay";
    };
};

这里有几个关键点:

  • 根节点:/ 是整个树的起点,相当于硬件系统的根
  • 节点名:比如 cpu@0,@ 后面是地址或单元号
  • 属性:compatible、reg、interrupts 这些都是属性,描述设备特性
  • 标签:uart0: 是标签,方便在其他地方引用这个节点
我的小习惯:写 DTS 时,我总会在每个节点后面加个注释,标明这个设备在原理图上的编号。比如 /* UART0, J1-4 */。调试时能省一半时间。

1.2 DTC:把文本变成二进制

DTC 是 Device Tree Compiler 的缩写。它的工作很简单——把 DTS 编译成 DTB。我刚开始用的时候,总觉得这步可有可无,直到有一次我手动改了 DTS 却忘了重新编译……嗯,那次调试花了我整整一个下午。

编译命令长这样:

dtc -I dts -O dtb -o am335x-boneblack.dtb am335x-boneblack.dts

参数说明:

  • -I dts:输入格式是 DTS
  • -O dtb:输出格式是 DTB
  • -o:指定输出文件名

反过来,你也可以把 DTB 反编译成 DTS:

dtc -I dtb -O dts -o am335x-boneblack.dts am335x-boneblack.dtb
注意:反编译出来的 DTS 会丢失注释和标签名。我曾经靠这个特性抓过一个同事的 bug——他直接在 DTB 上改,然后反编译回来当 DTS 提交,结果代码评审时被我一眼看穿。

1.3 DTB:内核真正吃的东西

DTB 是 Device Tree Blob 的二进制格式。它被烧录到 Flash 里,或者由 bootloader 加载到内存中,然后传给内核。

DTB 的结构其实挺规整的,我画了一张图帮你理解:

DTB 文件结构 DTB 头部(Header)—— 魔数、大小、版本、偏移量 内存预留块(Memory Reservation Block)—— 哪些内存区域不能被内核使用 结构块(Structure Block)—— 节点和属性的树形结构 包含:根节点 → 子节点 → 属性(compatible, reg, interrupts 等) 字符串块(Strings Block)—— 所有属性名的字符串表 每个块都按 4 字节对齐,保证内核能快速解析

你看,DTB 分四个部分:

  1. 头部:记录魔数(0xd00dfeed)、总大小、版本号、各块的偏移量
  2. 内存预留块:告诉内核哪些内存区域不能碰,比如 GPU 或 DSP 用的内存
  3. 结构块:核心内容,节点和属性的树形结构,用 token 分隔
  4. 字符串块:所有属性名的字符串,结构块里只存偏移量,节省空间

为什么这么设计? 内核启动时,DTB 被映射到内存中,内核直接解析结构块和字符串块,不需要额外的内存分配。说白了,就是为嵌入式系统“省吃俭用”设计的。

1.4 编译流程:从 DTS 到 DTB 的完整链路

整个流程其实就三步,但每一步都有坑。我画个流程图:

DTS 源文件 .dts + .dtsi 包含文件 预处理 DTC 编译 dtc 工具 生成 DTB 二进制 .dtb 文件 加载 内核 实际项目中,DTS 通常通过内核构建系统编译: make dtbs 或 在 defconfig 中指定 CONFIG_OF

实际项目中,DTS 文件通常不是孤立的。你会看到这样的文件结构:

arch/arm/boot/dts/
├── am33xx.dtsi          # SoC 级通用定义
├── am335x-bone.dtsi     # BeagleBone 通用定义
├── am335x-boneblack.dts # BeagleBone Black 具体配置
└── am335x-bonegreen.dts # BeagleBone Green 具体配置

这里用到了 .dtsi 文件,它是“设备树包含文件”。我习惯把 SoC 的通用外设定义放在 .dtsi 里,板级差异放在 .dts 里。这样复用性极高。

避坑指南:我曾经在一个项目里,把 am33xx.dtsi 里的 UART 地址写错了,结果所有基于 AM335x 的板子串口都打不开。排查了三天才发现是 .dtsi 的问题。从那以后,我每次改 .dtsi 都会在提交信息里加个 [COMMON] 前缀,提醒自己和团队——这玩意儿会影响所有板子。

1.5 编译过程中的常见问题

问题 现象 原因 解决方法
语法错误 DTC 报错,编译失败 漏了分号、括号不匹配 用 dtc -I dts -O dtb -o /dev/null 只做语法检查
地址冲突 两个设备用同一个地址 reg 属性写重复了 检查原理图,确认地址唯一
中断号错误 设备不响应中断 interrupts 属性值不对 查 SoC 手册,确认中断号映射
包含文件缺失 找不到 .dtsi 文件 路径或文件名写错 用 -I 参数指定搜索路径
特别提醒:DTC 的版本会影响编译结果。老版本 DTC 可能不支持某些新语法(比如 /dts-v1/; 声明)。我建议统一使用内核源码树里的 DTC,版本跟着内核走,最保险。

好了,设备树的基础回顾就到这里。DTS、DTC、DTB 这三者的关系,说白了就是“写文本 → 编译 → 二进制加载”。理解了这个流程,后面讲多平台复用策略时,你就能明白为什么有些做法行得通,有些行不通。

记住一句话:设备树是给内核看的,不是给人看的。所以写 DTS 时,多想想内核会怎么解析它,而不是你觉得怎么好看。

核心要点总结:

  • DTS 是源文件,用树形结构描述硬件
  • DTC 是编译器,把 DTS 转成 DTB
  • DTB 是二进制文件,内核启动时解析
  • .dtsi 用于复用,.dts 用于板级差异
  • 编译流程:DTS → 预处理 → DTC → DTB → 内核

可可精品资料库