4. Bootloader深度解析(以U-Boot为例)

Bootloader,说白了就是嵌入式系统的「第一段代码」。板子一上电,CPU 从复位向量开始执行,这时候内存还没初始化,内核还在 Flash 里躺着,谁来把硬件准备好、把内核请进内存?就是 Bootloader。

我刚开始做嵌入式那会儿,总觉得 Bootloader 就是个「跳板」,随便用用就行。直到有一次,我在一块新板子上调 U-Boot,串口死活没输出,查了三天才发现是 DDR 时序参数配错了。嗯,从那以后我再也不敢小看这玩意儿了。

4.1 Bootloader 到底干了什么?

一句话总结:Bootloader 负责把硬件从「原始状态」带到「可运行内核的状态」。具体来说,它要干这几件事:

  • 初始化最核心的硬件:时钟、CPU 频率、DDR 控制器、串口。没有串口,你连它死在哪都不知道。
  • 建立内存映射:让 CPU 能正常访问 DDR,把代码从 Flash 搬到内存里跑。
  • 加载内核映像:从 Flash、SD 卡、网络(TFTP)甚至 USB 里把内核读出来,放到内存指定地址。
  • 传递启动参数:把内核需要的命令行参数(比如根文件系统在哪、控制台用哪个串口)告诉内核。
  • 跳转到内核入口:最后一条指令,就是跳转到内核的起始地址,把控制权交给内核。

核心概念:Bootloader 是「硬件相关」的。换一块板子,哪怕 CPU 一样,DDR 型号不同、GPIO 复用不同,Bootloader 就得重新配。这也是为什么 U-Boot 要针对每块板子单独配置编译。

4.2 U-Boot 的启动流程

U-Boot 的启动分两个阶段,我习惯叫它 SPL(第一阶段)和 U-Boot proper(第二阶段)。为什么分两段?因为早期的 Bootloader 体积小,芯片内部 SRAM 可能只有 4KB 或 8KB,装不下完整的 U-Boot。所以先跑一个极简的 SPL,初始化 DDR,再把完整的 U-Boot 从 Flash 搬到 DDR 里跑。

下面这张图是我自己整理的启动流程,你一看就明白了:

U-Boot 启动流程 U-Boot 启动流程(SPL + U-Boot proper) 上电复位 SPL 入口(ROM 代码) SPL 初始化:时钟、DDR、串口 从 Flash 加载 U-Boot proper 到 DDR 跳转到 U-Boot proper 入口 U-Boot proper 初始化 完整外设初始化、环境变量、命令行 加载内核并跳转 说明 SPL 通常放在芯片内部 SRAM 跑 U-Boot proper 在 DDR 里跑 内核由 U-Boot 加载到 DDR 指定地址

你看,整个流程其实不复杂。SPL 是「瘦子」,只做最必要的事;U-Boot proper 是「胖子」,功能齐全,甚至能给你一个命令行交互环境。

4.3 获取 U-Boot 源码

U-Boot 的官方源码托管在 Denx 的 GitLab 上。我个人习惯用 LTS 版本,比如 2023.04 或 2024.01,稳定,社区支持好。

获取源码很简单:

# 克隆最新主线
git clone https://source.denx.de/u-boot/u-boot.git

# 或者指定某个稳定版本
git clone -b v2024.01 https://source.denx.de/u-boot/u-boot.git

如果你用的是厂商提供的 BSP,他们通常会基于某个 U-Boot 版本打上自己的补丁。我建议你先用主线版本试试,如果不行再切到厂商版本。为什么?主线版本文档全、社区活跃,遇到问题好搜。

小技巧:下载完源码后,先看看 board/ 目录下有没有你的板子。如果有,恭喜你,直接配置编译就行。如果没有,你得自己移植——那是另一门课了。

4.4 配置 U-Boot

U-Boot 的配置系统跟 Linux 内核很像,也是基于 Kconfig。配置分两步:

  1. 选择板级配置:每个板子都有一个默认配置,放在 configs/ 目录下。
  2. 微调配置项:用 menuconfig 打开图形界面,按需开关功能。

举个例子,假设我手头有一块 QEMU 的 ARM 虚拟板(vexpress_ca9x4):

# 第一步:加载默认配置
make vexpress_ca9x4_defconfig

# 第二步:微调配置(可选)
make menuconfig

在 menuconfig 里,你可以配置这些东西:

配置项 说明 我的建议
CONFIG_BOOTDELAY 启动倒计时(秒),倒计时结束自动启动内核 调试时设成 -1(不自动启动),发布时设成 3
CONFIG_BOOTCOMMAND 自动启动时执行的命令 比如 fatload mmc 0:1 0x60000000 uImage; bootm 0x60000000
CONFIG_BAUDRATE 串口波特率 115200 最常用,别用 9600,太慢
CONFIG_NET 网络功能支持 调试阶段建议打开,方便 TFTP 下载内核

注意:配置完一定要检查 .config 文件。我曾经遇到过 CONFIG_SYS_TEXT_BASE 设错了,U-Boot 加载到 DDR 后直接跑飞,串口输出一堆乱码。这个地址必须跟你的板子 DDR 映射一致。

4.5 编译 U-Boot

配置好之后,编译就简单了。但要注意交叉编译工具链的设置:

# 设置交叉编译工具链前缀
export CROSS_COMPILE=arm-linux-gnueabihf-

# 指定目标架构
export ARCH=arm

# 编译(-j 后面跟 CPU 核心数,加速编译)
make -j4

编译完成后,你会得到几个关键文件:

  • u-boot.bin:完整的 U-Boot 映像,可以直接烧录到 Flash 的偏移地址。
  • u-boot.img:带 U-Boot 头部信息的映像,通常由 SPL 加载。
  • SPL:如果板子支持 SPL,还会生成 SPL 或 u-boot-spl.bin。
  • u-boot.dtb:设备树文件,U-Boot 用它来知道板子上有哪些硬件。

我习惯在编译完后,用 file 命令检查一下生成的二进制文件:

file u-boot.bin
# 输出示例:u-boot.bin: data

如果输出是 ELF 格式,说明你忘了指定 CONFIG_OF_EMBED 或者链接脚本有问题。正常的 U-Boot 二进制是裸的 data 格式。

4.6 烧录到目标板

烧录方式取决于你的板子。我列几种常见的:

存储介质 烧录方法 常用工具
SD 卡 将 u-boot.bin 写入 SD 卡偏移地址(如 1MB 处) dd、balenaEtcher
NAND Flash 通过串口或网络下载,用 U-Boot 自己的 nand write 命令烧录 kermit、lrzsz
SPI Flash 用编程器或通过 U-Boot 的 sf write 命令 flashrom、sf probe
网络(TFTP) U-Boot 启动后,通过 TFTP 下载新版本并写入 Flash tftp、dhcp

以 SD 卡为例,假设你的 SD 卡设备是 /dev/sdb,U-Boot 要烧录到偏移 1MB 的位置:

# 注意:确认 /dev/sdb 是你的 SD 卡,不是硬盘!
sudo dd if=u-boot.bin of=/dev/sdb bs=1024 seek=1 conv=fsync

seek=1 的意思是跳过前 1KB?不对,bs=1024 时 seek=1 就是跳过 1KB。但大多数板子要求 U-Boot 放在偏移 1MB 处,所以应该是 bs=1024 seek=1024。嗯,这里容易搞混,我建议你查一下板子的手册,确认偏移地址。

避坑指南:我曾经在一块 i.MX6 板子上烧录 U-Boot,忘了设 seek 偏移,直接把 U-Boot 写到了 SD 卡的 0 扇区。结果板子启动时找不到分区表,SD 卡变砖了。后来用 fdisk 重建分区表才救回来。所以,烧录前一定要确认偏移地址。

4.7 验证启动

烧录完成后,插上 SD 卡(或接好 Flash),上电,打开串口终端(我用的是 screen 或 minicom,波特率 115200):

screen /dev/ttyUSB0 115200

如果一切正常,你会看到 U-Boot 的启动信息:

U-Boot 2024.01 (Jan 15 2025 - 10:30:00 +0800)

CPU:   ARM Cortex-A9
DRAM:  512 MiB
MMC:   mmc@0: 0
Net:   eth0: ethernet@10000000

Hit any key to stop autoboot:  3

看到这个提示,说明 U-Boot 已经跑起来了。按任意键进入命令行,你可以试试几个常用命令:

  • printenv:打印所有环境变量
  • bdinfo:查看板子信息(内存布局、时钟频率等)
  • mmc info:查看 SD 卡信息
  • help:列出所有可用命令

如果串口没输出,别慌。先检查这几样:

  1. 串口线接对了没?TX/RX 有没有交叉?
  2. 波特率是不是 115200?有些板子默认用 38400。
  3. U-Boot 的 CONFIG_DEBUG_UART 打开了没?
  4. DDR 初始化有没有报错?有时候 DDR 时序不对,U-Boot 根本跑不到串口初始化那一步。

调试技巧:如果 U-Boot 完全没输出,可以试试在 SPL 阶段加几个 GPIO 翻转的代码,用示波器看 GPIO 波形,判断代码执行到了哪一步。这招我在调新板子时用过无数次,比瞎猜管用多了。

好了,到这一步,U-Boot 已经成功在你的板子上跑起来了。接下来,你就可以用它来加载内核、调试驱动、甚至做量产烧录。Bootloader 这东西,配好了就是一劳永逸,配不好就是噩梦。但只要你理解了它的启动流程和配置方法,剩下的就是耐心和细心了。