1. U-Boot与设备树概述

大家好,我是这次课程的主讲。咱们直接进入正题——设备树。这东西现在做嵌入式开发几乎天天见,但很多人只是会用,不清楚它到底解决了什么问题。今天这一章,我就带大家把设备树的来龙去脉理清楚。

1.1 设备树(DTS)的起源与作用

先说说设备树是怎么来的。早些年,Linux内核里充斥着大量的板级代码。什么意思呢?就是每换一块板子,内核源码里就要加一堆 arch/arm/mach-xxx 的文件,里面全是硬件寄存器地址、中断号、GPIO映射这些信息。我刚开始做嵌入式那会儿,光维护这些板级文件就够头疼的。

后来大家发现这条路走不下去了。尤其是ARM架构,芯片厂商多、板子型号杂,内核里塞了几百个板级文件,每次改动都牵一发动全身。于是设备树(Device Tree)被引入了。

设备树的核心思想:把硬件描述从内核源码中剥离出来,放到一个独立的数据结构里。内核启动时解析这个结构,就知道自己跑在什么硬件上了。

设备树的源文件是 .dts,编译后变成 .dtb(二进制格式)。U-Boot和内核都能读懂它。说白了,它就是一张硬件配置清单。

举个例子,一块板子上有串口、网卡、LED灯。传统做法是在内核里写死地址:

// 传统板级代码风格
#define UART_BASE 0x10000000
#define ETH_BASE  0x20000000
#define LED_PIN   12

换成设备树后,这些信息写在 .dts 文件里:

/ {
    uart@10000000 {
        compatible = "ns16550";
        reg = <0x10000000 0x1000>;
    };
    eth@20000000 {
        compatible = "dwc,eqos";
        reg = <0x20000000 0x4000>;
    };
    led {
        gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>;
    };
};

这样内核源码就干净多了。换板子?换一个 .dts 文件就行,不用改内核。

1.2 U-Boot中设备树的生命周期

设备树在U-Boot里是怎么流转的?我画了一张图,大家一看就明白。

U-Boot中设备树生命周期 编译阶段 烧录/存储 U-Boot加载dtb U-Boot解析设备树 传递给Linux内核 关键点:U-Boot不修改dtb内容,只负责加载、解析和传递

这张图展示了设备树在U-Boot里的完整旅程。我拆开来讲:

  1. 编译阶段:开发机上用 dtc 工具把 .dts 编译成 .dtb。这一步通常在U-Boot或内核的构建系统里自动完成。
  2. 烧录/存储:.dtb 文件被烧录到板子的存储介质上——可以是SPI Flash、eMMC、SD卡,甚至网络加载。位置很关键,U-Boot得知道去哪找它。
  3. U-Boot加载dtb:U-Boot启动后,根据环境变量或默认配置,从存储介质里把 .dtb 读到内存中。我习惯把加载地址设成 0x40000000 附近,避开内核和ramdisk的区域。
  4. U-Boot解析设备树:U-Boot会解析dtb里的部分节点,比如内存大小、串口配置、启动参数。注意,U-Boot只解析它关心的部分,不会全量解析。
  5. 传递给Linux内核:最后一步,U-Boot把dtb在内存中的地址通过寄存器(比如ARM的 r2)传给内核。内核接管后,自己再完整解析一遍。

我的经验:曾经有个项目,U-Boot加载dtb后内核死活起不来。查了半天,发现是dtb加载地址和内核解压地址重叠了。所以加载地址一定要避开内核的加载区域,这个坑我踩过。

1.3 为什么U-Boot需要设备树

这个问题其实可以反过来问:没有设备树,U-Boot怎么知道硬件信息?

传统做法是硬编码。U-Boot源码里写死内存基址、外设地址、GPIO配置。换一块板子?重新编译U-Boot。这在小规模项目里还能忍,但到了产品线丰富的公司,维护成本就爆炸了。

设备树给U-Boot带来了三个核心好处:

好处 说明 实际影响
硬件与代码解耦 硬件描述从源码中分离 换板子只需换dtb,不用改U-Boot源码
统一接口 U-Boot和内核共用同一份dtb 减少维护两套配置的工作量
动态适配 U-Boot启动时动态解析硬件信息 同一份U-Boot镜像可适配多种硬件变体

举个例子,我做过一个项目,同一款芯片有3种板型,内存大小不同、外设略有差异。没有设备树的话,我得维护3份U-Boot配置。有了设备树,U-Boot镜像完全一样,只在烧录时选不同的dtb文件。省事多了。

注意:U-Boot对设备树的解析是有限的。它不会像内核那样把所有节点都解析一遍。U-Boot只关心启动阶段必须知道的信息——内存布局、串口、存储设备、网络接口。其他外设的配置,U-Boot直接忽略,留给内核去处理。

你可能会问:U-Boot能不能完全不依赖设备树?理论上可以,但实际中几乎不可能。现在的SoC越来越复杂,一个芯片可能有几十个外设模块。不用设备树,U-Boot的板级文件会膨胀到难以维护。我见过一些老项目,一个 board.c 文件几千行,全是寄存器地址和初始化代码。后来迁移到设备树,代码量直接砍掉三分之二。

嗯,这里要注意一点:设备树不是万能的。它只是描述硬件,不负责驱动逻辑。U-Boot里该写的驱动代码还是得写,只是硬件参数不再硬编码了。

总结一下这一章的核心:

  • 设备树把硬件描述从源码中抽离出来,用 .dts 文件表达
  • U-Boot负责加载、解析和传递dtb,但不修改它
  • 设备树让U-Boot更灵活,同一份镜像可以适配多种硬件