2. 硬件初始化与启动流程:从CPU上电复位到Bootloader入口
各位做嵌入式的朋友,今天我们来聊聊芯片上电后、Bootloader跑起来之前,那一段「黑暗中的舞蹈」。说白了,就是CPU从复位向量开始,到第一条有效指令执行之间,硬件到底干了些什么。
我记得刚入行那会儿,总觉得Bootloader就是启动的起点。后来被一个板子折腾了三天——上电后串口死活没输出,查到最后才发现是复位向量指向的地址根本没初始化。嗯,从那以后我再也不敢跳过硬件初始化这部分了。
2.1 上电复位:CPU的第一口气
按下电源键那一刻,芯片内部发生了什么?
- 电源稳定:内部POR(Power-On Reset)电路检测电压,达到阈值后释放复位信号
- 时钟起振:内部RC振荡器开始工作,产生基础时钟(通常是几MHz到几十MHz)
- 复位向量读取:CPU从预设地址(比如0x00000000或0xFFFF0000)读取复位向量
- 模式选择:根据硬件引脚电平,决定启动介质(NAND、eMMC、SPI Flash等)
关键点:复位向量里存的不是代码,而是地址。CPU拿到这个地址后,才会跳过去执行真正的启动代码。我见过有人把复位向量直接写成代码入口,结果跳转后PC跑飞——因为向量表格式不对。
2.2 关键寄存器:谁在掌控初始状态?
上电瞬间,CPU内部寄存器有默认值。这些默认值决定了启动流程的走向。我个人习惯把寄存器分成三类:
| 寄存器类别 | 典型寄存器 | 复位值 | 作用 |
|---|---|---|---|
| 控制类 | PC(程序计数器) | 复位向量地址 | 决定第一条指令位置 |
| 状态类 | CPSR/SPSR | SVC模式,IRQ/FIQ关闭 | CPU工作模式与中断使能 |
| 配置类 | SCB->VTOR(向量表偏移) | 0x00000000 | 中断向量表基址 |
| 内存类 | MMU页表基址 | 0(MMU关闭) | 地址映射是否生效 |
你想想看,PC指向复位向量,但此时MMU是关闭的——也就是说,CPU直接使用物理地址。这也是为什么Bootloader早期代码必须放在物理地址能访问到的位置。
避坑指南:我曾经在一个Cortex-A7平台上踩过坑——芯片支持从多个介质启动,但复位后默认的启动介质引脚电平被板级上拉电阻拉错了,导致CPU一直从空的SPI Flash读数据,串口毫无反应。检查复位引脚电平,是调试启动问题的第一步。
2.3 内存布局:启动阶段的地址空间
上电后,内存不是全部可用的。通常分为几个区域:
- 内部SRAM:芯片内置,上电即用,但容量小(几十KB到几百KB)
- Boot ROM:厂商固化的一段代码,不可修改,负责加载Bootloader
- 外部DDR:需要初始化控制器后才能使用,Bootloader早期代码不能依赖它
我画了一张图,帮你理清这个阶段的地址空间布局:
实用技巧:我建议你在设计板子时,把Boot ROM和内部SRAM的地址范围记在便签上贴到工位。调试启动问题时,先确认PC是否在合法地址范围内——如果PC跑到了DDR区域但DDR还没初始化,那基本就是死路一条。
2.4 启动流程:从复位到Bootloader入口
整个流程可以概括为四个阶段:
- 硬件自检:POR完成后,芯片内部做基本自检(时钟、电压、温度)
- 启动介质选择:根据BOOT引脚电平,决定从哪个设备加载代码
- Boot ROM执行:厂商代码运行,从选定介质读取Bootloader到内部SRAM
- 跳转到Bootloader:设置堆栈指针,跳转到SRAM中的Bootloader入口
这里有个细节很多人忽略——Boot ROM在跳转前会做一些清理工作,比如关闭看门狗、设置异常向量表。但不同厂商的做法不一样。我遇到过某款芯片的Boot ROM居然没关看门狗,导致Bootloader跑着跑着被复位了。解决方案是在Bootloader第一条指令就关掉它。
2.5 常见启动介质与加载顺序
| 启动介质 | 读取速度 | 是否需要初始化 | 典型场景 |
|---|---|---|---|
| NAND Flash | 慢(需ECC) | 是(控制器+时序) | 大容量存储 |
| eMMC | 快 | 是(MMC协议) | 主流嵌入式 |
| SPI NOR Flash | 中 | 是(SPI控制器) | 小容量、快速启动 |
| SD卡 | 中 | 是(SD协议) | 开发调试 |
| USB/串口 | 慢 | 否(Boot ROM支持) | 工厂烧录 |
核心原则:Boot ROM只负责把Bootloader从介质搬到SRAM,它不负责初始化DDR。DDR初始化是Bootloader第一阶段(SPL)的任务。这也是为什么Bootloader必须分阶段——第一阶段小到能塞进SRAM,第二阶段才大到需要DDR。
2.6 一个实际例子:Cortex-M系列启动
拿Cortex-M系列来说,启动流程更简单直接:
- 复位后,CPU从0x00000000读取栈顶指针(MSP)
- 从0x00000004读取复位向量(即第一条指令地址)
- 然后直接跳转执行
这里有个坑——如果你把代码烧录到0x08000000(内部Flash),但芯片默认从0x00000000启动,怎么办?
答案是:芯片内部有地址映射机制,会把Flash映射到0x00000000。但如果你用了外部Flash,就得手动配置VTOR寄存器来重定向向量表。我调试一个STM32H7项目时就遇到过——外部QSPI Flash启动,忘了改VTOR,中断全跑飞了。
/* Cortex-M 向量表示例 */
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
(void*)&_estack, /* 0x00000000: 栈顶 */
Reset_Handler, /* 0x00000004: 复位入口 */
NMI_Handler, /* 0x00000008: NMI */
HardFault_Handler, /* 0x0000000C: 硬错误 */
/* ... 其他中断向量 ... */
};
我曾经踩过的坑:在Cortex-A系列上,复位后CPU处于SVC模式,但中断是关闭的。我写Bootloader时忘了开IRQ,结果外设中断一直不响应,查了两天才发现是CPSR的I位没清。记住:复位后默认关中断,需要显式使能。
2.7 小结:启动流程的关键检查点
如果你在调试启动问题,按这个顺序检查:
- 电源:各电压域是否稳定?POR是否释放?
- 时钟:主时钟是否起振?PLL是否锁定?
- 复位向量:PC是否指向正确地址?向量表格式是否正确?
- 启动介质:BOOT引脚电平是否匹配?介质是否可读?
- SRAM可用性:Bootloader是否被正确加载到SRAM?