第2章:启动流程剖析——从复位向量到main函数的完整路径
做Bootloader这么多年,我见过太多工程师一上来就扎进代码里,结果连芯片复位后第一条指令从哪里取都没搞清楚。说白了,启动流程就是嵌入式系统的"出生证明",你不把它吃透,后面出了问题根本无从下手。
这一章,咱们就把从复位向量到main函数的完整路径掰开揉碎了讲。我会结合自己踩过的坑,带你搞懂启动模式的选择、两阶段启动的设计思路,以及每一步到底发生了什么。
2.1 复位向量:一切从这里开始
芯片上电或复位后,CPU会去一个固定的地址取第一条指令。这个地址就是复位向量。不同的架构,复位向量的位置不一样:
| 架构 | 复位向量地址 | 说明 |
|---|---|---|
| ARM Cortex-M | 0x00000000(或映射地址) | 向量表首地址,前4字节存栈指针,后4字节存复位入口 |
| ARM Cortex-A | 0x00000000 / 0xFFFF0000 | 由异常向量基址寄存器决定,低端或高端向量模式 |
| RISC-V | 0x80000000(典型) | 由mtvec寄存器指定,可配置 |
| x86 | 0xFFFFFFF0 | 复位后进入实模式,从BIOS映射地址开始执行 |
我记得刚做第一个ARM项目时,芯片死活跑不起来。查了半天,发现是向量表放错了位置——我把复位向量写到了0x08000000,但芯片默认从0x00000000取指令。嗯,这种低级错误,犯过一次就再也不会忘了。
2.2 启动模式:NOR/NAND/eMMC/SD卡
芯片怎么知道从哪里启动?靠的是启动模式选择。一般通过几个硬件引脚的电平组合来决定。我遇到过最坑的一次,是产线上有一批板子焊错了启动电阻,结果所有板子都从SD卡启动,而SD卡里根本没烧程序……
常见的启动介质有这几种:
- NOR Flash:支持XIP(就地执行),CPU可以直接取指令运行,速度最快。适合存放第一阶段Bootloader。
- NAND Flash:容量大、成本低,但不能XIP。需要先把前几KB代码拷贝到SRAM或DDR里再执行。
- eMMC:内置控制器,接口简单,顺序读写快。现在手机和平板的主流选择。
- SD卡:可插拔,方便调试和量产。但速度慢,可靠性不如eMMC。
你想想看,如果芯片从NAND启动,它内部Boot ROM会先读NAND的前几页,把代码搬到内部SRAM里。这个搬运过程是有严格时序要求的——NAND的坏块管理、ECC校验,都得在Boot ROM里处理好。我见过一个项目,就是因为NAND的前4KB刚好落在坏块上,导致Bootloader死活加载不进来。
2.3 第一阶段启动(SPL / Boot ROM)
第一阶段启动,也叫SPL(Secondary Program Loader)或Boot ROM阶段。它的任务很简单:初始化最基本的硬件(时钟、DDR控制器、存储控制器),然后把第二阶段的代码加载到内存里。
为什么要有第一阶段?说白了,就是鸡生蛋蛋生鸡的问题。DDR内存需要初始化才能用,但初始化DDR的代码本身又需要内存来运行。所以第一阶段只能用芯片内部SRAM来跑,SRAM通常只有几十KB到几百KB。
第一阶段的核心流程:
- 设置栈指针(SP),建立C语言运行环境
- 初始化时钟和PLL,让CPU跑在目标频率
- 初始化DDR控制器(如果使用外部内存)
- 从启动介质读取第二阶段Bootloader到DDR
- 跳转到第二阶段入口地址
这里有个关键点:第一阶段代码必须足够小。我见过有人把整个文件系统驱动都塞进SPL里,结果SPL编译出来200多KB,内部SRAM根本放不下。后来我建议他只保留最基础的NAND驱动,其他功能全部放到第二阶段,问题就解决了。
2.4 第二阶段启动(U-Boot / Bootloader主体)
第二阶段启动,就是我们通常说的Bootloader主体,比如U-Boot、GRUB、Lk等。它运行在DDR里,有充足的内存空间,可以做更多事情:
- 初始化更多外设(网卡、USB、显示、文件系统)
- 加载操作系统内核(从eMMC、网络、USB等)
- 提供交互式命令行(方便调试和配置)
- 支持启动参数传递(设备树、内核命令行)
第二阶段启动的典型流程:
- 设置异常向量表,确保中断能正确处理
- 初始化BSS段(清零全局未初始化变量)
- 调用板级初始化函数(board_init)
- 初始化串口、定时器等基础外设
- 检测启动设备(SD卡、eMMC、网络等)
- 加载内核和设备树到内存
- 跳转到内核入口
我曾经调试过一个U-Boot启动卡死的问题。现象是串口打印到"Starting kernel..."就停了。我加了调试打印,发现是设备树地址传错了——U-Boot把设备树加载到了0x48000000,但内核期望它在0x49000000。这种地址不匹配的问题,在移植Bootloader时特别常见。
2.5 从复位到main函数的完整路径
咱们把整个流程串起来,画一张图就清楚了:
从这张图可以清楚看到,复位后CPU先取向量,然后根据硬件引脚判断启动介质,接着执行第一阶段(SPL/Boot ROM),再加载第二阶段,最后才进入我们熟悉的main函数。整个过程,快的芯片几十毫秒就能完成,慢的(比如从SD卡加载)可能要一两秒。
2.6 常见故障与排查思路
启动流程中容易出问题的环节,我列几个典型的:
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 复位后无任何输出 | 复位向量配置错误、时钟未起振、电源不稳 | 用示波器测复位引脚和时钟输出;检查电源时序 |
| SPL加载失败 | NAND坏块、启动介质时序不匹配、SRAM不足 | 检查NAND坏块标记;降低SPL大小;加调试打印 |
| 第二阶段卡死 | DDR初始化失败、设备树地址错误、驱动冲突 | 用JTAG单步调试;检查DDR配置寄存器;核对地址映射 |
| 跳转到内核后无响应 | 内核入口地址错误、参数传递格式不对 | 检查内核加载地址;确认设备树和命令行参数正确 |
我个人习惯,在调试启动流程时,会先在SPL和第二阶段各加一个GPIO翻转点。用示波器看GPIO波形,就能知道代码执行到了哪一步。这个方法比串口打印更可靠——因为串口驱动本身可能还没初始化好。
好了,启动流程的完整路径就讲到这里。从复位向量到main函数,每一步都有它的设计逻辑和潜在陷阱。你把这些搞清楚了,后面调试Bootloader故障时,就能快速定位问题出在哪个阶段,而不是像无头苍蝇一样乱试。