Bootloader原理与移植:U-Boot启动流程分析、U-Boot源码结构、板级配置与设备树修改、U-Boot烧写与调试
好,我们进入第二章。Bootloader,说白了就是嵌入式系统的"第一口饭"。CPU上电后,第一个跑的程序就是它。U-Boot是目前最主流的Bootloader,几乎支持所有主流架构。我这些年移植过的板子,十块里有八块用的都是U-Boot。
这一章,我会带你从启动流程、源码结构、板级配置、设备树修改,一直讲到烧写与调试。嗯,内容不少,但每一步都是实战中必须啃下来的硬骨头。
U-Boot启动流程分析
先搞清楚U-Boot是怎么跑起来的。我习惯把启动流程分成两个阶段:SPL(Secondary Program Loader)和U-Boot proper。为什么分两段?因为早期SRAM太小,装不下完整的U-Boot。
核心要点:SPL负责初始化最小硬件(时钟、DDR、串口),然后从存储介质加载U-Boot proper到DDR,跳转执行。
具体流程如下:
- CPU上电,从ROM执行固化代码——读取启动介质(SD卡、eMMC、NAND Flash等)的前几个扇区到SRAM。
- SPL阶段——初始化时钟、串口、DDR控制器。我在项目中遇到过SPL跑不起来的情况,最后发现是DDR时序参数配错了,板子直接"死"在DDR初始化。
- 加载U-Boot proper——SPL从存储介质读取U-Boot主镜像到DDR,然后跳转。
- U-Boot proper阶段——初始化更多外设(网卡、存储、USB等),解析环境变量,进入命令行或自动启动内核。
为什么会这样设计?你想想看,如果CPU内部SRAM只有64KB,而U-Boot proper有几百KB,不拆成两段根本放不下。
实战技巧:调试SPL时,可以在串口输出加打印信息,定位卡死位置。我习惯在SPL的board_init_f()和board_init_r()里各加一个puts()。
U-Boot源码结构
U-Boot源码目录看起来复杂,其实有规律可循。我刚开始看的时候也懵,后来总结出一套"按图索骥"的方法。
| 目录/文件 | 作用 | 我常用的场景 |
|---|---|---|
| arch/ | 架构相关代码(arm、riscv、x86等) | 修改start.S、lowlevel_init.S |
| board/ | 板级初始化代码 | 添加新板子时,复制参考板目录修改 |
| configs/ | 板级默认配置文件(xxx_defconfig) | make xxx_defconfig 生成.config |
| include/configs/ | 板级头文件配置(旧版方式) | 定义宏如CONFIG_SYS_TEXT_BASE |
| dts/ | 设备树源文件(.dts、.dtsi) | 修改硬件描述,如DDR容量、外设地址 |
| drivers/ | 驱动代码(串口、网卡、MMC等) | 移植新网卡驱动时重点看 |
| cmd/ | 命令行命令实现 | 添加自定义命令时参考 |
| common/ | 通用功能(环境变量、启动逻辑) | 修改bootcmd时涉及 |
我个人习惯,拿到一个新板子,先看board/和configs/目录,找到最接近的参考板。然后对比dts/里的设备树,确认硬件差异。这样能快速定位需要修改的文件。
避坑指南:我曾经在移植时直接复制了另一个厂商的板级目录,结果编译出来的U-Boot在串口初始化就挂了。后来发现是GPIO复用配置不同。所以,复制参考板后,一定要逐行检查board.c里的引脚初始化。
板级配置与设备树修改
板级配置,说白了就是告诉U-Boot你的板子长什么样。现在主流做法是通过设备树(Device Tree)来描述硬件。U-Boot从2016年开始全面转向设备树,我建议你也用这种方式。
修改设备树的典型步骤:
- 找到对应.dts文件——在
dts/目录下,按厂商和板子名称查找。 - 修改内存节点——设置DDR容量和起始地址。我遇到过客户给的DDR容量和实际焊接的不一致,导致U-Boot只能检测到一半内存。
- 修改串口节点——确认UART的基地址和时钟频率,否则串口输出乱码或没输出。
- 修改网卡节点——配置PHY地址、复位GPIO、时钟等。
- 修改存储节点——SD卡、eMMC、NAND Flash的分区和时序参数。
举个例子,修改DDR容量:
// 在.dts文件中
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>; // 1GB DDR,起始0x80000000
};
嗯,这里要注意:reg属性的第三个值是容量,单位是字节。0x40000000就是1GB。如果板子只有512MB,就改成0x20000000。
个人经验:修改设备树后,一定要用make dtbs重新编译,然后检查生成的.dtb文件大小是否合理。我曾经改错了语法,编译没报错,但生成的dtb只有几十字节,加载后U-Boot直接崩溃。
U-Boot烧写与调试
烧写U-Boot,不同平台方法不同。我以最常用的SD卡启动为例:
- 编译U-Boot——
make xxx_defconfig && make -j4,生成u-boot.bin或u-boot.img。 - 烧写到SD卡——
sudo dd if=u-boot.bin of=/dev/sdb bs=1k seek=1。注意seek=1,跳过第一个1KB(分区表保留区)。 - 插入SD卡,上电启动——串口工具(如minicom、putty)看输出。
调试U-Boot,我常用的方法:
- 串口打印——在关键函数加
printf()或puts(),定位卡死位置。 - LED闪烁——如果串口还没初始化,用GPIO控制LED闪烁,判断代码执行到哪一步。
- JTAG调试——用OpenOCD + GDB,可以单步调试SPL和U-Boot proper。我习惯在board_init_f()设断点。
- 保存环境变量——
saveenv命令,避免每次重启都重新设置IP地址和启动参数。
避坑指南:我曾经在烧写U-Boot时,dd命令的seek参数写错了,写到了分区表位置,结果SD卡在电脑上无法识别。后来我养成了习惯:烧写前先用fdisk -l确认设备名,再用dd if=u-boot.bin of=/dev/sdb bs=1k seek=1 conv=fsync,加conv=fsync确保数据写回。
U-Boot启动流程与板级适配关系图
下面这张图,是我根据多年移植经验总结的。它把启动流程、源码目录、板级配置串在了一起。你移植时,可以对照这张图,一步步排查问题。
这张图把整个流程串起来了。你移植时,可以按这个顺序:先确认SPL能不能跑,再看U-Boot proper能不能加载,最后看设备树解析和内核启动。每一步都有对应的源码目录和配置项。