3、设备树文件结构:.dts、.dtsi、.dts.pre、.dtb文件的区别与作用,include机制
说到设备树的文件结构,很多初学者第一眼看到那一堆后缀名就懵了。.dts、.dtsi、.dts.pre、.dtb……这到底都是些什么鬼?
别急,我当年刚接触Zephyr时也被绕晕过。后来在项目里踩了几次坑,才算真正搞明白。今天咱们就把这几个文件类型彻底说清楚。
3.1 四种文件类型,各司其职
先给个总览表格,方便你快速对照:
| 后缀名 | 全称 | 作用 | 是否可读 |
|---|---|---|---|
.dts |
Device Tree Source | 设备树源文件,描述硬件 | 是(文本) |
.dtsi |
Device Tree Source Include | 可被包含的公共定义文件 | 是(文本) |
.dts.pre |
Device Tree Preprocessed | 预处理后的中间文件 | 是(文本) |
.dtb |
Device Tree Blob | 编译后的二进制文件 | 否(二进制) |
3.2 .dts 文件:真正的“主菜”
.dts 文件是设备树的源文件。说白了,它就是描述一块具体板子上所有硬件信息的文本文件。
每个板子通常都有一个对应的 .dts 文件。比如 nrf52840dk_nrf52840.dts,一看名字就知道是给 nRF52840 DK 开发板用的。
我个人习惯把 .dts 文件想象成“板子的身份证”。它里面写了:
- CPU 是什么型号
- 内存有多大
- 哪些外设被使能了
- GPIO 引脚怎么分配的
- 时钟频率是多少
举个例子,一个简单的 .dts 文件片段长这样:
/dts-v1/;
#include "nrf52840.dtsi"
/ {
model = "Nordic nRF52840 DK";
compatible = "nordic,nrf52840-dk";
chosen {
zephyr,console = &uart0;
};
&uart0 {
status = "okay";
current-speed = <115200>;
};
};
看到没?它用 #include 引用了另一个文件。这就是咱们接下来要说的 .dtsi。
3.3 .dtsi 文件:公共定义的“仓库”
.dtsi 文件,全称是 Device Tree Source Include。它存在的意义就一个:复用。
你想想看,同一颗芯片可能用在几十种不同的板子上。如果每个板子的 .dts 文件都从头写一遍芯片的引脚定义、时钟配置……那不得累死?
所以 .dtsi 文件就充当了“公共仓库”的角色。芯片厂商会把芯片的通用定义放在 .dtsi 里,板子厂商只需要 #include 一下,再补充板子特有的配置就行了。
我在项目中遇到过这样的情况:有个同事把芯片的引脚定义直接写在了 .dts 文件里,结果换了块板子,所有东西都得重写。嗯,从那以后我就养成了习惯——能用 .dtsi 复用的,绝不写在 .dts 里。
.dtsi 文件通常放在 dts/arm/ 或 dts/riscv/ 等目录下。你可以把它理解成 C 语言里的头文件(.h),但语法上用的是设备树语言。
3.4 .dts.pre 文件:预处理后的“半成品”
这个文件你可能平时不太注意,但它其实挺重要的。
.dts.pre 是 .dts 文件经过 C 预处理器(CPP)处理后的结果。什么意思呢?就是所有 #include、#define、#ifdef 这些预处理指令都被展开和解析了。
为什么要生成这个中间文件?
- 方便调试:如果编译出错,你可以打开
.dts.pre看看最终展开成什么样了 - 方便检查:确认
#include的路径对不对,宏定义有没有生效
我曾经有一次死活编译不过,查了半天发现是 .dtsi 文件路径写错了。后来养成习惯,先看一眼 .dts.pre,确认所有 #include 都正确展开了,再往下排查。
.dts.pre 是构建过程中自动生成的临时文件。你不需要手动创建它,但学会看它,能帮你省下不少调试时间。
3.5 .dtb 文件:机器能读懂的“二进制”
最后这个 .dtb,是设备树的最终形态——二进制文件。
Zephyr 的构建系统会把 .dts 文件(经过预处理后)编译成 .dtb 文件。这个二进制文件可以直接被 bootloader 或内核解析,用来初始化硬件。
说白了,.dts 是给人看的,.dtb 是给机器吃的。
你可以用 dtc 工具把 .dtb 反编译回 .dts:
dtc -I dtb -O dts -o output.dts input.dtb
这个命令我偶尔会用,比如拿到一块别人编译好的固件,想看看它的设备树配置。
3.6 include 机制:设备树的“模块化”之道
Zephyr 的设备树 #include 机制,其实和 C 语言的 #include 是同一套东西。因为设备树源文件在编译前会先经过 C 预处理器处理。
这意味着你可以:
- 用
#include "nrf52840.dtsi"引入芯片定义 - 用
#include "nrf52840dk_common.dtsi"引入板级公共配置 - 用
#define定义宏,然后在.dts里使用 - 用
#ifdef做条件编译
举个例子,Zephyr 的 nrf52840dk_nrf52840.dts 文件里,你经常能看到这样的结构:
/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include "nrf52840dk_nrf52840_common.dtsi"
/ {
model = "Nordic nRF52840 DK";
compatible = "nordic,nrf52840-dk";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
zephyr,uart-mcumgr = &uart0;
};
};
这里 #include 了两层:
- 芯片级:
nrf52840_qiaa.dtsi定义了芯片内部所有外设 - 板级:
nrf52840dk_nrf52840_common.dtsi定义了这块板子的引脚分配和默认配置
最后在 .dts 文件里,只需要写板子特有的配置(比如 chosen 节点)就行了。
3.7 避坑指南:我踩过的几个坑
最后分享几个我实际项目中遇到的坑,希望能帮你少走弯路:
- 路径问题:
#include的路径是相对于dts/目录的,不是相对于当前文件。我第一次用的时候就在这上面栽了跟头。 - 重复定义:如果你在
.dts和.dtsi里定义了同一个节点,.dts里的会覆盖.dtsi里的。这个特性可以用来做板级定制。 - 预处理顺序:设备树的预处理是在编译之前进行的。如果你在
.dts里用了#define,记得它只在当前文件有效,不会影响到被#include进来的.dtsi文件。
嗯,关于设备树的文件结构,今天就聊到这儿。下一节咱们会深入设备树的语法和节点结构,到时候你会看到这些文件是怎么组织成一颗完整的“树”的。