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 里跑。
下面这张图是我自己整理的启动流程,你一看就明白了:
你看,整个流程其实不复杂。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。配置分两步:
- 选择板级配置:每个板子都有一个默认配置,放在
configs/目录下。 - 微调配置项:用
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:列出所有可用命令
如果串口没输出,别慌。先检查这几样:
- 串口线接对了没?TX/RX 有没有交叉?
- 波特率是不是 115200?有些板子默认用 38400。
- U-Boot 的
CONFIG_DEBUG_UART打开了没? - DDR 初始化有没有报错?有时候 DDR 时序不对,U-Boot 根本跑不到串口初始化那一步。
调试技巧:如果 U-Boot 完全没输出,可以试试在 SPL 阶段加几个 GPIO 翻转的代码,用示波器看 GPIO 波形,判断代码执行到了哪一步。这招我在调新板子时用过无数次,比瞎猜管用多了。
好了,到这一步,U-Boot 已经成功在你的板子上跑起来了。接下来,你就可以用它来加载内核、调试驱动、甚至做量产烧录。Bootloader 这东西,配好了就是一劳永逸,配不好就是噩梦。但只要你理解了它的启动流程和配置方法,剩下的就是耐心和细心了。