第三章 Bootloader深度解析:U-Boot的移植与配置、设备树(Device Tree)的编写、启动流程分析

各位同学,今天我们来啃一块硬骨头——Bootloader。说实话,很多做应用开发的兄弟觉得Bootloader就是个黑盒子,上电后自动把内核加载起来就完事了。但做ADAS域控,这玩意儿你必须吃透。为什么?因为你的系统要跑在毫秒级启动、安全启动、多核异构的平台上,Bootloader一旦出问题,整个系统都起不来。

我个人习惯把Bootloader比作「看门大爷」。它负责开门(初始化硬件)、查证件(校验镜像)、指路(引导内核)。今天我们就从U-Boot的移植配置、设备树编写、启动流程三个维度,把这套东西彻底讲透。

3.1 U-Boot移植:从零开始让板子跑起来

U-Boot的移植,说白了就是让这个通用的Bootloader适配你的特定硬件。我在项目中遇到过最头疼的事——明明照着参考板配的,板子就是死活不启动。后来发现是DDR时序参数差了那么几个纳秒。

3.1.1 移植前的准备工作

动手之前,你得先搞清楚三件事:

  • 你的SoC是哪家的? 比如NXP的i.MX8、TI的TDA4、还是瑞萨的R-Car?不同厂商的启动流程差异很大。
  • 参考板是什么? 厂商一般会提供评估板的U-Boot源码,这是你的起点。
  • 你的板子改了哪些? DDR型号、Flash类型、外设接口,这些都要列清楚。
我的经验: 别一上来就改U-Boot源码。先在参考板上跑通,确认工具链、编译环境没问题。我曾经跳过这步,结果折腾了两天发现是交叉编译器版本不对。

3.1.2 配置U-Boot:Kconfig与板级文件

U-Boot现在用Kconfig管理配置,跟Linux内核很像。你需要在board/目录下创建你的板级文件夹,里面放什么呢?

board/mycompany/myboard/
├── Kconfig          # 板级配置选项
├── MAINTAINERS      # 维护者信息
├── Makefile         # 编译规则
├── myboard.c        # 板级初始化代码
└── myboard.h        # 板级配置头文件

嗯,这里要注意:myboard.c里的board_init_f()函数是第一个被调用的C函数。它负责初始化DDR、时钟、串口等基础外设。我建议你在这个函数里加个串口打印,方便调试——「Hello from U-Boot!」能出来,说明最基础的硬件通路通了。

3.1.3 编译与烧录

配置好之后,编译命令大概是这样的:

make ARCH=arm CROSS_COMPILE=aarch64-linux-gnu- myboard_defconfig
make ARCH=arm CROSS_COMPILE=aarch64-linux-gnu- -j8

编译出来的u-boot.bin怎么烧?这取决于你的SoC。有些用SD卡,有些用eMMC,还有些用SPI Flash。以i.MX8为例,你需要把U-Boot打包成flash.bin,然后通过UUU工具烧录。

避坑指南: 我曾经因为烧录地址搞错,把U-Boot写到了内核分区。结果每次启动都卡在「Starting kernel...」,查了三天才发现是分区表乱了。记住:U-Boot的烧录地址由SoC的启动ROM决定,别自己想当然。

3.2 设备树(Device Tree):硬件的「身份证」

设备树,英文叫Device Tree,简称DT。它的作用是什么?说白了,就是告诉内核你的板子上有哪些硬件、它们怎么连接、需要什么驱动。没有设备树,内核就是个瞎子。

3.2.1 设备树的基本结构

一个典型的设备树文件长这样:

/dts-v1/;

/ {
    model = "MyCompany ADAS Board";
    compatible = "mycompany,myboard", "vendor,soc";

    chosen {
        stdout-path = &uart1;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x80000000>;
    };

    &uart1 {
        status = "okay";
        clock-frequency = <115200>;
    };
};

你想想看,这里每个节点都对应一个硬件设备。memory节点告诉内核DDR的起始地址和大小,uart1节点告诉内核串口的配置。内核启动时,会解析这些节点,然后加载对应的驱动。

3.2.2 编写设备树的要点

写设备树,我最怕遇到什么?语法错误。DT的语法很严格,少个分号、多个括号,编译就过不去。我建议你用dtc -I dts -O dtb -o output.dtb input.dts来编译,然后用dtc -I dtb -O dts output.dtb反编译检查。

几个关键点:

  • compatible属性: 这是驱动匹配的关键。驱动里会写of_match_table,跟这里的字符串匹配上了,驱动才会加载。
  • reg属性: 描述设备的地址空间。比如I2C控制器的基地址、SPI Flash的片选地址。
  • 中断:interrupt-parentinterrupts指定中断控制器和中断号。
核心原则: 设备树只描述硬件,不描述行为。别把驱动逻辑写进DT里,那是内核的事。

3.2.3 设备树与U-Boot的关系

U-Boot也会用到设备树。它需要知道DDR的配置、时钟的初始化参数。有些SoC的U-Boot会用一个叫u-boot.dtsi的文件,里面包含U-Boot特有的节点,比如u-boot,dm-pre-reloc属性,告诉U-Boot在重定位前就要初始化这个设备。

我记得有一次,板子的eMMC在U-Boot下能识别,但内核里死活找不到。查了半天,发现是设备树里eMMC节点的bus-width属性写成了<4>,但实际硬件是8位总线。改过来就好了。你看,一个属性值错了,整个存储子系统就废了。

3.3 启动流程分析:从ROM到内核

好了,现在U-Boot移植好了,设备树也写好了。上电后,系统是怎么一步步把内核拉起来的?我们来走一遍完整的启动流程。

3.3.1 第一阶段:ROM Code

SoC上电后,CPU会从固定的地址执行代码。这个地址在ROM里,是芯片厂商固化好的。ROM Code会做几件事:

  1. 初始化CPU和缓存(Cache)
  2. 检测启动设备(SD卡、eMMC、SPI Flash等)
  3. 从启动设备读取U-Boot的SPL(Secondary Program Loader)到SRAM
  4. 跳转到SPL执行

这里有个坑:ROM Code对启动设备的支持是有限的。如果你的板子用了特殊的Flash,ROM Code可能不认识。这时候就需要用「回退启动」——先从SD卡启动,再跳转到Flash。

3.3.2 第二阶段:SPL

SPL是U-Boot的精简版,体积很小,只能放在SRAM里运行。它的任务很简单:初始化DDR,然后把完整的U-Boot从Flash加载到DDR里。

SPL的代码在spl/目录下。你可以通过配置CONFIG_SPL_BUILD来定制SPL的功能。我建议SPL里只开最必要的外设——串口、DDR、Flash控制器。别加什么网络、USB,那会撑爆SRAM。

3.3.3 第三阶段:U-Boot Proper

完整的U-Boot在DDR里运行,这时候它就有「三头六臂」了。它会做:

  • 板级初始化: 调用board_init_r(),初始化所有外设
  • 环境变量加载: 从Flash或eMMC读取bootargsbootcmd等环境变量
  • 设备树加载: 把设备树从Flash读到内存
  • 内核加载: 根据bootcmd的指示,加载内核镜像到指定地址

嗯,这里要提一下bootcmd。它是个环境变量,定义了U-Boot启动内核的命令序列。比如:

setenv bootcmd 'ext4load mmc 0:1 0x80000000 /boot/Image; ext4load mmc 0:1 0x83000000 /boot/board.dtb; booti 0x80000000 - 0x83000000'

这条命令的意思是:从eMMC的第一个分区加载内核到0x80000000,加载设备树到0x83000000,然后启动内核。

3.3.4 第四阶段:内核启动

U-Boot调用booti(ARM64)或bootz(ARM32)后,控制权就交给了内核。内核会先解压自己,然后解析设备树,初始化驱动,最后挂载根文件系统。

到这里,Bootloader的任务就完成了。它就像接力赛的第一棒,把系统稳稳地交到内核手里。

调试技巧: 如果内核启动卡住了,别急着怀疑内核。先在U-Boot里用md命令检查内核和设备树在内存里的内容是否正确。我曾经遇到过设备树加载地址写错,内核拿到了一堆乱码,自然起不来。

3.4 实战中的避坑指南

最后,我总结几个实战中容易踩的坑:

  • DDR时序: 别用参考板的时序参数直接套。不同PCB布局、不同批次的内存颗粒,时序都可能要微调。用memtest工具跑一遍,确保DDR稳定。
  • 启动设备顺序: 有些SoC支持多个启动设备。如果SD卡和eMMC都插着,ROM Code会按优先级选一个。我建议在产品中只保留一个启动设备,避免混乱。
  • 安全启动: ADAS域控要求安全启动。U-Boot需要校验内核和设备树的签名。这需要在U-Boot里集成fitImage支持,并配置公钥。
  • 环境变量存储: 别把环境变量放在跟内核同一个分区。万一你改错了环境变量,板子变砖了,还能通过擦除环境变量分区来恢复。

好了,Bootloader这块就讲到这里。下一章我们会深入内核启动的细节,看看内核是怎么接管硬件的。记住,Bootloader是系统的基石,这块搞扎实了,后面的路就好走了。