Linux设备树详解

一:设备树的前世今生

导语: 在嵌入式Linux的世界里,从海量的板级C文件到"设备树(Device Tree)"重构了Linux内核框架。今天,我们就来聊聊设备树的前世今生。


一、 前世:那些年,我们"硬编码"的痛

在早期的Linux内核(3.x之前),你要为一款新的开发板编写BSP(板级支持包),你通常会怎么做?

答案是:写一大堆的C代码。

硬件信息是直接写死在内核源码里的。如果你想描述一块开发板上的UART、I2C、SPI或者GPIO,必须在 arch/arm/mach-xxx/ 目录下创建一个专门的C文件。

你会看到成百上千行的代码,用来定义:

  • 寄存器的物理地址

  • 中断号(IRQ)

  • 时钟频率

  • 各种GPIO的引脚复用关系

这带来了什么?

  1. 代码冗余度极高:

    假设有两款开发板,仅仅因为LED灯接的GPIO引脚不同,必须在内核中维护两份几乎一模一样的代码。

  2. 内核极度臃肿:

    随着ARM生态的爆发,出现了成千上万款ARM开发板。每一款板子的硬件细节都要写进内核,Linux内核将变成一个巨大的"垃圾场",充满了各种 xxxx_board.c。

  3. 升级维护困难:

    每次硬件改版(哪怕换了一个电阻),软件工程师都要重新修改内核代码、重新编译内核。

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. 它是如何工作的?
  1. 硬件工程师:

    设计好电路图。

  2. 软件工程师:

    编写 .dts (Device Tree Source) 文件,用特定的语法描述电路连接。

  3. 编译阶段:

    使用 dtc (Device Tree Compiler) 工具将 .dts 编译成 .dtb (Device Tree Blob) 二进制文件。

  4. 启动阶段:

    Bootloader(如U-Boot)将 dtb 文件加载到内存中,并把它的地址告诉Linux内核。

  5. 内核运行:

    内核启动时解析 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 做地址映射,串口就可用了。

总结

设备树不是程序,没有执行逻辑,它只是一份结构化的硬件清单。编程时把握三点:

  1. 分层

    :dtsi 列清单,dts 开开关。

  2. 三属性

    :compatible 找驱动,reg 定位硬件,status 控制启停。

  3. 查表填空

    :遇到具体外设的额外参数,去内核 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 控制器也已被配置好

结果:硬件在启动早期就被"点亮"。


三、总结

  1. 框架本质

    • U-Boot DM:轻量级设备管家,管 bind 不管 probe

    • Linux OF:硬件翻译官 + driver core,翻译完立即绑定

  2. 应用场景差异

    • 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 打开看:差异在哪?

  1. 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;
 };
};
  1. 内核的 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>/

相关推荐
云梦谭2 小时前
让 Linux 主动连接 Windows:配置 Windows 11 SSH 服务端
linux·windows·ssh
奇树谦2 小时前
Windows CMD 下 mc 运维指南:SeaweedFS 安装验收与 MinIO 数据迁移
运维·windows
刚入门的大一新生2 小时前
Linux-信号
linux·运维·服务器
车软学堂2 小时前
CANoe自动化:跑起第一个测试用例
运维·车载系统·自动化·汽车·测试用例
L0208244 小时前
Kubernetes Ingress|CNI 网络模型|Calico 网络策略|Pod 调度器完整实战教程
运维·网络·云原生·容器·kubernetes
考拉讲支付5 小时前
延迟分账 / 核销分账的技术实现方案:服务完成后再分钱,到底怎么设计?
大数据·运维·人工智能·微信小程序·小程序·系统架构
ADAI_Bowen6 小时前
写文章 - 知乎
大数据·运维·人工智能
GeW6 小时前
新质生产力路线图解读:数据库性能优化与Linux调优专场
linux