3. Bootloader原理与U-Boot移植:启动流程、板级配置与烧录
Bootloader,说白了就是板子上电后第一个跑的程序。它负责把内核从Flash里搬出来,放到内存里,然后跳过去执行。我刚开始接触嵌入式时,总觉得Bootloader就是个“跳板”,后来踩了几次坑才明白——它其实是整个系统的基石。
U-Boot是目前最主流的Bootloader,支持ARM、RISC-V、x86等几乎所有架构。这一章,我们就把它彻底搞透。
3.1 U-Boot启动流程分析
U-Boot的启动分两个阶段。为什么分两段?因为第一阶段在SRAM里跑,空间小、功能弱;第二阶段才把代码搬到DDR里,这时候才能放开手脚。
核心要点:第一阶段(SPL)负责初始化最小硬件(时钟、DDR),第二阶段(U-Boot proper)负责加载内核、文件系统。
3.1.1 第一阶段:SPL(Secondary Program Loader)
SPL是U-Boot的“瘦身版”。它被烧录在ROM或Flash的固定位置,上电后由片内ROM加载到SRAM里执行。
SPL做的事儿很纯粹:
- 设置CPU为SVC模式,关中断
- 初始化时钟、串口、DDR控制器
- 从Flash/MMC/NAND加载U-Boot proper到DDR
- 跳转到U-Boot proper入口
我记得第一次移植U-Boot到一块新板子上,SPL死活跑不起来。后来用示波器量时钟引脚,发现PLL配置错了——分频系数写反了。嗯,这种低级错误,犯过一次就记住了。
3.1.2 第二阶段:U-Boot proper
U-Boot proper才是我们常说的“U-Boot”。它功能完整,支持命令行、网络、文件系统、环境变量等。
它的启动流程大致如下:
- board_init_f():初始化板级外设(GPIO、I2C、SPI等),设置内存布局
- relocate_code():把U-Boot自身从Flash拷贝到DDR的高地址
- board_init_r():重新初始化外设,建立堆栈,启动命令行
- main_loop():等待用户输入,或自动加载内核
我的习惯:调试阶段,我会在board_init_f()里加串口打印,确认板级初始化是否通过。如果串口没输出,问题大概率在SPL阶段。
3.2 板级配置文件的修改
U-Boot的板级支持,核心就是改配置文件。每个板子对应一个头文件,放在include/configs/目录下。
举个例子,我移植一块基于i.MX6ULL的板子,配置文件叫mx6ull_myboard.h。里面主要配置这几项:
| 配置项 | 说明 | 常见值 |
|---|---|---|
CONFIG_SYS_TEXT_BASE |
U-Boot在DDR中的加载地址 | 0x87800000 |
CONFIG_SYS_LOAD_ADDR |
内核加载地址 | 0x82000000 |
CONFIG_BOOTCOMMAND |
自动启动时执行的命令 | "mmc read ...; bootz" |
CONFIG_BOOTARGS |
传给内核的命令行参数 | "console=ttymxc0,115200" |
CONFIG_SYS_CLK_FREQ |
系统时钟频率 | 24000000 |
你想想看,这些配置如果有一个不对,板子就可能起不来。我曾经因为CONFIG_SYS_TEXT_BASE写错了地址,U-Boot加载到DDR后直接跑飞,串口输出一堆乱码。排查了半天才发现是地址冲突了。
3.2.1 设备树(Device Tree)的适配
现代U-Boot使用设备树来描述硬件。板级设备树文件放在arch/arm/dts/目录下。
你需要修改或新建一个.dts文件,至少包含:
- CPU型号、主频
- DDR大小、时序参数
- 串口、网卡、MMC等外设的寄存器地址和中断号
- GPIO复用配置(pinctrl)
避坑指南:我曾经在设备树里漏配了网卡的复位GPIO,导致U-Boot能启动,但网络死活不通。后来用mdio工具读PHY寄存器,发现PHY一直处于复位状态。加上GPIO配置后,问题解决。
3.3 U-Boot的编译与烧录
编译U-Boot,说白了就是三步:配置、编译、打包。
3.3.1 编译步骤
# 1. 清理
make distclean
# 2. 配置板级
make mx6ull_myboard_defconfig
# 3. 编译
make -j4
# 4. 生成SPL和U-Boot镜像
# 输出文件:SPL、u-boot.img、u-boot-dtb.img
编译完成后,你会得到几个关键文件:
- SPL:第一阶段引导程序,烧录到偏移0x0
- u-boot.img:第二阶段镜像,烧录到偏移0x200(或根据板子定义)
- u-boot-dtb.img:包含设备树的完整镜像
3.3.2 烧录方式
烧录U-Boot,常见的有三种方式:
| 方式 | 工具 | 适用场景 |
|---|---|---|
| SD卡烧录 | dd命令 |
开发阶段,快速验证 |
| NAND/NOR Flash烧录 | JTAG或厂家工具 | 量产阶段 |
| 网络烧录(TFTP) | U-Boot自带tftp命令 | 已有U-Boot,升级版本 |
我个人习惯用SD卡烧录,因为快。命令如下:
# 假设SD卡设备是/dev/sdb
sudo dd if=SPL of=/dev/sdb bs=1k seek=1
sudo dd if=u-boot.img of=/dev/sdb bs=1k seek=69
sync
注意seek偏移量,不同板子不一样。我见过有人把U-Boot烧到SD卡的分区表区域,结果卡在电脑上无法识别——嗯,分区表被覆盖了。
我的建议:烧录前先用hexdump查看SD卡前几个扇区的内容,确认没有重要数据。另外,烧录完成后,用md命令在U-Boot命令行里读一下内存,验证镜像是否加载正确。
3.4 U-Boot启动流程图
下面这张图,把整个启动流程串起来了。你对照着看,会更清楚。
这张图里,从“上电复位”到“main_loop()”,一共7个关键节点。你移植时,每个节点都可能出问题。我的经验是:先确保SPL能跑通,再调试U-Boot proper。别想一口吃成胖子。
3.5 常见问题与调试技巧
最后,分享几个我实际项目中遇到的坑:
- 串口无输出:检查波特率、引脚复用、时钟配置。我遇到过因为时钟树配错,串口波特率实际是115200但配置成9600的情况。
- DDR初始化失败:用厂家提供的DDR训练工具,先确认DDR时序参数。别自己瞎猜,DDR时序差一点就起不来。
- U-Boot加载内核后死机:检查内核加载地址和
bootm命令的参数是否匹配。我曾经把内核加载到DDR的末尾,结果解压时覆盖了U-Boot自身。
重要提醒:调试U-Boot时,一定要保留一个能用的版本。我习惯在SD卡上同时放两个U-Boot镜像,一个稳定版,一个测试版。万一测试版刷坏了,还能从稳定版启动。
好了,U-Boot的启动流程、板级配置、编译烧录,就讲到这里。你动手移植时,记得对照着流程图一步步来。遇到问题别慌,先看串口输出,再看设备树,最后查配置——90%的问题都能解决。