第二章 嵌入式系统启动流程:从复位向量到操作系统启动的完整链路分析
做嵌入式开发这么多年,我见过不少工程师把精力全花在应用层代码上,一碰到系统起不来就抓瞎。说白了,启动流程就是嵌入式系统的「生命线」——从芯片上电那一刻起,到操作系统跑起来,中间每一步都有讲究。这一章,我就带你把这根线从头捋一遍。
2.1 复位向量:一切从这里开始
芯片上电后,CPU 会去一个固定的地址取第一条指令。这个地址就叫复位向量。不同架构的复位向量地址不一样:
| 架构 | 复位向量地址 | 说明 |
|---|---|---|
| ARM Cortex-A | 0x00000000 或 0xFFFF0000 | 由异常向量基址寄存器决定 |
| ARM7/9 | 0x00000000 | 固定地址,不可重映射 |
| x86 | 0xFFFFFFF0 | 靠近 4GB 地址空间顶端 |
| RISC-V | 0x80000000 或 0x00000000 | 由硬件配置决定 |
这里有个坑,我踩过。有一次做 Cortex-A7 的板子,复位向量默认指向 0x00000000,但我的 BootROM 在 0x00000000 放的是跳转指令,而异常向量表却在 0xFFFF0000。结果一触发中断就飞了。嗯,后来我养成了习惯——启动初期第一件事就是检查异常向量基址寄存器的值。
2.2 启动 ROM 阶段:芯片出厂自带的「小管家」
大部分现代 SoC 内部都有一小块 Mask ROM,出厂就固化好了代码。这段代码叫 BootROM,它干的事很固定:
- 初始化最基本的硬件——时钟、SRAM、看门狗
- 检测启动介质——从哪启动?NAND?eMMC?SD 卡?SPI Flash?
- 加载下一级代码——把 Bootloader 的第一阶段从存储介质搬到 SRAM 里
- 跳转执行——把控制权交给 Bootloader
关键点:BootROM 是只读的,你改不了它。所以它的代码必须足够小、足够稳定。我见过有些芯片的 BootROM 只支持 4 种启动介质,选错了板子就起不来。选型时一定要看芯片的 BootROM 支持列表。
2.3 Bootloader 第一阶段:汇编里的「黄金三秒」
Bootloader 通常分两个阶段。第一阶段用汇编写,因为这时候 C 环境还没准备好。它要干的事,说白了就三件:
- 关中断——防止在初始化过程中被异常打断
- 初始化 CPU 模式——设置栈指针、切换到 SVC 模式
- 初始化关键硬件——时钟、DDR 控制器、串口
我习惯在第一阶段末尾加一段「自检灯」代码——点亮一个 LED,表示第一阶段跑通了。调试的时候特别管用,能快速定位问题出在前半段还是后半段。
@ U-Boot 第一阶段典型代码片段 (arch/arm/cpu/armv7/start.S)
reset:
/* 设置 CPU 为 SVC 模式 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关中断 */
cpsid if
/* 初始化栈指针 */
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
/* 跳转到 C 语言入口 */
bl board_init_f
我的经验:第一阶段代码越短越好。我见过有人把 DDR 初始化写得巨复杂,结果第一阶段代码体积超标,BootROM 的 SRAM 装不下。记住,第一阶段只做「不得不做」的事,其他都扔给第二阶段。
2.4 Bootloader 第二阶段:C 语言的主战场
第二阶段用 C 写,这时候 DDR 已经能用了,代码可以跑在更大的内存空间里。它要干的事就多了:
- 初始化更多外设——网卡、USB、存储控制器
- 建立内存映射——MMU 初始化、TLB 表设置
- 加载内核镜像——从 Flash、网络或 SD 卡读取内核
- 准备启动参数——设置 ATAG 或设备树
- 跳转到内核——清理环境,跳转到内核入口
你想想看,第二阶段其实就是一个「小操作系统」——它有设备驱动、文件系统支持、网络协议栈。U-Boot 的第二阶段代码量通常在几十万行,比很多 RTOS 还大。
2.5 内核启动:从压缩镜像到进程调度
Bootloader 跳转到内核后,内核自己还要走一段路才能跑起来。这段路大致分几步:
- 解压缩——如果内核是压缩的(zImage),先自解压
- 架构初始化——设置页表、中断向量、CPU 特性检测
- 内核子系统初始化——调度器、内存管理、文件系统、网络栈
- 驱动初始化——按设备树或模块顺序加载驱动
- 挂载根文件系统——从指定设备挂载 /
- 启动 init 进程——用户空间的第一个进程
注意:内核启动过程中,如果根文件系统挂载失败,系统会直接 panic。我曾经遇到过一个问题——Bootloader 传的设备树里根设备路径写错了,内核死活找不到根分区。查了两天才发现是设备树里一个字符串多了一个斜杠。这种问题,printk 都救不了你。
2.6 完整启动链路图
下面这张图把整个流程串起来了。我建议你把它打印出来贴在工位上——调试启动问题的时候,对着图一步步排查,比瞎猜快得多。
2.7 常见启动失败场景与排查思路
启动出问题,别慌。我总结了一套排查思路,按阶段来:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电后无任何输出 | 复位电路问题、电源不稳、晶振不起振 | 示波器测复位引脚、电源纹波、晶振波形 |
| BootROM 阶段卡死 | 启动介质选择错误、启动介质损坏 | 检查启动引脚电平、换一片 Flash 试试 |
| Bootloader 第一阶段崩溃 | DDR 初始化参数错误、栈溢出 | 用 JTAG 单步调试、检查 DDR 时序配置 |
| Bootloader 第二阶段无法加载内核 | 存储驱动问题、分区表错误 | 在 Bootloader 命令行手动读 Flash 验证 |
| 内核启动后 panic | 设备树错误、根文件系统缺失 | 开启 early printk、检查内核启动日志 |
我的习惯:每次做新板子,我都会在 Bootloader 里加一个「启动计时」功能——记录每个阶段花了多少毫秒。这样不仅能定位问题,还能优化启动速度。有一次我发现 BootROM 阶段花了 3 秒,查了半天发现是 Flash 的读取模式没配好,改成 Quad SPI 后直接降到 0.3 秒。
2.8 小结
嵌入式系统的启动链路,说白了就是一条「接力棒」——从硬件到固件,从固件到 Bootloader,从 Bootloader 到内核,最后到用户空间。每一棒交接的时候,都要保证状态干净、参数正确。我见过太多人只盯着内核那一端,忽略了前面的环节。其实大部分启动问题,都出在 BootROM 和 Bootloader 的交接处。