第2章 芯片上电瞬间:CPU复位向量、PC指针初始化、第一条指令执行

芯片上电那一刻,到底发生了什么?

很多人觉得,不就是按下电源开关嘛。但在我眼里,这其实是整个系统最脆弱的时刻。电压还没稳,时钟还在抖,复位信号刚释放——CPU却要在这种“混沌”状态下找到第一条指令,然后准确无误地执行下去。

这一章,我们就来拆解这个“从0到1”的过程。

2.1 复位向量的硬件约定

CPU怎么知道上电后该从哪里开始跑?

答案很简单:硬件写死了。芯片设计时,就把第一条指令的地址固化在逻辑里。这个地址,就叫复位向量。

架构 复位向量地址 说明
ARM Cortex-M 0x00000000 向量表首地址,存放栈指针和复位入口
ARM Cortex-A 0x00000000 或 0xFFFF0000 由硬件配置引脚决定
RISC-V 0x00000000 或 0x80000000 由mtvec寄存器复位值决定
x86 0xFFFFFFF0 传统复位向量,靠近4GB顶端

我个人习惯把复位向量理解成“CPU的出厂设置”。你没法改它,只能顺着它走。芯片一上电,PC指针就被硬件强制赋值为这个地址。

关键点:复位向量指向的地址,通常不是RAM,而是非易失性存储器——要么是片内ROM(BootROM),要么是Flash。因为此时DDR还没初始化,RAM还不能用。

2.2 PC指针的初始化过程

PC指针,全称Program Counter,就是CPU的“当前执行位置”。上电瞬间,它的值怎么来的?

我举个例子。以ARM Cortex-M3为例:

// 硬件自动完成,不需要软件干预
1. 从地址 0x00000000 读取 4 字节 → 存入 MSP(主栈指针)
2. 从地址 0x00000004 读取 4 字节 → 存入 PC(程序计数器)
3. CPU 开始从 PC 指向的地址执行

你看,Cortex-M的复位过程其实是两步取指。第一步拿栈指针,第二步拿入口地址。这个设计很巧妙——它保证了在第一条指令执行前,栈就已经可用了。

但Cortex-A就不一样。它更“裸”:

// Cortex-A 复位流程
1. 硬件强制 PC = 0x00000000(或 0xFFFF0000)
2. CPU 进入 SVC(超级用户)模式
3. 关闭中断(IRQ/FIQ)
4. 开始执行地址 0x00000000 处的指令

没有自动读向量表,没有自动设栈。一切都要靠第一条指令自己搞定。这就是为什么Cortex-A的启动代码里,第一件事往往是设置栈指针。

经验之谈:我在调试一块新板子时,经常用JTAG直接读PC寄存器的复位值。如果读出来是0xFFFFFFFF或者全0,说明芯片根本没跑起来——先查电源和复位电路,别急着看代码。

2.3 第一条指令的“生存环境”

第一条指令执行时,芯片处于什么状态?

说白了,就是“三无”状态:

  • 无时钟:虽然内部RC振荡器可能已经起振,但PLL还没锁相,主频很低
  • 无栈:SP指针要么是默认值,要么还没设(取决于架构)
  • 无外设:DDR、Flash控制器、UART都还没初始化

你想想看,在这种环境下,第一条指令能做什么?

它只能做三件事:

  1. 设置栈指针——不然没法调函数
  2. 关中断——防止意外打断初始化流程
  3. 跳转到初始化代码——通常是C语言的入口

我见过有人在这时候开中断,结果芯片直接跑飞。嗯,调试了一整天才找到原因。

2.4 从复位到第一条指令的完整流程

下面这张图,是我自己画的上电启动流程。它把硬件和软件的边界画得很清楚。

芯片上电启动流程 上电复位 硬件初始化 PC = 复位向量 取第一条指令 执行指令 硬件阶段:芯片内部逻辑自动完成,不需要软件参与 软件阶段:从第一条指令开始,由程序员编写的代码控制

注意看,前三个步骤(上电复位、硬件初始化、PC赋值)全是硬件行为。从“取第一条指令”开始,才进入软件领域。这个分界线,就是BootROM的起点。

2.5 实际芯片的复位行为差异

不同芯片厂商,对复位向量的处理方式也不一样。我列几个常见的:

芯片 复位向量 第一条指令位置 备注
STM32F4 0x00000000 Flash起始地址 通过Boot引脚可映射到系统存储器
i.MX6UL 0x00000000 BootROM 片内ROM,约96KB
Allwinner H3 0x00000000 BROM 片内BootROM,约32KB
BCM2837 0x00000000 GPU固件 ARM核由GPU唤醒,比较特殊

这里有个坑,我提醒一下。有些芯片的复位向量指向的是BootROM,但BootROM里并不是你的代码——而是芯片厂预置的一段启动程序。它负责检测启动介质(SD卡、NAND、SPI Flash等),然后把你的代码加载到内部SRAM里执行。

注意:如果你的板子焊了新芯片,上电后什么反应都没有,别急着怀疑代码。先确认复位引脚电平是否正确。我遇到过好几次,复位引脚被电容拉低,芯片一直处于复位状态,PC根本不会动。

2.6 第一条指令的“真面目”

说了这么多,第一条指令到底长什么样?

以ARM为例,复位向量处存放的通常是一条跳转指令:

; ARM 汇编,复位向量处的典型代码
Reset_Handler:
    LDR     SP, =_estack      ; 设置栈指针
    BL      SystemInit        ; 初始化系统时钟
    BL      _start            ; 跳转到C运行时入口

你看,这三条指令就完成了我们前面说的三件事:设栈、关中断(SystemInit里通常会关)、跳转。

但如果是RISC-V,写法略有不同:

# RISC-V 汇编,复位向量处的典型代码
.section .text.init
.globl _start
_start:
    # 设置栈指针
    la      sp, _stack_top
    # 清除BSS段
    la      a0, _bss_start
    la      a1, _bss_end
    call    clear_bss
    # 跳转到main
    call    main

我个人更喜欢RISC-V的写法,因为它把BSS清零也放在了启动阶段。ARM的启动代码里,这一步通常由C运行时库完成,但如果你不用库,就得自己写。

小技巧:调试启动代码时,我习惯在第一条指令处加个死循环,比如“b .”。然后用JTAG单步执行,确认PC确实走到了这里。如果连这一步都到不了,说明复位向量配置有问题。

2.7 避坑指南:上电瞬间的常见问题

做嵌入式这么多年,我在上电启动这块踩过的坑,可以写个小册子了。挑几个典型的说说:

  • 复位时间不够:电源芯片的复位输出延时太短,CPU在电压还没稳定时就释放了复位。结果就是PC乱跳。我曾经用示波器抓过,复位信号只有1ms,而电源纹波要5ms才能稳定。后来换了复位芯片,问题解决。
  • Boot引脚电平不对:有些芯片通过Boot引脚选择启动介质。如果引脚悬空或者电平不对,CPU会从错误的地址取指令。我遇到过一块板子,Boot引脚虚焊,芯片一直从Flash启动,但Flash里是空的。
  • 向量表地址不对:特别是Cortex-M系列,如果向量表首地址不是0x00000000,需要设置VTOR寄存器。我见过有人把代码下载到0x08000000(Flash),但忘了改VTOR,结果CPU从0x00000000读到的全是0xFF,直接跑飞。

嗯,这些坑说大不大,但排查起来真的很费时间。所以我的建议是:新板子第一次上电,先别急着跑完整系统。写个最简单的点灯程序,确认CPU能正常取指执行,再往上加东西。

芯片上电瞬间,从复位向量到第一条指令,整个过程不过几微秒。但这几微秒,决定了整个系统能不能正常启动。把这一步吃透了,后面的BootROM、U-Boot,才能顺理成章地跑起来。