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(只读,厂商固化) 地址范围:0x00000000 - 0x0000FFFF(64KB) 内部SRAM(可读写,上电可用) 地址范围:0x20000000 - 0x20007FFF(32KB) 外设寄存器空间(MMIO) 地址范围:0x40000000 - 0x400FFFFF(UART、GPIO、Timer等) 外部DDR(需初始化控制器) 地址范围:0x80000000 - 0x8FFFFFFF(256MB,Bootloader阶段才可用) 说明: Bootloader早期代码只能使用Boot ROM和内部SRAM。外部DDR需要先初始化内存控制器, 这部分工作通常由Bootloader的第一阶段(SPL/MLO)完成。

实用技巧:我建议你在设计板子时,把Boot ROM和内部SRAM的地址范围记在便签上贴到工位。调试启动问题时,先确认PC是否在合法地址范围内——如果PC跑到了DDR区域但DDR还没初始化,那基本就是死路一条。

2.4 启动流程:从复位到Bootloader入口

整个流程可以概括为四个阶段:

  1. 硬件自检:POR完成后,芯片内部做基本自检(时钟、电压、温度)
  2. 启动介质选择:根据BOOT引脚电平,决定从哪个设备加载代码
  3. Boot ROM执行:厂商代码运行,从选定介质读取Bootloader到内部SRAM
  4. 跳转到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?