3. Bootloader深度解析:U-Boot启动流程、命令与脚本、驱动模型、内核跳转
Bootloader,说白了就是嵌入式系统的“第一口饭”。上电后CPU第一个跑的就是它。我这些年调过的板子,十有八九的问题都出在U-Boot阶段——要么起不来,要么起错了。今天咱们就把U-Boot从里到外扒一遍。
3.1 U-Boot启动流程:从汇编到C的世界
U-Boot的启动分两个阶段。第一阶段用汇编,第二阶段用C。为什么这么设计?因为刚上电时,C语言运行环境还没准备好——栈指针没设、BSS段没清、全局变量没法用。你想想看,这时候只能用汇编干粗活。
第一阶段:SPL(Secondary Program Loader)
现在的U-Boot普遍采用SPL机制。SPL是个迷你版U-Boot,体积控制在几十KB以内。它负责最基础的硬件初始化,然后加载完整的U-Boot到DDR里。
SPL的核心任务:
- 设置CPU为SVC模式,关中断
- 初始化时钟、PLL、DDR控制器
- 设置栈指针(SP),为C语言铺路
- 从启动介质(SD卡、eMMC、NAND Flash)读取U-Boot主体
- 跳转到U-Boot的入口地址
我在项目中遇到过一块板子,SPL加载U-Boot时总是校验失败。查了两天,发现是DDR时序参数配错了——SPL初始化DDR时用了保守参数,但U-Boot主体加载后重新配置了DDR,导致数据错位。嗯,这里要注意:SPL和U-Boot对DDR的配置必须一致。
第二阶段:U-Boot主体
U-Boot主体从SPL接手后,会做更完整的初始化。流程大致如下:
- board_init_f():初始化串口、定时器、GPIO等基础外设
- relocate_code():把自己从Flash搬到DDR里运行
- board_init_r():初始化完整的外设驱动、文件系统、网络协议栈
- 进入主循环:等待用户输入命令,或自动执行bootcmd
我个人习惯在board_init_r()里加调试打印,打印出当前DDR地址和栈指针。这招帮我抓过好几次内存越界问题。
3.2 U-Boot命令与脚本:不只是敲命令
U-Boot的命令行看着简单,但用好它,调试效率能翻倍。我见过不少工程师只会用printenv和boot,其实U-Boot的命令体系很强大。
常用命令分类
| 类别 | 命令 | 用途 |
|---|---|---|
| 内存操作 | md mw mm |
查看、写入、修改内存 |
| 存储设备 | mmc sf fatload |
操作SD卡、SPI Flash、文件系统 |
| 网络 | tftp ping dhcp |
网络下载、连通性测试 |
| 环境变量 | setenv saveenv printenv |
配置持久化 |
| 调试 | go bootm bootz |
跳转到指定地址执行 |
调试技巧:用md.b 0x80000000 100可以dump内存前256字节。配合cmp.b命令,能快速比对两段内存是否一致。我曾经用这个命令定位过DMA传输的数据错位问题。
U-Boot脚本:自动化你的调试流程
U-Boot支持脚本化执行。环境变量里可以写多行命令,用分号或换行分隔。举个例子:
setenv myboot 'tftp 0x82000000 kernel.itb; tftp 0x83000000 dtb; bootm 0x82000000 - 0x83000000'
saveenv
run myboot
我个人习惯把常用的调试流程写成脚本,比如“从网络加载内核并启动”、“从SD卡加载备用系统”、“进入工厂测试模式”。这样量产时,产线工人只需要按电源键,U-Boot自动跑脚本。
注意:U-Boot脚本里不要写死内存地址。不同板子的DDR基地址可能不同。我建议用${loadaddr}、${fdt_addr}这类预定义变量,或者通过getenv动态获取。
3.3 U-Boot驱动模型:DM(Driver Model)
U-Boot的驱动模型(DM)是2014年后引入的。说白了,就是把Linux的设备树和驱动框架搬到了U-Boot里。为什么要这么做?因为传统U-Boot的驱动代码太乱了——每个板子一套初始化函数,复用性极差。
DM的核心概念
- udevice:代表一个设备实例,由设备树节点创建
- driver:驱动结构体,包含probe、remove、ops等回调
- uclass:设备类,比如所有GPIO控制器归为GPIO uclass
- phandle:设备树中的引用关系,驱动通过它找到依赖设备
举个例子,一个串口驱动的probe流程:
static int serial_s3c_probe(struct udevice *dev) {
struct s3c_serial_plat *plat = dev_get_plat(dev);
// 从设备树获取寄存器基地址
plat->base = dev_read_addr(dev);
// 初始化硬件
s3c_serial_init(plat);
return 0;
}
U_BOOT_DRIVER(serial_s3c) = {
.name = "s3c_serial",
.id = UCLASS_SERIAL,
.probe = serial_s3c_probe,
.plat_auto = sizeof(struct s3c_serial_plat),
};
我在项目中遇到过一个问题:某个外设在U-Boot下能工作,但进内核后就挂了。查到最后,发现U-Boot的DM驱动在probe时没有正确复位设备,导致内核驱动拿到的硬件状态是脏的。所以,我建议在驱动的probe函数里,一定要做硬件复位。
DM的启动流程
U-Boot启动时,DM的初始化顺序是固定的:
- 解析设备树,创建所有udevice实例
- 按依赖关系排序(父设备先于子设备)
- 依次调用每个设备的probe函数
- probe失败时,标记设备为“不可用”,不影响其他设备
调试DM问题:在U-Boot命令行输入dm tree,可以查看所有设备的probe状态。如果某个设备显示not probed,说明它的probe函数没被调用。这时候检查设备树里的status属性,或者看驱动有没有注册到对应的uclass。
3.4 从U-Boot到内核的跳转
这是Bootloader的终极使命——把控制权交给内核。跳转过程看似简单,但细节决定成败。
跳转前的准备工作
U-Boot在跳转前,必须完成以下几件事:
- 关闭中断:所有外设中断必须关掉,否则内核启动时会被打断
- 设置内核启动参数:通过设备树(DTB)或ATAGS传递内存布局、命令行参数
- 设置CPU寄存器:ARM架构下,r0=0,r1=机器类型ID,r2=DTB地址
- 刷新Cache和TLB:确保数据一致性,避免内核读到脏数据
我曾经踩过的坑:有一块板子,U-Boot跳转内核后总是卡在“Starting kernel...”不动。查了两天,发现是U-Boot在跳转前没有关闭MMU。内核启动时,MMU配置和U-Boot不一致,导致指令预取异常。解决方案:在跳转前调用cleanup_before_linux(),这个函数会关MMU、关Cache、刷TLB。
跳转命令详解
U-Boot支持三种跳转命令:
| 命令 | 适用场景 | 参数格式 |
|---|---|---|
bootm |
传统uImage格式 | bootm [kernel_addr] [ramdisk_addr] [dtb_addr] |
bootz |
zImage格式(ARM) | bootz [kernel_addr] [ramdisk_addr] [dtb_addr] |
booti |
Image格式(ARM64) | booti [kernel_addr] [ramdisk_addr] [dtb_addr] |
我个人习惯用bootm配合FIT Image(Flattened Image Tree)。FIT Image把内核、设备树、ramdisk打包成一个文件,用签名校验确保完整性。量产时,这个功能能防止固件被篡改。
跳转后的状态
U-Boot跳转后,CPU的控制权完全交给内核。U-Boot不再运行,它的代码段和数据段可以被内核覆盖。所以,U-Boot在跳转前要把所有需要保留的信息(比如环境变量)写回存储介质。
调试跳转问题:如果内核启动卡住,可以在U-Boot里用bootargs加上earlyprintk参数。这样内核在早期初始化阶段就会打印调试信息。另外,检查DTB地址是否正确——我见过有人把DTB加载到DDR的末尾,结果被内核的初始页表覆盖了。
3.5 U-Boot启动全流程SVG图
下面这张图总结了U-Boot从上电到跳转内核的完整流程:
3.6 实战经验总结
最后,分享几个我这些年积累的U-Boot调试经验:
- SPL起不来:先检查DDR初始化。用示波器量DDR的时钟和使能信号,看时序对不对。我遇到过DDR的ODT配置不对,导致信号反射严重。
- U-Boot加载内核失败:检查存储介质的读写速度。有些SD卡在高速模式下会丢数据,降速到25MHz就稳定了。
- 内核启动卡住:在U-Boot里用
bootargs加earlyprintk和debug参数。如果还不行,检查设备树里的内存节点——我见过有人把内存大小写错了,内核只认到一半内存。 - 环境变量丢失:检查存储介质的坏块。NAND Flash有坏块是常态,U-Boot的环境变量驱动必须支持坏块管理。
一句话总结:U-Boot调试,本质上是“硬件初始化 + 数据搬运 + 状态传递”三个环节的排查。从SPL到U-Boot,再到内核,每一步都要确认前一步的结果正确。别跳步,别假设,用打印和示波器说话。