3、CPU上电与复位向量:CPU复位后的第一条指令、复位向量地址、异常向量表

芯片上电那一刻,CPU到底在干什么?

很多人觉得这很简单——不就是从某个地址开始执行代码吗?
对,但也不全对。我做过好几款不同架构的板子,每次上电流程里都有坑。
今天咱们就把这个「第一条指令」的来龙去脉彻底讲清楚。

3.1 复位后,CPU的第一口气

CPU上电后,内部逻辑会先做几件事:

  • 等待电源稳定(Power Good信号)
  • 内部寄存器复位到默认值
  • 程序计数器(PC)加载复位向量地址
  • 从该地址取第一条指令并执行

说白了,CPU就是个「无情的取指-执行机器」。
复位信号一撤除,它立刻从PC指向的地方开始干活。

关键点: 复位向量地址是硬件固化的,不是软件能改的。
不同架构的复位地址完全不同,选型时一定要先查手册。

3.2 复位向量地址:各架构的差异

我整理了一张表,把主流架构的复位向量列出来:

架构 复位向量地址 说明
ARM Cortex-M 0x00000000(向量表基址) 栈顶指针在0x0000,复位向量在0x0004
ARM Cortex-A 0x00000000 或 0xFFFF0000 由硬件配置引脚决定,低端或高端向量
x86 0xFFFFFFF0 靠近4GB地址空间顶端,跳转到BIOS
RISC-V 0x00000000 或 0x80000000 由mtvec寄存器决定,可配置
PowerPC 0x00000100 复位异常入口,偏移0x100

你看,ARM Cortex-M 的复位向量其实是个「向量表」的概念。
地址0x00000000存的是栈顶指针,0x00000004才是复位后要执行的函数地址。
我第一次用STM32时,debug发现PC跳到了一个奇怪的值,查了半天才发现是向量表没对齐——嗯,这个坑我踩过。

3.3 异常向量表:CPU的紧急联络簿

异常向量表,你可以把它想象成CPU的「紧急联络簿」。

当发生复位、中断、硬件错误、系统调用等事件时,CPU会暂停当前任务,跳转到向量表中对应的入口地址去处理。

以ARM Cortex-M为例,向量表长这样:

; 向量表布局(部分)
0x00000000: 栈顶指针初始值
0x00000004: 复位向量      — 上电后第一条指令
0x00000008: NMI向量       — 不可屏蔽中断
0x0000000C: HardFault向量 — 硬件错误
0x00000010: MemManage向量 — 内存管理错误
0x00000014: BusFault向量  — 总线错误
0x00000018: UsageFault向量— 用法错误
...
0x0000002C: SVCall向量    — 系统服务调用
0x00000030: DebugMonitor  — 调试监视器
0x00000034: PendSV向量    — 可挂起系统服务
0x00000038: SysTick向量   — 系统滴答定时器

每个向量占4字节,存的是对应处理函数的地址。
CPU复位后,自动从0x00000004取出地址,跳过去执行。

我的习惯: 在Bootloader里,我会把向量表重定位到RAM中。
这样运行时可以动态修改中断处理函数,方便做固件升级和热补丁。
但要注意——重定位前必须关中断,否则会出大问题。

3.4 复位流程的完整链路

咱们把整个流程串起来,画一张图:

CPU上电复位与向量表执行流程 CPU上电复位与向量表执行流程 ① 芯片上电 ② 复位信号释放 ③ PC ← 复位向量地址 ④ 从复位向量取指令 ⑤ 执行Bootloader入口 ⚠️ 注意 复位向量地址由硬件决定 不同架构差异很大 选型时务必确认 Cortex-M需注意向量表对齐 异常向量表结构 0x00: 栈顶指针 0x04: 复位向量 ← 入口 0x08: NMI 0x0C: HardFault 0x10: MemManage 0x14: BusFault 0x18: UsageFault ...

这张图把整个链路串起来了。
从芯片上电到执行Bootloader的第一条指令,中间就这5步。
但每一步都可能出问题——我见过有人把复位向量地址写错,导致芯片一直跑飞。

3.5 避坑指南:我踩过的几个雷

做嵌入式这么多年,复位这块我栽过不少跟头。分享几个典型的:

  • 向量表对齐问题: 有一次我把向量表放在0x08002000,但忘了做512字节对齐,结果Cortex-M3直接HardFault。查了两天才发现——嗯,手册上写得清清楚楚,我没仔细看。
  • 栈指针初始化: 复位向量表第一个字是栈顶指针。如果这个值指向了不存在的RAM区域,第一条指令都执行不了。我建议你在链接脚本里显式定义栈顶地址。
  • 中断使能过早: 在Bootloader阶段,外设还没初始化完,千万别开全局中断。我曾经在设置完向量表后立刻开中断,结果一个悬而未决的中断直接让系统崩溃。
  • 看门狗干扰: 有些芯片上电后看门狗默认是开启的。如果Bootloader初始化时间太长,看门狗会复位芯片,形成死循环。我习惯在复位后第一时间关看门狗。

重要警告: 千万不要在中断服务函数里做耗时操作!
尤其是HardFault处理函数——如果你在里面打印日志、操作外设,很可能再次触发异常,形成嵌套死锁。
我见过一个产品,HardFault里调了printf,结果printf又触发HardFault,直接死机。

3.6 实战:如何验证复位向量是否正确

你写完了Bootloader,怎么确认复位向量是对的?
我一般用这三种方法:

  1. 读map文件: 编译后查看.map文件,确认复位向量地址和函数入口是否匹配。
  2. 用调试器单步: 连接JTAG/SWD,复位后看PC寄存器的值,是不是指向了你预期的地址。
  3. 反汇编验证: 用objdump反汇编,找到复位向量地址处的指令,确认是跳转到Bootloader入口。

举个例子,ARM GCC编译后,你可以这样查:

# 查看向量表
arm-none-eabi-objdump -s -j .vectors your_bootloader.elf

# 输出示例(部分)
# 0x00000000: 20010000 08000145 08000149 0800014d
#            ^栈顶指针 ^复位向量 ^NMI      ^HardFault

# 复位向量 0x08000145 的最低位为1,表示Thumb模式
# 实际入口地址 = 0x08000144

你看,复位向量的最低位必须是1(ARM Thumb模式),否则CPU会进入错误状态。
这个细节,我第一次做移植时完全不知道,debug了整整一个下午。

3.7 小结

CPU上电复位,说白了就是硬件把PC指向一个固定地址,然后开始执行。
但背后的向量表机制、架构差异、对齐要求,每一个细节都可能让你翻车。

我个人习惯是:拿到一个新芯片,第一件事就是查复位向量地址和向量表布局。
把这个搞清楚了,后面的Bootloader开发才能稳得住。