1. 启动流程基础:从按下电源键到内核启动的完整链路
各位同学,今天咱们来聊聊嵌入式系统里最基础、也最关键的一环——启动流程。说白了,就是按下电源键之后,芯片到操作系统之间到底发生了什么。我做了这么多年嵌入式,发现很多问题都出在启动阶段,搞懂这个链路,后面AB分区切换、多系统启动这些内容才能吃得透。
1.1 按下电源键的那一刻
你按下电源键,系统就开始干活了。但这时候CPU还处于“混沌”状态——寄存器全是随机值,内存还没初始化,外设也没通电。嗯,这里要注意,CPU的第一件事不是执行你的应用程序,而是去固定的物理地址取第一条指令。
对于ARM Cortex-A系列,复位后通常从0x00000000或0xFFFF0000开始执行。x86平台则是从0xFFFFFFF0(也就是所谓的Reset Vector)开始。我当年调试一块i.MX6的板子,发现上电后一直跑不起来,最后用示波器量CS信号,才发现是Boot ROM的映射地址被拉低了——这种坑,你光看芯片手册是看不出来的。
核心要点:CPU复位后,硬件逻辑强制PC指针指向一个固定的ROM地址,这个ROM里固化的是芯片厂商的启动代码(Boot ROM)。
1.2 Boot ROM:芯片出厂自带的“小管家”
Boot ROM是芯片出厂时写死的,你改不了。它的任务很简单:初始化最基本的硬件,然后根据启动引脚的电平状态,决定从哪个设备加载下一级代码。
举个例子,我手头这块STM32MP157开发板,Boot ROM会先检查BOOT0、BOOT1两个引脚的电平:
- 00:从Flash启动
- 01:从SD卡启动
- 10:从UART下载
- 11:从NAND启动
你想想看,如果Boot ROM本身出了问题,那整个系统就变砖了。所以芯片厂商对这块代码的测试极其严格。我个人习惯,在设计板子时会把启动引脚拉成可配置的,方便调试时切换启动介质。
1.3 BIOS vs UEFI:x86世界的两种“活法”
在x86平台上,Boot ROM之后接的是BIOS或UEFI。这两者本质上都是固件,但理念完全不同。
| 特性 | BIOS(Legacy) | UEFI |
|---|---|---|
| 启动方式 | 读取MBR(主引导记录) | 读取GPT分区表的EFI系统分区 |
| 磁盘支持 | 最大2TB,最多4个主分区 | 支持超大磁盘,128个分区 |
| 引导程序 | 直接执行MBR中的代码 | 加载.efi文件(如grubx64.efi) |
| 用户界面 | 文本菜单,键盘操作 | 支持鼠标、图形界面、网络 |
| 安全性 | 无签名校验 | 支持Secure Boot |
我曾经帮客户调试一台工控机,BIOS模式下能正常启动,换成UEFI模式就黑屏。查了两天才发现,是EFI系统分区没有正确格式化——UEFI要求分区必须是FAT32格式,且文件系统标签得是"EFI"。这种细节,文档里写得很清楚,但真遇到问题你才会记住。
我的建议:新项目尽量用UEFI。虽然BIOS兼容性好,但UEFI的模块化设计、驱动加载机制、以及安全启动功能,在工业场景下优势明显。尤其是做AB分区切换时,UEFI的Boot Manager能帮你省不少事。
1.4 Bootloader:承上启下的“二传手”
不管是BIOS还是UEFI,最终都会把控制权交给Bootloader。在嵌入式Linux世界里,最常用的就是U-Boot(全称Das U-Boot)。
Bootloader要干的事其实很纯粹:
- 初始化硬件——DDR内存、串口、网卡、存储控制器
- 加载内核镜像——从Flash、SD卡、网络等介质读取zImage或Image
- 传递启动参数——告诉内核内存布局、根文件系统位置、控制台参数
- 跳转到内核入口——把CPU控制权交给内核
这里有个关键点:Bootloader本身也是程序,它需要被加载到内存里执行。所以Boot ROM会先把Bootloader的前几KB(通常是SPL,Secondary Program Loader)加载到SRAM中,由SPL初始化DDR,再把完整的U-Boot拷贝到DDR中运行。
避坑指南:我曾经遇到过SPL和U-Boot版本不匹配导致启动卡死的情况。SPL是U-Boot的“迷你版”,两者必须来自同一套源码编译。如果你自己移植U-Boot,记得检查SPL的链接地址和DDR初始化时序,这两个地方最容易出问题。
1.5 从Bootloader到内核:最后一步
U-Boot加载内核时,会做几件关键的事:
- 解压内核镜像(如果是压缩过的zImage)
- 设置设备树(Device Tree Blob,DTB)的地址
- 填充内核启动参数(ATAG或bootargs)
- 跳转到内核入口地址
我习惯在U-Boot命令行里手动验证这些参数:
# 查看当前启动参数
printenv bootargs
# 手动加载内核
tftp 0x42000000 zImage
tftp 0x43000000 myboard.dtb
bootz 0x42000000 - 0x43000000
你想想看,如果DTB地址给错了,内核根本不知道你的板子上有哪些外设,直接就panic了。我在调试一块新板子时,经常先用bootz手动启动,确认没问题了再写进环境变量。
1.6 完整链路回顾
来,咱们把整个流程串起来:
- 上电复位 → CPU从Reset Vector取指令
- Boot ROM执行 → 初始化时钟、检测启动介质
- 加载SPL → 初始化DDR,加载完整U-Boot
- U-Boot运行 → 初始化外设,加载内核和设备树
- 内核启动 → 解压、初始化、挂载根文件系统
这个链路里,任何一个环节出问题,系统都起不来。我见过最离谱的案例,是某款芯片的Boot ROM有bug,在低温环境下读取SD卡会出错——最后只能靠修改硬件启动引脚,强制从NAND启动来绕过。
一句话总结:启动流程就是“接力跑”——Boot ROM把棒交给SPL,SPL交给U-Boot,U-Boot交给内核。每一棒都不能掉。
好了,这一章的内容就到这里。下一章咱们会深入U-Boot的源码结构,看看它到底是怎么把内核“请”进内存的。到时候我会拿一份实际的U-Boot代码,一行一行带你看。