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 的启动流程,我画了一张图,你跟着走一遍就清楚了。

U-Boot 启动流程 复位向量 start.S(汇编阶段) lowlevel_init(时钟/DDR) _main(C 环境) board_init_f(重定位前) 重定位 + board_init_r 最终进入 main_loop(),等待用户输入命令或自动启动内核

流程其实不复杂,我拆成几个关键阶段讲:

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 以内。