1. U-Boot与设备树概述

大家好,我是你们这门课的老朋友。做嵌入式底层开发十几年了,U-Boot 和设备树几乎是我每天都要打交道的两样东西。今天咱们先不急着写代码,把基础概念理清楚,后面调试起来才不慌。

1.1 U-Boot 启动流程——从上电到加载内核

U-Boot 的全称是 "Universal Boot Loader",说白了就是个引导程序。它的任务很简单:把操作系统内核从存储介质里读出来,放到内存里,然后跳过去执行。

但这个过程其实挺讲究的。我画了一张流程图,你看一眼就明白了:

U-Boot 启动流程 上电 / 复位 ROM Code (BootROM) SPL (第一阶段引导) U-Boot 完整镜像 解析设备树 (DTB) 加载并启动内核 关键说明 • SPL 大小通常 ≤ 64KB • DTB 在 U-Boot 阶段解析 • 内核启动参数通过 DTB 传递 避坑提醒 • DTB 地址必须与内核约定一致 • 我曾因 DTB 地址写错, 内核死活起不来

嗯,这里要注意:现在的 SoC 基本都带 BootROM,它先初始化最基本的硬件(比如时钟、DDR 控制器),然后从 SD 卡、eMMC 或 NAND Flash 里把 SPL 读进来。SPL 再加载完整的 U-Boot。U-Boot 跑起来之后,解析设备树,最后把控制权交给内核。

个人习惯:我调试新板子时,第一步不是看 U-Boot 源码,而是先确认 BootROM 到 SPL 这条链路通不通。串口能打印出 SPL 的 log,后面的事才好办。

1.2 设备树(DTS/DTB)基本概念

设备树,英文叫 Device Tree,是一种描述硬件信息的数据结构。它用树形结构把 CPU、内存、外设、中断、GPIO 这些东西组织起来。

你想想看,以前没有设备树的时候,Linux 内核里塞满了各种板级文件(board-xxx.c)。每换一款板子,就得重新编译内核。后来大家觉得这样太累了,于是设备树就诞生了。

设备树涉及三个文件格式:

文件格式 全称 说明
.dts Device Tree Source 文本格式的源文件,人类可读写
.dtsi Device Tree Source Include 公共头文件,类似 C 语言的 .h
.dtb Device Tree Blob 编译后的二进制文件,U-Boot 和内核直接使用

举个例子,一个简单的 .dts 文件长这样:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a8";
            clock-frequency = <1000000000>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "okay";
    };
};

这段代码描述了一颗 Cortex-A8 的 CPU、512MB 内存、一个 UART 控制器。每个节点都有 compatible 属性,内核通过它来匹配对应的驱动。

核心要点:设备树把硬件描述和内核代码解耦了。换板子只需要改 .dts 文件,重新编译生成 .dtb,内核不用动。

1.3 为什么需要设备树——我踩过的坑

说实话,我刚入行那会儿用的是旧版 U-Boot,没有设备树。每次移植都要改一堆 board 文件,改完还得重新编译整个 U-Boot。有一次我改错了一个 GPIO 的宏定义,整块板子的 LCD 不亮了,查了两天才找到问题。

设备树解决了三个核心痛点:

  1. 硬件描述标准化——所有外设信息统一写在 .dts 里,不用散落在各处。
  2. 内核与板级解耦——同一份内核镜像可以跑在不同板子上,只要换 .dtb 就行。
  3. U-Boot 与内核共享——U-Boot 解析 DTB 后,把硬件信息直接传给内核,避免重复配置。

举个例子,我在做 i.MX6Q 的板子移植时,客户要求支持两种不同容量的 DDR3 颗粒。以前我得准备两套 U-Boot 镜像,现在只需要在 .dts 里改 memory 节点的 reg 属性,重新编译 .dtb 就搞定了。

注意:设备树虽然方便,但也不是银弹。我曾经遇到一个坑:某个外设的时钟频率在 .dts 里写错了,U-Boot 启动时没报错,但内核驱动加载后系统直接死机。所以 DTB 的每个参数都要仔细核对芯片手册。

1.4 设备树在 U-Boot 中的角色

U-Boot 使用设备树的方式和内核不太一样。U-Boot 本身也会读取 DTB,用来初始化一些外设(比如网卡、MMC、显示控制器)。然后 U-Boot 会把 DTB 的地址通过寄存器或参数传递给内核。

我整理了一个对比表,帮你理解两者的差异:

对比项 U-Boot 中的设备树 Linux 内核中的设备树
解析时机 启动阶段,初始化外设前 内核初始化过程中
主要用途 配置时钟、DDR、存储、网卡 驱动匹配、资源分配、中断映射
修改方式 可在 U-Boot 命令行用 fdt 命令动态修改 通常只读,需重新编译 .dtb
传递方式 通过 bootm 命令将 DTB 地址传给内核 从 U-Boot 指定的地址读取

嗯,这里有个细节:U-Boot 的 fdt 命令非常强大。我调试时经常用它来临时修改 DTB 里的参数,比如关掉某个没用的外设,或者调整内存大小。这样不用重新编译,效率高很多。

调试技巧:在 U-Boot 命令行输入 fdt list / 可以查看整个设备树的结构。如果某个节点没显示出来,说明 DTB 里根本没包含它,或者编译时被裁剪掉了。

好了,这一章的内容就到这里。设备树的概念虽然简单,但它是整个 U-Boot 移植和调试的基石。后面我们会一步步深入,从 DTS 的编写、编译,到 U-Boot 中设备树的解析和修改,再到实际项目中的调试技巧。你先把这些基础概念消化掉,后面学起来会轻松很多。