第三章 Bootloader原理与U-Boot
嵌入式Linux系统启动,第一步是谁在干活?
不是内核,不是文件系统。是Bootloader。
我经常跟团队里的新人说:Bootloader就是嵌入式系统的"接生婆"。它负责把硬件从复位状态唤醒,然后稳稳当当地把内核请进内存。今天我们就来聊聊这个"接生婆"——它的作用、启动流程,以及U-Boot这个最主流的Bootloader怎么用。
3.1 Bootloader到底在干什么?
说白了,Bootloader就干三件事:
- 初始化硬件——CPU、内存、串口、存储控制器,这些都得先跑起来
- 加载内核——从Flash、SD卡或者网络把内核镜像搬到内存里
- 跳转执行——把控制权交给内核,自己功成身退
你想想看,如果没有Bootloader,CPU上电后寄存器全是默认值,内存都没初始化,内核代码往哪儿放?
核心要点:Bootloader是硬件与操作系统之间的桥梁。它不依赖操作系统,自己直接操作硬件寄存器。
3.2 启动流程——从复位到内核
我做过好几个平台的移植,ARM、RISC-V都碰过。启动流程虽然细节不同,但大框架基本一致。下面这张图是我自己总结的:
我的经验:Stage 1的汇编代码是最容易出问题的。我记得有一次调试,板子死活起不来,最后发现是DDR时序参数配错了。嗯,这种问题用示波器量一下DQ信号就能看出来。
3.3 U-Boot的配置与编译
U-Boot是目前最主流的Bootloader,没有之一。为什么?因为它支持几百种开发板,社区活跃,功能强大。
3.3.1 获取源码
从官方仓库拉下来就行:
git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
我个人习惯用LTS版本,稳定。开发板厂商也会提供自己的分支,里面通常有板级支持包。
3.3.2 配置
U-Boot的配置系统跟Linux内核很像,也是基于Kconfig。配置命令:
# 针对特定开发板
make <board_name>_defconfig
# 例如,树莓派3
make rpi_3_defconfig
# 然后可以进一步微调
make menuconfig
这里有个坑——defconfig只是基础配置。实际项目中,你往往需要自己加一些驱动或者改一些参数。比如我做过一个项目,需要从NAND Flash启动,但默认配置只支持eMMC。那就得进menuconfig里把NAND驱动勾上。
⚠️ 注意:配置完成后,会生成一个.config文件。千万别手动改这个文件!用make menuconfig或者make savedefconfig来管理配置。
3.3.3 编译
配置好了就编译:
# 设置交叉编译工具链
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
# 编译
make -j4
编译完成后,会生成几个关键文件:
| 文件 | 说明 | 用途 |
|---|---|---|
| u-boot.bin | 原始二进制镜像 | 直接烧写到Flash |
| u-boot.img | 带U-Boot头部的镜像 | 用于网络加载或SPL加载 |
| u-boot-spl.bin | SPL(第一阶段Bootloader) | 某些SoC需要SPL来初始化DDR |
| tools/mkimage | 镜像打包工具 | 制作内核镜像、ramdisk等 |
避坑指南:我曾经在编译时忘记设置CROSS_COMPILE,结果编译出来的东西在板子上跑不起来。检查了半天才发现是x86的代码。所以,交叉编译工具链一定要配对。
3.4 U-Boot常用命令
U-Boot启动后,会进入命令行界面。这里我挑几个最常用的命令讲讲。
3.4.1 信息查询类
# 查看板子信息
bdinfo
# 查看内存布局
iminfo
# 查看环境变量
printenv
printenv是我用得最多的命令。环境变量里存了启动参数、IP地址、文件路径等关键信息。
3.4.2 内存操作类
# 查看内存内容
md.b 0x80000000 100 # 以字节方式显示
md.w 0x80000000 100 # 以字方式显示
# 修改内存
mm.b 0x80000000 # 交互式修改
# 拷贝内存
cp.b 0x80000000 0x90000000 10000
调试驱动时,这些命令特别有用。比如你想验证某个寄存器写没写对,直接md看一眼就知道了。
3.4.3 存储设备操作
# 查看存储设备
mmc list # 列出MMC设备
mmc dev 0 # 切换到MMC设备0
mmc info # 查看设备信息
# 从MMC读取到内存
mmc read 0x80000000 0x800 0x4000
# 从内存写入MMC
mmc write 0x80000000 0x800 0x4000
这里要注意地址对齐。我曾经遇到过一个问题:mmc read的源地址必须是块对齐的(512字节对齐),否则会报错。
3.4.4 网络操作
# 设置网络
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10
saveenv
# 通过TFTP下载文件
tftp 0x80000000 uImage
# 通过NFS挂载根文件系统
nfs 0x80000000 192.168.1.10:/nfsroot/uImage
网络启动是开发阶段最常用的方式。改完内核,编译,tftp下载,重启——整个过程不到10秒。比反复烧写Flash快多了。
3.4.5 启动内核
# 从内存启动
bootm 0x80000000
# 或者用bootz(针对zImage)
bootz 0x80000000
# 设置启动参数
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw'
bootm 0x80000000
这里有个小技巧:把启动命令写成脚本,存到环境变量里:
setenv bootcmd 'mmc read 0x80000000 0x800 0x4000; bootm 0x80000000'
saveenv
这样上电后自动执行,不用每次都手动敲命令。
我的习惯:我会在U-Boot里留一个"救急"环境变量。比如setenv rescue 'tftp 0x80000000 uImage; bootm'。万一板子起不来,按任意键进入U-Boot,敲个run rescue就能从网络启动。这个习惯救过我好几次。
3.5 小结
Bootloader是嵌入式系统的起点。U-Boot作为事实标准,你得学会配置、编译、烧写和调试。今天讲的这些命令,在实际开发中天天都会用到。
嗯,最后提醒一句:别小看Bootloader。很多系统启动问题,追根溯源都是Bootloader没配好。花点时间把它搞透,后面调试内核和驱动会省很多事。
一句话总结:Bootloader是硬件的"初始化器",U-Boot是Bootloader的"瑞士军刀"。