3. Bootloader原理与U-Boot:Bootloader作用、U-Boot源码结构、U-Boot启动流程、常用命令
好,咱们进入第三章。Bootloader,说白了就是嵌入式系统的「看门人」加「搬运工」。你想想看,CPU 上电那一刻,内存还没初始化,Flash 还不能随便读,更别提 Linux 内核了——谁来干这些脏活累活?就是 Bootloader。
3.1 Bootloader 到底在干什么
我个人习惯把 Bootloader 的作用归纳为三件事:
- 硬件初始化:关看门狗、设置时钟、初始化 SDRAM/DDR、初始化串口。这些事不做,后面啥也跑不起来。
- 加载内核:从 Flash、SD 卡、网络或 USB 把内核镜像读到内存里。
- 传递启动参数:把内核需要的参数(比如根文件系统在哪、控制台用哪个串口)通过某种约定方式传给内核。
核心要点:Bootloader 是硬件相关最紧密的软件层。换一块板子,Bootloader 几乎必须重写或至少大改。我在项目中遇到过有人直接把 A 板的 U-Boot 烧到 B 板上,结果串口没输出——因为 GPIO 复用配置完全不同。
常见的 Bootloader 有 U-Boot、Barebox、GRUB(x86 用得多)。工业嵌入式领域,U-Boot 是绝对的主流,没有之一。为什么?支持架构多、社区活跃、功能全。咱们后面全围绕 U-Boot 讲。
3.2 U-Boot 源码结构——别被目录吓到
我第一次看 U-Boot 源码树,说实话有点懵。目录太多了。但摸清规律后,其实就几个关键区域:
| 目录/文件 | 作用 | 我关注的点 |
|---|---|---|
arch/ |
架构相关代码,比如 arm、riscv、x86 | 移植时主要改 arch/arm/mach-xxx/ |
board/ |
板级代码,每个板子一个目录 | 这里放你的板子特有的初始化 |
include/configs/ |
板级配置头文件 | 裁剪和移植的核心战场 |
common/ |
通用命令和框架代码 | 一般不动,除非加新命令 |
drivers/ |
驱动,串口、网卡、Flash、MMC 等 | 移植时可能需要加新驱动 |
net/ |
网络协议栈,TFTP、NFS 等 | 调试时常用,裁剪时可去掉 |
fs/ |
文件系统支持,ext4、fat、jffs2 等 | 按需选,不用全开 |
tools/ |
主机端工具,比如 mkimage | 打包内核镜像必须用 |
我的习惯:拿到一个新板子,先看 board/ 下有没有类似参考板,然后直接复制一份改。别从头写,那是给自己找麻烦。
3.3 U-Boot 启动流程——从复位到命令行
U-Boot 的启动流程,我画了一张图,你跟着走一遍就清楚了。
流程其实不复杂,我拆成几个关键阶段讲:
3.3.1 第一阶段:汇编阶段(start.S)
CPU 复位后第一条指令就在这里。这个阶段干的事很纯粹:
- 设置 CPU 为 SVC 模式(超级用户模式)
- 关中断、关 MMU、关 D-Cache
- 设置栈指针(SP),为跳转到 C 语言做准备
嗯,这里要注意:这个阶段的代码必须与位置无关(PIC)。因为这时候 U-Boot 可能还在 Flash 里运行,地址和链接地址不一样。我在移植时踩过这个坑——忘了加 pie 编译选项,结果跳转到 C 函数直接跑飞。
3.3.2 第二阶段:lowlevel_init
这个函数负责最基础的硬件初始化:
- 设置系统时钟(PLL、分频)
- 初始化 DDR 控制器
- 初始化串口(早期调试输出)
说白了,这一步做完,内存能用了,串口能打印了。我习惯在这里加一个串口打印,确认板子至少跑到了这里——这是移植的第一步验证。
3.3.3 第三阶段:_main 到 board_init_f
进入 C 环境后,U-Boot 开始做更复杂的事:
- 初始化一个临时堆栈
- 调用
board_init_f(),完成板级初始化(GPIO、定时器、环境变量准备) - 计算重定位地址,把 U-Boot 自身从 Flash 复制到 DDR 中
避坑指南:我曾经在 board_init_f 里加了一个很大的数组,结果栈溢出,U-Boot 在重定位前就挂了。记住,这个阶段栈空间非常有限,别搞大数组。
3.3.4 第四阶段:board_init_r 到命令行
重定位完成后,U-Boot 在 DDR 里全速运行:
- 初始化所有驱动(网卡、Flash、MMC、USB 等)
- 设置环境变量(从 Flash 或 EEPROM 读取)
- 进入
main_loop(),等待用户输入命令
如果没用户干预,U-Boot 会执行 bootcmd 环境变量里的命令,自动加载并启动内核。
3.4 常用命令——调试三板斧
U-Boot 的命令行,说白了就是你的调试瑞士军刀。我列几个最常用的:
| 命令 | 作用 | 我常用的场景 |
|---|---|---|
printenv |
打印所有环境变量 | 检查 bootargs、bootcmd 对不对 |
setenv |
设置环境变量 | 临时改启动参数,比如 setenv bootargs console=ttyS0,115200 |
saveenv |
保存环境变量到 Flash | 改完别忘了保存,不然重启就丢了 |
tftp |
通过网络下载文件 | 调试内核时最常用,tftp 0x42000000 zImage |
fatload |
从 FAT 分区加载文件 | 从 SD 卡或 eMMC 的 FAT 分区读内核 |
bootm |
启动内核镜像 | bootm 0x42000000,启动内存里的内核 |
md |
内存显示 | 调试时看内存内容,md.b 0x42000000 100 |
mw |
内存写 | 改内存值,调试硬件寄存器 |
flinfo |
查看 Flash 信息 | 确认 Flash 型号和分区 |
erase |
擦除 Flash | 烧写前必须擦,erase 0x200000 +0x100000 |
我的调试流程:拿到一块新板子,先 printenv 看默认配置,然后 tftp 下载一个已知能跑的内核,bootm 启动。如果串口有内核打印,说明 U-Boot 移植基本成功了。如果没打印,回头查串口驱动和 DDR 初始化。
3.5 一个典型的启动命令序列
假设你从网络启动内核,命令大概是这样的:
# 设置服务器 IP 和本机 IP
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.10
# 下载内核和设备树
tftp 0x42000000 zImage
tftp 0x43000000 board.dtb
# 设置启动参数
setenv bootargs console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/nfsroot ip=192.168.1.10
# 启动
bootm 0x42000000 - 0x43000000
注意 bootm 后面的三个参数:内核地址、ramdisk 地址(没有就写 -)、设备树地址。这个顺序别搞反了,我见过有人把设备树和内核地址写反,内核直接 panic。
总结一下:Bootloader 是嵌入式系统的起点,U-Boot 是工业界的事实标准。理解它的源码结构、启动流程和常用命令,是后续裁剪和移植的基础。下一章咱们会深入 U-Boot 的配置系统和裁剪方法——到时候你会知道,U-Boot 其实可以瘦身到 200KB 以内。