第三章 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类型、外设接口,这些都要列清楚。
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工具烧录。
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-parent和interrupts指定中断控制器和中断号。
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会做几件事:
- 初始化CPU和缓存(Cache)
- 检测启动设备(SD卡、eMMC、SPI Flash等)
- 从启动设备读取U-Boot的SPL(Secondary Program Loader)到SRAM
- 跳转到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读取
bootargs、bootcmd等环境变量 - 设备树加载: 把设备树从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的任务就完成了。它就像接力赛的第一棒,把系统稳稳地交到内核手里。
md命令检查内核和设备树在内存里的内容是否正确。我曾经遇到过设备树加载地址写错,内核拿到了一堆乱码,自然起不来。
3.4 实战中的避坑指南
最后,我总结几个实战中容易踩的坑:
- DDR时序: 别用参考板的时序参数直接套。不同PCB布局、不同批次的内存颗粒,时序都可能要微调。用
memtest工具跑一遍,确保DDR稳定。 - 启动设备顺序: 有些SoC支持多个启动设备。如果SD卡和eMMC都插着,ROM Code会按优先级选一个。我建议在产品中只保留一个启动设备,避免混乱。
- 安全启动: ADAS域控要求安全启动。U-Boot需要校验内核和设备树的签名。这需要在U-Boot里集成
fitImage支持,并配置公钥。 - 环境变量存储: 别把环境变量放在跟内核同一个分区。万一你改错了环境变量,板子变砖了,还能通过擦除环境变量分区来恢复。
好了,Bootloader这块就讲到这里。下一章我们会深入内核启动的细节,看看内核是怎么接管硬件的。记住,Bootloader是系统的基石,这块搞扎实了,后面的路就好走了。