Bootloader原理与移植:从启动流程到板级验证
Bootloader,说白了就是嵌入式系统的"第一口饭"。CPU上电后,它得先知道自己该干什么——初始化硬件、加载操作系统,然后功成身退。我这些年做BSP,跟U-Boot打交道最多,今天就带你把它拆开看看。
Bootloader启动流程:CPU上电后发生了什么?
先理清一个概念:Bootloader不是操作系统,它只是个"搬运工"。但这位搬运工干的事,直接决定了系统能不能跑起来。
典型的启动流程分三个阶段:
- 第一阶段(汇编):设置异常向量、初始化CPU模式、关闭看门狗、初始化时钟和内存控制器。这段代码通常放在start.S里,跟硬件绑得最紧。
- 第二阶段(C语言):初始化串口、检测内存、加载设备树、启动内核。这部分可移植性相对好一些。
- 第三阶段(内核引导):跳转到内核入口,把控制权交给操作系统。
核心要点:第一阶段代码必须用汇编写,因为这时候C运行环境还没准备好。我见过有人想偷懒,结果堆栈指针都没设对,系统直接跑飞。
为什么会这样?因为CPU上电时,内存控制器还没初始化,堆栈根本用不了。你想想看,连局部变量都存不了,C语言怎么跑?
U-Boot源码结构解析
U-Boot的源码目录,我刚开始看的时候也觉得乱。但摸清规律后,其实很好找东西。
| 目录 | 作用 | 我常用的场景 |
|---|---|---|
| arch/ | CPU架构相关代码(arm、riscv、x86等) | 改start.S、lowlevel_init.S |
| board/ | 板级初始化代码 | 加新板子时,直接复制改 |
| drivers/ | 外设驱动(网卡、存储、串口等) | 调网卡驱动时最常翻 |
| dts/ | 设备树源文件 | 改引脚复用、内存映射 |
| include/configs/ | 板级配置头文件 | 裁剪功能、调环境变量 |
| cmd/ | 命令行命令实现 | 加自定义命令时参考 |
我个人习惯,拿到一个新板子,先看 board/ 和 include/configs/ 这两个目录。前者告诉你板子怎么初始化,后者告诉你功能怎么配。
小技巧:U-Boot的代码复用做得不错。如果你的板子跟某款开发板相似,直接拿它的board目录做模板,改起来快很多。我在项目中就经常拿 board/ti/am335x/ 做起点。
U-Boot配置与编译
配置U-Boot,现在主流是用 make menuconfig,跟Linux内核配置一样。但要注意,U-Boot还有一套传统的配置方式——通过头文件。
两种方式对比:
| 配置方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| menuconfig(Kconfig) | 图形化、直观、依赖自动处理 | 新板子需要先写Kconfig | 新项目、功能多 |
| 头文件配置(include/configs/) | 直接、灵活、老项目兼容 | 容易漏配、依赖手动管理 | 快速原型、老平台维护 |
编译步骤其实很简单:
# 1. 配置(以树莓派3为例)
make rpi_3_defconfig
# 2. 菜单配置(可选,用于裁剪)
make menuconfig
# 3. 编译
make -j4
# 4. 查看生成文件
ls u-boot.bin u-boot.img
注意:编译前一定要确认交叉编译器版本。我踩过坑——用gcc 9编译U-Boot 2018,结果网卡驱动死活不工作,后来发现是编译器优化导致的。建议用U-Boot官方推荐的编译器版本。
嗯,这里还要提一句:make -j 后面的数字别太大,我一般设成CPU核心数+1。设太大反而容易编译出错,尤其是老版本的U-Boot。
板级设备树修改
设备树(Device Tree)是U-Boot和内核之间的"翻译官"。它告诉内核:你的硬件长什么样,引脚怎么接,内存有多大。
修改设备树,最常见的就是调引脚复用和改内存映射。举个例子:
// 修改前:UART0引脚配置
&uart0 {
pinctrl-0 = &uart0_pins_a;
status = "okay";
};
// 修改后:换成UART1,并调整引脚
&uart1 {
pinctrl-0 = &uart1_pins_b;
status = "okay";
};
// 内存映射调整
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x20000000>; // 512MB
};
我曾经在一个项目里,因为设备树里漏配了SD卡的中断引脚,结果系统启动时死活检测不到存储设备。查了两天才发现,就是少了一行 interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;。
避坑指南:改设备树后,一定要用 dtc -I dts -O dtb -o output.dtb input.dts 编译检查语法。我习惯在Makefile里加一个 dtbs 目标,一键编译所有设备树。
U-Boot烧录与验证
烧录U-Boot,不同平台方法不一样。但核心思路就一个:把编译好的二进制文件写到启动介质里。
常见烧录方式:
- SD卡启动:用
dd命令写到特定扇区 - eMMC/NAND:通过fastboot或厂商工具烧写
- SPI Flash:用编程器或U-Boot自身命令烧写
- 网络启动:通过TFTP加载到内存再烧写
以SD卡为例:
# 烧录到SD卡(假设设备是/dev/sdb)
sudo dd if=u-boot.bin of=/dev/sdb bs=1k seek=1
# 验证(读回来对比)
sudo dd if=/dev/sdb of=dump.bin bs=1k count=512
md5sum u-boot.bin dump.bin
警告:dd 命令写错设备会直接毁掉你的硬盘。我建议每次烧录前都执行 lsblk 确认设备名,别问我怎么知道的...
验证环节,我一般做三件事:
- 串口输出检查:看U-Boot启动日志是否正常打印
- 命令行交互:按任意键进入U-Boot命令行,执行
version、bdinfo等命令 - 功能测试:测试网络、存储、内存等关键外设
下面这张图,概括了U-Boot从配置到验证的完整流程:
验证通过后,U-Boot就算移植成功了。但别高兴太早——这只是第一步。后面还有内核裁剪、驱动适配、文件系统定制等着你。不过那是后面章节的事,咱们先把Bootloader这块吃透。
总结一下:U-Boot移植的核心就三件事——配好板级配置、改对设备树、烧录到正确位置。每一步都有坑,但只要你按流程走,遇到问题先看串口日志,大部分问题都能定位。