一:设备树的前世今生
导语: 在嵌入式Linux的世界里,从海量的板级C文件到"设备树(Device Tree)"重构了Linux内核框架。今天,我们就来聊聊设备树的前世今生。
一、 前世:那些年,我们"硬编码"的痛
在早期的Linux内核(3.x之前),你要为一款新的开发板编写BSP(板级支持包),你通常会怎么做?
答案是:写一大堆的C代码。
硬件信息是直接写死在内核源码里的。如果你想描述一块开发板上的UART、I2C、SPI或者GPIO,必须在 arch/arm/mach-xxx/ 目录下创建一个专门的C文件。
你会看到成百上千行的代码,用来定义:
-
寄存器的物理地址
-
中断号(IRQ)
-
时钟频率
-
各种GPIO的引脚复用关系
这带来了什么?
-
代码冗余度极高:
假设有两款开发板,仅仅因为LED灯接的GPIO引脚不同,必须在内核中维护两份几乎一模一样的代码。
-
内核极度臃肿:
随着ARM生态的爆发,出现了成千上万款ARM开发板。每一款板子的硬件细节都要写进内核,Linux内核将变成一个巨大的"垃圾场",充满了各种
xxxx_board.c。 -
升级维护困难:
每次硬件改版(哪怕换了一个电阻),软件工程师都要重新修改内核代码、重新编译内核。
2011年3月17日Linux之父Linus Torvalds抨击ARM的代码:"This whole ARM thing is a fcking pain in the ass.*"。
他要求ARM开发者停止这种疯狂的"复制粘贴"行为,寻找一种描述硬件的统一方法。
二、 转机:设备树的诞生与引入
为了解决"描述硬件"与"内核代码"强耦合的问题,PowerPC和OpenFirmware(IEEE 1275)社区提出的**设备树(Device Tree)**概念被引入到了Linux内核中。
什么是设备树?
简单说,设备树是一种描述硬件资源的数据结构。
它借鉴了树状结构的思想,将CPU、内存、总线、外设等硬件信息,以一种**文本文件(.dts)**的形式独立出来。
核心思想是:"内核代码只负责跑逻辑,硬件长什么样,去问设备树。"
三、 今生:Linux内核的"瘦身革命"
自Linux 3.x版本开始,ARM架构全面拥抱设备树。这一变革被称为Linux内核的"瘦身革命"。
1. 它是如何工作的?
-
硬件工程师:
设计好电路图。
-
软件工程师:
编写
.dts(Device Tree Source) 文件,用特定的语法描述电路连接。 -
编译阶段:
使用
dtc(Device Tree Compiler) 工具将.dts编译成.dtb(Device Tree Blob) 二进制文件。 -
启动阶段:
Bootloader(如U-Boot)将
dtb文件加载到内存中,并把它的地址告诉Linux内核。 -
内核运行:
内核启动时解析
dtb文件,动态地生成platform_device,并匹配相应的驱动。
2. 设备树的"三剑客"
-
DTS (Device Tree Source):
设备树源文件,人类可读的文本格式,类似于C语言的
.c文件。 -
DTC (Device Tree Compiler):
设备树编译器,用于将
.dts编译成.dtb。 -
DTB (Device Tree Blob):
编译后的二进制文件,由Bootloader传递给内核。
3. 带来的好处
-
解耦:
驱动代码和硬件配置彻底分离。同一个LCD驱动,既可以用于A板,也可以用于B板,只要换一个
.dtb文件即可。 -
精简:
内核源码中删除了海量的
mach-xxx板级文件,内核变得更加干净。 -
统一:
无论是ARM、PowerPC还是RISC-V,都采用了统一的设备树机制来描述硬件。
四、 总结
设备树的出现,是Linux嵌入式开发史上的一个重要里程碑。
它把软件工程师从繁重的"板级代码维护"中解放出来,让我们能够更加专注于驱动逻辑本身。
二:设备树编写方法和举例
设备树编写2个总体思路:
1、设备树编写只描述硬件资源的物理信息(地址、中断、时钟等),不写任何驱动逻辑;
2、设备树可遵循dtsi和dts分层定义,dtsi描述 SoC 内部所有硬件资源,dts描述板级硬件构成和配置。编写过程中,compatible、reg、status 是硬件或配置对象的三个核心属性,其余参数按绑定文档查表填空。
绑定文档见:内核源码根目录/Documentation/devicetree/bindings/
既然设备树编写遵循dtsi和dts分层定义,那首先清晰它们的职责定位
| 文件 | 定位 | 内容 |
|---|---|---|
.dtsi |
芯片级(SoC) | 描述芯片内部所有硬件模块,默认 status = "disabled" |
.dts |
板级 | #include 引入 dtsi,开启板子上实际使用的设备,补充板级参数 |
为什么分开? 同一颗芯片可以做出无数种板子。芯片里有什么硬件是固定的,写一次放 dtsi 里;板子用了哪些、怎么接的,写在 dts 里。换板子只换 dts,dtsi 不动。
三个核心属性
写设备树时,所有节点归根结底都在回答三个问题:
| 属性 | 回答的问题 | 含义 |
|---|---|---|
compatible |
这个硬件是什么? | 字符串,内核用它匹配对应的驱动程序 |
reg |
这个硬件在哪? | 寄存器基地址和长度,格式 <地址 长度> |
status |
用不用它? | "okay"启用,"disabled" 禁用 |
填好这三个,内核就能找到硬件、加载驱动。其余属性(时钟、中断、引脚等)都是具体外设的附加信息,查内核绑定文档照填即可。
一个完整示例
虚拟 ARM 板,片上有一个串口控制器,板子把它接出来当控制台。
芯片级描述(soc.dtsi):
bash
/ {
#address-cells = <1>;
#size-cells = <1>;
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@fe000000 {
compatible = "ns16550a";
reg = <0xfe0000000x1000>;
status = "disabled";
};
};
};
板级描述(board.dts):
cpp
/dts-v1/;
#include"soc.dtsi"
/ {
model = "Virtual ARM Board";
};
&uart0 {
status = "okay";
clock-frequency = <24000000>;
};
说明:
-
dtsi 里
uart0用compatible声明了硬件身份,reg给出了寄存器物理地址和长度,status设为disabled------芯片有这个硬件,但先关着。 -
dts 里通过
&uart0引用该节点,把status改为"okay"开启设备,并补充了板级晶振频率。 -
内核启动时解析这份设备树,根据
compatible匹配到 8250 串口驱动,根据reg做地址映射,串口就可用了。
总结
设备树不是程序,没有执行逻辑,它只是一份结构化的硬件清单。编程时把握三点:
-
分层
:dtsi 列清单,dts 开开关。
-
三属性
:
compatible找驱动,reg定位硬件,status控制启停。 -
查表填空
:遇到具体外设的额外参数,去内核
Documentation/devicetree/bindings/下查对应绑定文档,照着填。
三:节点核心属性详解
在Linux设备树的编写与理解中,掌握节点核心属性的含义与使用规则,是打通软硬件框架认知的关键一步。
今天以一块典型的板级DTS为例,将几个"出镜率"最高、最容易让人困惑的关键属性逐一解析。
一、先看一个简化的板级DTS源码
bash
/ {
compatible = "myboard,myboard-v1";
#address-cells = <1>;
#size-cells = <1>;
intc: interrupt-controller@40000000 {
compatible = "arm,gic-400";
reg = <0x40000000 0x1000>,
<0x40100000 0x1000>;
interrupt-controller;
#interrupt-cells = <3>;
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x10000000 0x100000>;
uart0: serial@1000 {
compatible = "ns16550a";
reg = <0x1000 0x100>;
interrupt-parent = <&intc>;
interrupts = <0 32 4>;
status = "okay";
};
};
};
短短几十行,涵盖了设备树中最核心的属性,抽取几个详解如下。
二、关键属性详解
1. compatible ------ 设备匹配的"身份证"
简要原理:
compatible 是所有代表设备的节点都需要的,在典型设备树中,这类节点占比超过 90%,它的值是一个字符串列表,用于把设备和驱动程序绑定起来。内核在启动时会扫描设备树,将节点的 compatible 值与已注册的驱动中的 of_match_table 比对,匹配成功则调用驱动的 probe 函数。
使用规则:
-
格式通常为
"厂商,具体型号",例如"fsl,imx6ul-uart"。 -
可以写多个字符串,按兼容性从具体到通用的顺序排列。
-
根节点的
compatible用于内核判断是否支持该板子。
举例:
compatible = "ns16550a";
对应内核驱动中:
bash
static const struct of_device_iduart_of_match[] = {
{ .compatible = "ns16550a" },
{ /* sentinel */ }
};
2. #address-cells 和 #size-cells ------ 地址编码的"尺子"
简要原理:
这两个属性决定了子节点中 reg 属性的地址和长度各用多少个 32 位整数(cell)来表示。它们不是描述资源本身,而是定义"如何解析子节点的 reg"。
-
#address-cells:表示子节点
reg中地址部分的 cell 个数。 -
#size-cells:表示子节点
reg中长度部分的 cell 个数。
使用规则:
-
通常出现在总线节点(如
soc、axi)或根节点。 -
如果
#size-cells = <0>,表示子节点reg中只有地址,没有长度。
举例:
#address-cells = <1>;
#size-cells = <1>;
那么子节点中:
reg = <0x10001000 0x100>;
表示:地址是 0x10001000,长度是 0x100。
3. reg ------ 设备的"资源坐标"
简要原理:
reg(register)属性描述设备使用的硬件资源,通常是寄存器地址范围。它由一系列"地址、长度"对组成,具体如何解读取决于父节点的 #address-cells 和 #size-cells。
使用规则:
-
值是一组 32 位整数,按"地址、长度"成对出现。
-
可以描述多段不连续的地址空间。
举例:
reg = <0x10001000 0x100>;
表示设备寄存器从 0x10001000 开始,长度为 256 字节。
4. ranges ------ 地址翻译的"桥梁"
简要原理:
ranges 属性用于描述子地址空间到父地址空间的映射关系,即"地址翻译"。它为空时表示子地址空间和父地址空间是 1:1 映射;非空时则提供转换规则。
使用规则:
格式为:<子地址 父地址 长度>
-
子地址的 cell 数由当前节点的
#address-cells决定。 -
父地址的 cell 数由父节点的
#address-cells决定。 -
长度的 cell 数由当前节点的
#size-cells决定。
注意项:
-
如果
ranges属性存在但为空,表示映射是透传的(1:1)。 -
如果完全没有
ranges属性,则表示子节点不能直接从父节点地址空间访问(通常用于 DMA 场景)。
举例:
ranges = <0x0 0x10000000 0x10000>;
含义:子地址空间 0x0 映射到父地址空间 0x10000000,映射长度为 0x10000。
那么子节点 uart0 的 reg = <0x10001000 0x100> 经过翻译后,实际物理地址为 0x10000000 + 0x10001000 = 0x20001000(注意:此处仅为示例逻辑,实际子地址 0x10001000 已超出映射范围,真实 DTS 中需确保子地址落在映射长度内)。
5. interrupt-controller ------ 中断控制器的"身份牌"
简要原理:
interrupt-controller 是标记一个节点是否为中断控制器 的布尔属性。在 Linux 设备树中,只要一个硬件模块负责接收、仲裁并向 CPU 递送中断(例如 ARM GIC、SoC 内部 INTC、GPIO 中断控制器等),就必须包含该属性。
内核在启动阶段解析设备树时,识别所有带有 interrupt-controller 属性的节点,并为它们建立 IRQ domain 。此后,其他设备节点通过 interrupts 和 interrupt-parent 向这些控制器"注册"中断线,内核才能将硬件中断号翻译成软件使用的虚拟中断号(virq),并最终调用驱动中的中断处理函数。
使用规则:
-
父中断控制器 定义了
#interrupt-cells = <3>时,才能使用三参数格式,格式:<中断类型、中断号、触发方式> -
一个设备可以有多个中断,用多个三参数组表示
举例:
interrupts = <0 32 4>, <0 33 4>;
| 参数序号 | 名称 | 常见取值 |
|---|---|---|
| 第 1 个 | 中断类型 | 0 = SPI(共享外设中断) 1 = PPI(私有外设中断) |
| 第 2 个 | 中断号 | 硬件中断 ID(如 32) |
| 第 3 个 | 触发方式 | 1 = 上升沿 2 = 下降沿 4 = 高电平 8 = 低电平 |
对应内核驱动:
cpp
static int gic_probe(struct platform_device *pdev)
{
/* 初始化中断控制器硬件 */
gic_init();
/* 向内核注册 IRQ domain */
irq_domain_create();
return 0;
}
static const struct of_device_id gic_of_match[] = {
{ .compatible = "arm,gic-400" },
{ /* sentinel */ }
};
static struct platform_driver gic_driver = {
.driver = {
.name = "gic",
.of_match_table = gic_of_match,
},
.probe = gic_probe,
};
三、总结
设备树属性参数看似繁多,只要抓住核心参数的理解,就能快速建立起清晰的认知框架。
四:编译和反编译
Linux内核加载dtb获取硬件配置。编译dts可得dtb,反编译dtb可得dts。掌握编译和反编译,是验证硬件配置、排查驱动问题的必备技能。
1、dtc工具说明
dtc(Device Tree Compiler)是设备树的标准编译工具,内核自带(scripts/dtc/)
核心参数:
| 参数 | 含义 |
|---|---|
-I |
输入格式(dts/dtb) |
-O |
输出格式(dtb/dts) |
-o |
输出文件名 |
2、dts → dtb:编译
手动编译:
cpp
dtc -I dts -O dtb -o test.dtb test.dts
内核编译(标准做法):
bash
make dtbs ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
编译后的 .dtb 位于 arch/<架构>/boot/dts/ 目录下。
3、dtb → dts:反编译
bash
dtc -I dtb -O dts -o test.dts test.dtb
反编译后的 .dts 可直接查看硬件配置,是调试时确认节点是否生效的利器。
4、dts->编译->dtb->反编译->dts
前后2次dts得差异,反编译文件功能等价但可读性降级,适合查看配置,不适合直接作为源码二次开发。
| 对比项 | 原始 dts | 反编译后的 dts |
|---|---|---|
| 宏定义 | 有(GPIO_ACTIVE_LOW 等) |
全部展开成数字 |
| 头文件 | #include 存在 |
已展开,看不到 |
| 注释 | 有 | 全部丢失 |
| 节点顺序 | 人为组织 | 编译器可能重排 |
| 标签(label) | i2c0: i2c@xxx {} |
标签丢失 |
| 引用方式 | &i2c0 |
变成完整路径 |
| 可读性 | 高 | 较低 |
| 信息完整性 | 完整 | 只保留"最终结果" |
5、总结
-
dtc是编译/反编译的核心工具,掌握 -I、-O、-o即可应对大多数场景。 -
编译有手动和内核构建两种方式,后者为实际开发标准。
-
反编译是逆向查看设备树的关键手段,但必须清楚其局限性。
-
调试时通过反编译可快速确认硬件节点是否正确加载。
五:驱动框架
设备树本身不干活,真正让它"活"起来的是两套完全不同的驱动框架。
在 U-Boot 阶段,它叫 DM;
在 Linux 内核阶段,它叫 OF。
在 U-Boot 中,设备树由 DM(Driver Model)框架结合 libfdt/ofnode 解析,采用"触发模式"策略:设备节点先 bind 为 udevice,只有在被 uclass_get_device() 等路径显式访问时才会触发 probe。
在 Linux内核启动中,设备树由 OF 框架解析并生成 platform_device,再由 driver core 采用"及时模式"策略:一旦设备与驱动匹配成功,就会尽快触发 probe;若资源未就绪,则通过 deferred probe 机制重试。
一、下面从启动到硬件初始化角度看两个框架之别:
U-Boot DM 时序(触发模式):
① 编译期:U_BOOT_DRIVER 链接到 .u_boot_list 段(驱动就位)
② dm_init():初始化 DM 核心
③ dm_scan_fdt():解析 DTB → 匹配驱动 → 创建 udevice → bind
此时硬件未动
④ 运行时:某代码路径调用 uclass_get_device()
→ device_probe() → 时钟开、引脚配、寄存器设
没人用就不 probe
关键特征:bind 与 probe 分离,probe 延迟到"被访问"。
Linux OF 时序(及时模式)
① 驱动注册:builtin 驱动在 do_initcalls() 中注册
bash
② unflatten_device_tree():DTB → device_node 树
③ of_platform_populate():device_node → platform_device
④ platform_device_add():设备注册到总线 → 立即匹配驱动
→ really_probe() → 硬件初始化
若依赖不足,返回 -EPROBE_DEFER,稍后重试
⑤ 用户 open():使能中断、DMA(数据流全开)
关键特征:设备注册后尽快 probe,缺依赖自动排队重试。
二、举例说明:同一个 UART,差异之别
DTS 中定义:
uart0:
serial@4000 {
compatible = "ns16550a";
reg = <0x40000x100>;
status = "okay";
};
在 U-Boot 中
-
dm_scan_fdt()扫描到该节点 → bind 创建
udevice -
如果
stdout-path指向 uart0→
initr_serial()调用uclass_get_device()→ probe -
如果 stdout 指向网络控制台
→ 无人访问 → 永远不 probe
结果:硬件可能全程未被初始化。
在 Linux 内核中
-
of_platform_populate()创建
platform_device -
驱动匹配成功 →
really_probe()→ 立即初始化硬件 -
即便没有任何进程
open("/dev/ttyS0"),UART 控制器也已被配置好
结果:硬件在启动早期就被"点亮"。
三、总结
-
框架本质
-
U-Boot DM:轻量级设备管家,管 bind 不管 probe
-
Linux OF:硬件翻译官 + driver core,翻译完立即绑定
-
-
应用场景差异
-
U-Boot 受限于启动速度和内存,必须"能省则省"
-
Linux 追求系统完整性和用户空间友好,必须"尽早就绪"
-
六:隐藏的U-boot设备树
U-boot和内核的启动阶段,你大概率会产生这个疑问:内核启动时显式加载一个独立.dtb文件;而U-boot启动仅加载一个u-boot.bin,找不到单独加载.dtb的动作。
难道U-boot启动不需要设备树?
答案是:需要,但它的设备树被"藏"起来了。
U-boot为什么不像内核那样,单独拎一个.dtb文件加载呢?
一、加载设备树差异:
U-boot的设备树在编译阶段就被"嵌入"到U-boot的二进制镜像里,而内核的设备树是独立文件,由bootloader在启动时加载。
为什么U-boot要这么做?
U-boot 把 dtb 嵌入镜像,不是因为"技术上做不到独立加载",而是独立加载带来的复杂度远大于收益,嵌入镜像是最简单、最可靠、最不容易出错的设计选择。
二、U-boot阶段 dtb加载原理
先看看U-boot启动前 dtb从哪里来:
一句话总结:编译dtb嵌入U-boot镜像
在U-boot的编译流程中,设备树源文件(.dts)会被编译成.dtb,构建后期由 Makefile 将 u-boot-nodtb.bin 与 dts/dt.dtb 拼接生成u-boot.bin,最终作为独立二进制附加在 U-Boot 镜像末尾。
备注:如上适用于.config中配置CONFIG_OF_SEPARATE=y 分离方式。
最终生成的u-boot.bin结构大致如下:
+-------------------+
| U-boot代码段 |
| U-boot数据段 |
| ... |
| === DTB数据 === | ← 设备树就在这里
| ... |
+-------------------+
U-boot启动加载dtb
上电后,SPL 将 U-Boot 镜像搬移到 RAM,U-Boot 启动后通过 gd->fdt_blob 直接访问已就位的设备树。
三、内核阶段 dtb加载原理
先看看内核加载前dtb从哪里来:
一句话总结:dtc编译成独立文件,和内核镜像分开。
使用手动编译 dtb 方式,即:make dtbs,底层调用 dtc 生成二进制设备树,是一份独立的xx.dtb文件,在内核启动时通过booti命令加载使用。
内核启动前加载dtb
cpp
# 从存储加载 dtb 到内存
fatload mmc 0:1 0x90000000 /boot/my-board.dtb
# 启动内核,显式指定 dtb
booti 0x80080000 - 0x90000000
备注:
cpp
booti <kernel> <initrd> <dtb>
第三个参数就是 DTB 在 RAM 中的地址
四、U-boot和内核阶段设备树应用的关联
U-boot阶段内置的设备树,也可作为内核启动阶段的设备树。两者关系如下:
U-Boot 加载(或自身携带)的 DTB 经过运行时修改后,其内存地址通过寄存器直接传递给内核,成为内核启动时使用的设备树。
本质:二者在逻辑上是同一份硬件描述的不同阶段呈现,只是设备树内容有所不同。
五、总结
U-boot不是不需要设备树,而是把设备树嵌入U-boot.bin中。商用场景不同,内核阶段可以选择复用U-boot内嵌的设备树或者加载独立设备树文件。
七:设备树属性的"定制"
设备树属性的"定制",体现了不同的应用场景:U-boot可用、最低代价os启动、完整OS运行。
U-Boot最小启动设备树:CPU + 内存 + 存储 + chosen
内核最小启动设备树:CPU + 内存 + 中断 + timer + 时钟 + pinctrl + 串口 + chosen
内核完整启动设备树:内核最小 + 所有外设 + 完整时钟/电源/pinctrl/总线
一、U-Boot最小启动设备树
U-Boot的目标:把内核加载到内存并跳转执行。因此只解析启动必需的最小子集:
| 对象 | 关键属性 | 用途 |
|---|---|---|
/cpus |
device_type, compatible |
架构初始化 |
/memory |
reg |
内存布局,用于自身重定位和加载地址决策 |
/chosen |
stdout-path |
指定早期调试串口 |
| 存储控制器 | compatible, reg, clocks |
从nor flash等加载内核镜像 |
其余全部不需要。 网络、中断控制器、I2C等外设对U-Boot毫无意义。
二、内核最小启动设备树
内核的目标:加载并进入用户态,用最短的时间验证OS系统运行。在U-Boot最小集的基础上,必须额外包含:
| 对象 | 关键属性 | 用途 |
|---|---|---|
| 中断控制器 | interrupt-controller, #interrupt-cells |
中断子系统初始化 |
| 定时器 | compatible, interrupts |
系统节拍和延时 |
| 时钟控制器 | clocks, clock-names |
UART/timer依赖的时钟源 |
| pinctrl | pinctrl-0, pinctrl-names |
串口引脚复用 |
/chosen |
bootargs |
内核启动参数(console、rootfs等) |
可以disabled的:I2C/SPI从设备、网络、USB、GPU、音频、DMA、regulator。
一句话 :内核最小集 = CPU + 内存 + 中断 + timer + 时钟 + pinctrl + 串口 + chosen。
三、内核完整启动设备树
| 对象 | 关键属性 | 用途 |
|---|---|---|
/cpus下全部核心 |
device_type, compatible, reg, enable-method |
多核初始化、CPU热插拔 |
/memory + /reserved-memory |
device_type, reg, no-map |
系统物理内存布局、固件/外设预留 |
| GIC + 所有级联中断控制器 | compatible, #interrupt-cells, interrupt-controller |
全系统中断分发与映射 |
| 完整时钟树 | compatible, #clock-cells, clocks |
全外设时钟使能与频率管理 |
| 所有timer | compatible, interrupts, clock-frequency |
系统节拍、调度、延时 |
| 所有UART | compatible, reg, interrupts, clocks |
调试控制台、串口通信 |
/chosen |
bootargs, stdout-path |
启动参数传递、标准输出 |
/aliases |
serial*, mmc*, ethernet* |
为设备提供稳定编号 |
| 完整引脚配置 | pins, function, bias-*, drive-strength |
引脚复用与电气属性配置 |
| eMMC/SD/NAND/QSPI | compatible, reg, clocks, vmmc-supply |
存储介质访问 |
| MAC + PHY | compatible, phy-handle, phy-mode |
网络通信 |
| USB controller + PHY + hub | compatible, phys, dr_mode |
USB主机/设备功能 |
| LCD/HDMI/DSI + panel | compatible, port, data-lines |
图形输出 |
| GPU节点 | compatible, reg, interrupts, clocks |
图形加速 |
| codec + sound card | compatible, dais, audio-routing |
音频播放与录制 |
| I2C/SPI总线 + 所有从设备 | compatible, reg, clock-frequency |
板级外设通信 |
| 加速度计、陀螺仪等传感器 | compatible, reg, interrupts |
姿态/环境感知 |
| sensor + ISP | compatible, reg, clocks, port |
图像采集与处理 |
| regulator拓扑 | compatible, regulator-name, voltage-tables |
电源管理与供电约束 |
| LED、按键、风扇、RTC等 | compatible, gpios, label |
板级辅助功能 |
一句话 :内核完整设备树 = 内核最小 + 所有外设 + 完整时钟/电源/pinctrl/总线。
六、总结
U-Boot取最小启动集,内核取最小存活集或完整功能集。同一份数据、多个消费者、按需裁剪,这正是设备树设计的精髓。
八:DTB 两阶段加载
如前文章说,U-boot->内核启动过程中,有两个阶段要使用设备树。
-
U-Boot 早期:用"最小设备树"把自己跑起来
-
U-Boot 后期:把"全量设备树"传给内核
本篇来说明,这两个dtb来自哪个.dts?
一、U-Boot 最小设备树来源:
U-Boot编译时,被嵌入到U-boot.bin尾部的dtb,来源通过U-boot工程源码中的.config可见。
bash
CONFIG_OF_CONTROL=y
CONFIG_DEFAULT_DEVICE_TREE="my-board.dts "
位置:U-boot源码根目录/arch/arm/dts/my-board.dts
编译步骤:
bash
arch/arm/dts/my-board.dts
↓ dtc
arch/arm/dts/my-board.dtb
↓
u-boot-nodtb.bin + my-board.dtb
↓
u-boot.bin(DTB 附在U-boot镜像尾部)
U-Boot 启动后通过:gd->fdt_blob直接拿到这份 DTB。
二、U-Boot 完整设备树来源
U-Boot跑起来后去加载"真正的板级设备树"。即加载命令:load mmc 0:1 ${fdt_addr_r} /boot/my-board.dtb。这里my-board.dtb来自 Linux 内核源码树dts的编译,该dts路径:
源码根目录/arch/arm64/boot/dts/<vendor>/my-board.dts。
三、把两份 DTS 打开看:差异在哪?
- U-Boot 的 DTS(最小树)
bash
dts
/ {
cpus { ... };
timer { ... };
uart0: serial@... {
compatible = "ns16550a";
u-boot,dm-pre-reloc;
};
ddr: memory-controller@... {
u-boot,dm-spl;
};
mmc0: mmc@... {
u-boot,dm-pre-reloc;
};
};
- 内核的 DTS(全量树)
bash
dts
/ {
cpus { ... };
memory@... { ... };
gic: interrupt-controller@... { ... };
timer { ... };
uart0: serial@... { ... };
mmc0: mmc@... { ... };
pcie@... { ... };
gpu@... { ... };
i2c@... { ... };
spi@... { ... };
regulator-x { ... };
thermal-zones { ... };
};
u-boot,dm-pre-reloc 不是 Linux 内核用的属性 ,而是 U-Boot 驱动模型(Driver Model)里的"阶段标签"。
打上如上的"标签",relocation 之前,这个设备树节点要和对应的 driver建立绑定关系(bind),生成 udevice,具备probe设备前的准备工作,实际的probe还需要依赖U-boot程序触发调用。
四、总结
U-Boot 最小启动用的 DTB:由 CONFIG_DEFAULT_DEVICE_TREE 决定,位置: /arch/arm/dts/my-board.dts
给内核的 DTB:来自 Linux 源码树/arch/arm64/boot/dts/<vendor>/