处理器复位与初始化:从按下复位键到第一行C代码

各位同学,今天我们来聊聊嵌入式启动流程里最基础、也最容易踩坑的一环——处理器复位与初始化。很多初学者觉得这步就是「上电,跑起来」,其实没那么简单。我当年第一次做STM32项目时,就因为堆栈指针没配好,程序死活跑不起来,折腾了两天才发现是启动文件里SP初始值写错了……嗯,从那以后我再也不敢轻视这步了。

复位向量:处理器醒来后第一眼看到的东西

处理器上电或复位后,CPU会去一个固定的地址取第一条指令。这个地址,就是复位向量。说白了,它告诉CPU:「嘿,你该从这里开始干活了!」

以ARM Cortex-M系列为例,复位向量位于中断向量表的开头。向量表的前两个word分别是:

  • 初始堆栈指针(MSP):地址偏移0x0000
  • 复位向量(Reset_Handler):地址偏移0x0004

CPU上电后,硬件自动完成以下动作:

  1. 从0x00000000读取SP初始值
  2. 从0x00000004读取PC初始值
  3. 跳转到Reset_Handler执行

关键点:复位向量必须指向有效的代码地址,且该地址处的指令必须是合法的ARM/Thumb指令。如果向量表配错了,CPU会直接跑飞——我见过有人把0x00000004写成了数据地址,结果芯片一上电就进HardFault。

CPU模式切换:SVC、IRQ、FIQ……你到底该用哪个?

ARM处理器有好几种工作模式,每种模式都有自己的用途和寄存器组。你想想看,如果所有代码都在同一个模式下跑,中断来了怎么办?堆栈乱了怎么办?

常见的CPU模式包括:

模式 用途 可访问的寄存器
SVC(超级用户模式) 操作系统内核、异常处理 所有寄存器,包括CPSR
IRQ(普通中断模式) 普通中断服务 有自己的SP和LR
FIQ(快速中断模式) 高速中断处理 有额外的banked寄存器
User(用户模式) 普通应用程序 受限,不能直接改CPSR
System(系统模式) 与SVC类似,但使用User模式的寄存器组 所有寄存器

复位后,CPU默认进入SVC模式。为什么?因为初始化阶段需要最高权限——你要配置时钟、初始化内存、设置中断控制器,这些操作在User模式下根本做不了。

我的习惯:在Reset_Handler里,我会先确认当前模式确实是SVC。如果发现不是,就手动切换。虽然大多数芯片复位后默认就是SVC,但有些老平台或自定义硬件可能会有差异——别问我怎么知道的……

堆栈指针(SP)初始化:别让局部变量无处安放

堆栈指针SP,是嵌入式开发里最容易被忽视、却又最致命的东西。没有正确的SP,你的局部变量、函数调用、中断现场保存全都会崩。

SP初始化分两步:

  1. 主堆栈指针(MSP):由硬件从向量表0x0000处加载,用于SVC模式和异常处理
  2. 进程堆栈指针(PSP):由软件在初始化时设置,用于User模式

在Cortex-M上,复位后默认使用MSP。你需要在启动代码里确保:

; 启动文件中的典型SP初始化
__initial_sp   EQU  0x20010000   ; 假设RAM从0x20000000开始,大小64KB

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

; 向量表
                DCD     __initial_sp          ; 0x0000: 初始SP
                DCD     Reset_Handler         ; 0x0004: 复位向量

这里有个坑:SP必须对齐到8字节边界。ARM架构要求SP在函数调用时保持8字节对齐,否则某些指令(如LDRD、STRD)会触发异常。我曾在一次项目里因为SP没对齐,导致浮点运算结果全错——查了三天才发现是堆栈地址写成了0x20010004。

避坑指南:我曾经在移植uC/OS-II时,忘了给每个任务分配独立的堆栈空间。结果任务切换时,PSP指向了未初始化的内存区域,系统跑几分钟就死机。记住:每个任务、每个中断模式,都要有自己的堆栈空间。

中断向量表重定位:把向量表搬到RAM里

默认情况下,中断向量表放在Flash的起始地址(0x00000000)。但有些场景下,我们需要把向量表搬到RAM里——比如做IAP在线升级、或者运行RTOS时动态修改中断处理函数。

Cortex-M3/M4提供了VTOR(向量表偏移寄存器),位于系统控制块(SCB)中,地址0xE000ED08。通过修改VTOR,你可以告诉CPU:「向量表现在搬家了,新地址是XXX」。

典型的重定位代码:

#define SCB_VTOR      (*(volatile uint32_t *)0xE000ED08)

void vector_table_relocate(void)
{
    uint32_t *vector_table_src = (uint32_t *)0x08000000;  // Flash中的向量表
    uint32_t *vector_table_dst = (uint32_t *)0x20000000;  // RAM中的目标地址
    int i;

    // 1. 复制向量表到RAM
    for (i = 0; i < 16 + 48; i++)  // 16个系统异常 + 48个外部中断
    {
        vector_table_dst[i] = vector_table_src[i];
    }

    // 2. 设置VTOR指向RAM
    SCB_VTOR = (uint32_t)vector_table_dst;

    // 3. 确保内存屏障(DSB+ISB)
    __DSB();
    __ISB();
}

为什么要加DSB和ISB?因为修改VTOR后,CPU可能还在用旧的向量表。DSB保证所有内存访问完成,ISB刷新指令流水线——说白了就是让CPU「清醒一下」,重新认识新地址。

注意:重定位后,RAM中的向量表必须保持与Flash中一致。如果你只复制了前16个异常向量,却忘了复制外部中断向量,那中断来了CPU会直接跑飞——我有个同事就因为这个,调试了整整一周。

知识体系总览

下面这张图,把复位与初始化的核心流程串起来了。你可以把它当作本章的「思维导图」:

处理器复位与初始化流程 处理器复位与初始化核心流程 复位事件 硬件加载向量表 读取SP + PC 跳转到 Reset_Handler 确认CPU模式(SVC) 初始化堆栈指针 向量表重定位 完成以上步骤后,CPU才能安全地跳转到 main() 函数

这张图里,从复位事件到Reset_Handler是硬件自动完成的,后面的三步才是软件要干的活。我个人习惯在Reset_Handler里先检查CPU模式,再配SP,最后做向量表重定位——顺序不能乱,否则中间任何一步出错,后面都跑不起来。

实战中的常见陷阱

  • SP初始值写错:很多人直接把SP设为RAM的起始地址,忘了留空间给堆栈。结果一调用函数就溢出,覆盖了全局变量区。
  • VTOR设置后忘了加内存屏障:不加DSB/ISB,CPU可能还在用旧的向量表,中断来了直接跑飞。
  • 中断向量表复制不完整:只复制了前几个异常向量,外部中断向量没动——这种bug最难查,因为不是每次都会触发。
  • 模式切换时忘了保存LR:在ARM上切换模式会改变LR寄存器,如果不保存,返回地址就丢了。

我的建议:如果你用的是STM32或类似MCU,直接拿官方的启动文件改。别自己从头写向量表——官方已经帮你配好了SP和Reset_Handler,你只需要确认RAM大小和地址对不对就行。我刚开始做项目时总想「自己动手丰衣足食」,结果每次都在启动文件上浪费大量时间。