3、Bootloader深度解析:U-Boot启动流程、U-Boot设备树支持、U-Boot环境变量与分区表、工业产品中的安全启动链
Bootloader,说白了就是板子上电后第一个跑起来的程序。它负责把内核从Flash里捞出来,放到内存里,然后跳转过去。在工业产品里,这一步要是出问题,整个系统就瘫了。我这些年调试过的板子,至少有一半的启动故障都跟Bootloader配置有关。
今天咱们就聊聊U-Boot,这个嵌入式世界里的“老黄牛”。我会结合自己踩过的坑,把启动流程、设备树、环境变量、分区表和安全启动链这几个关键点掰开揉碎了讲。
3.1 U-Boot启动流程:从复位向量到命令行
U-Boot的启动分两个阶段。第一阶段是汇编写的,跟CPU架构强相关。第二阶段是C语言写的,可移植性好。
3.1.1 Stage 1:汇编阶段
板子上电,CPU从复位向量地址开始执行。通常是0x00000000或者0xFFFF0000。U-Boot的第一条指令就在这里。
这个阶段干的事很纯粹:
- 设置CPU为SVC模式,关中断
- 初始化关键硬件:时钟、看门狗、SDRAM控制器
- 把自身从Flash拷贝到SDRAM(这叫重定位)
- 设置栈指针,跳转到C语言入口
3.1.2 Stage 2:C语言阶段
进入C语言后,U-Boot调用一系列初始化函数。入口是board_init_f()和board_init_r()。
关键流程如下:
- board_init_f():初始化串口、定时器、GPIO等外设。这时候你就能看到串口打印了。
- init_sequence_f[]:一个函数指针数组,按顺序调用各个子系统的初始化。
- relocate_code():把U-Boot自身从临时位置搬到最终运行地址。
- board_init_r():初始化完整的外设栈,包括NAND、MMC、网络等。
- main_loop():进入命令行循环,等待用户输入或者自动启动内核。
嗯,这里要注意:board_init_f()和board_init_r()之间有个重定位操作。重定位之后,全局变量和函数指针的地址都变了。所以U-Boot里大量使用gd(全局数据)结构体,它通过寄存器来寻址,不受重定位影响。
核心要点: U-Boot启动流程可以概括为“三段式”——汇编初始化、C语言板级初始化、命令行/自动启动。每一段都有明确的入口和出口。
3.2 U-Boot设备树支持:从扁平到结构化
早期的U-Boot靠头文件里的宏定义来描述硬件。比如#define CONFIG_SYS_CLK_FREQ 50000000。这种做法在芯片越来越多、板级越来越复杂之后,简直是一场噩梦。
设备树(Device Tree)的出现解决了这个问题。U-Boot从2014年开始全面支持设备树。现在的新板子,基本都走设备树路线。
3.2.1 设备树在U-Boot中的角色
U-Boot使用设备树干三件事:
- 传递硬件信息给内核:U-Boot把设备树二进制(DTB)加载到内存,把地址告诉内核。
- 自身驱动匹配:U-Boot的驱动模型(Driver Model)通过设备树节点来匹配和初始化外设。
- 运行时修改:U-Boot可以在启动前修改设备树节点,比如调整内存大小、使能/禁用某个外设。
3.2.2 设备树编译与加载
设备树源文件(DTS)通过dtc编译器生成DTB。U-Boot里通常把DTB打包进U-Boot镜像,或者单独存放在Flash分区里。
# 编译设备树
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# U-Boot中加载DTB
# 假设DTB存放在nand的dtb分区
nand read ${fdt_addr} dtb
fdt addr ${fdt_addr}
3.2.3 设备树覆盖(Overlay)
工业产品经常需要支持多种硬件变体。比如同一款控制器,有的带CAN总线,有的带RS485。设备树覆盖允许你在基础设备树上叠加修改,不用维护多份完整的DTS。
U-Boot支持在启动时动态应用overlay:
# 加载基础DTB
load mmc 0:1 ${fdt_addr} base.dtb
fdt addr ${fdt_addr}
# 加载并应用overlay
load mmc 0:1 ${fdt_addr} can-overlay.dtbo
fdt apply ${fdt_addr}
3.3 U-Boot环境变量与分区表
环境变量是U-Boot的“记忆”。它保存了启动参数、IP地址、文件路径等配置。分区表则告诉U-Boot和内核,Flash里哪些区域归谁管。
3.3.1 环境变量存储与操作
环境变量默认存储在Flash的固定区域。U-Boot启动时把它读到内存里。常用的存储介质有:
| 存储介质 | 特点 | 适用场景 |
|---|---|---|
| NOR Flash | 字节寻址,擦除寿命长 | 小容量、高可靠性产品 |
| NAND Flash | 块寻址,容量大 | 大容量存储产品 |
| MMC/SD卡 | 可插拔,容量大 | 开发调试阶段 |
| EEPROM | 字节寻址,寿命极长 | 关键参数存储 |
常用的环境变量操作命令:
# 查看所有环境变量
printenv
# 设置环境变量
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2'
# 保存到Flash
saveenv
# 删除环境变量
setenv myvar
3.3.2 分区表设计
工业产品的Flash分区表需要精心设计。我见过太多产品因为分区规划不合理,导致后期升级困难。
一个典型的分区表如下:
| 分区名称 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| SPL | 0x00000000 | 64KB | 一级Bootloader |
| U-Boot | 0x00010000 | 512KB | 主Bootloader |
| U-Boot环境变量 | 0x00090000 | 128KB | 环境变量存储 |
| DTB | 0x000B0000 | 128KB | 设备树 |
| Kernel | 0x000D0000 | 4MB | Linux内核 |
| Rootfs | 0x004D0000 | 剩余空间 | 根文件系统 |
设计分区表时,我建议遵循几个原则:
- 预留冗余空间:U-Boot和内核分区留出20%的余量,方便后续升级。
- 环境变量单独分区:避免跟其他数据混在一起,防止误擦除。
- 坏块管理:NAND Flash要用UBI/UBIFS,不要直接使用原始分区。
3.4 工业产品中的安全启动链
工业产品对安全性的要求越来越高。安全启动链确保只有经过签名的固件才能运行。从Bootloader到内核,每一级都要验证下一级的签名。
3.4.1 安全启动链的层级
典型的工业安全启动链分四层:
- ROM Bootloader:固化在芯片内部,不可修改。验证SPL的签名。
- SPL:验证U-Boot主镜像的签名。
- U-Boot:验证内核和设备树的签名。
- 内核:验证文件系统的签名(可选)。
每一级验证通过后,才把控制权交给下一级。任何一级验证失败,系统都会停止启动并报错。
3.4.2 签名与验证流程
以U-Boot验证内核为例,流程如下:
# 1. 生成密钥对
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem
# 2. 对内核镜像签名
openssl dgst -sha256 -sign private_key.pem -out kernel.sig kernel.itb
# 3. U-Boot中验证
# 假设内核镜像和签名已加载到内存
load mmc 0:1 ${loadaddr} kernel.itb
load mmc 0:1 ${sigaddr} kernel.sig
# 使用公钥验证
rsa verify ${loadaddr} ${filesize} ${sigaddr}
3.4.3 工业产品的特殊要求
工业产品跟消费电子不一样,有几个特殊点:
- 长期供货:密钥管理要考虑10年以上的生命周期。密钥丢失意味着产品报废。
- 现场升级:安全启动不能阻碍合法的固件升级。需要设计安全的升级流程。
- 故障恢复:万一安全启动失败,要有恢复机制。比如从备份分区启动,或者进入恢复模式。
我个人建议,工业产品至少要实现两级安全启动:ROM验证SPL,SPL验证U-Boot。内核级别的验证可以根据产品安全等级来决定是否启用。
总结一下: 安全启动链的核心是“信任链”。每一级都信任上一级,验证下一级。这个链条一旦断裂,整个系统就不可信了。
好了,这一章的内容就到这里。U-Boot的启动流程、设备树支持、环境变量和安全启动链,是工业产品BSP开发的基础。把这些搞明白了,后面移植内核和文件系统就会顺畅很多。