设备树基础: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里,省得每个板子都重复写一遍。
编译流程:从文本到二进制
整个流程其实就三步,我画了个图帮你理解:
实际编译的时候,你可能会看到这样的命令:
# 最基本的编译
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为例,流程是这样的:
- U-Boot从存储介质(NAND、SD卡、网络)加载DTB到内存
- U-Boot把DTB的物理地址通过寄存器
r2传给内核(ARM架构) - 内核启动时,在
setup_arch()中解析DTB - 内核遍历DTB中的节点,创建platform_device
- 驱动匹配后,probe函数被调用
这里有个关键点:DTB必须放在内核能访问到的内存区域。我曾经在调试时把DTB放到了某个保留内存区域,结果内核解压时直接覆盖了它,板子就卡死了。嗯,从那以后我都在U-Boot里用fdt addr命令确认DTB的加载地址。
常见的编译错误与避坑
我整理了几个常见的编译错误,你遇到了别慌:
- "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