5、复位处理流程:从复位向量到SystemInit

芯片上电那一刻,CPU 做的第一件事是什么?

不是跑你的 main 函数,也不是初始化外设。它先得找到「我从哪里开始执行」——这个起点,就是复位向量。

我早年调试一块 STM32F103 的板子,焊好之后死活跑不起来。用示波器抓复位引脚,波形正常。后来才发现,是我把启动文件里的复位向量地址写偏了 4 个字节。CPU 一跳进去就取到了错误指令,直接 HardFault。嗯,从那以后,我每次建新工程都会先看一眼 .s 文件里的向量表。

5.1 复位向量的本质

复位向量不是一个函数,它是一个地址。CPU 在复位之后,硬件会自动从这个地址加载数据,然后跳转过去执行。

在 Cortex-M 系列内核里,复位向量的机制是这样的:

  • 向量表通常放在 Flash 的起始地址,也就是 0x08000000(具体看芯片)
  • 向量表的第一个字(4 字节)存放的是栈顶指针初始值
  • 第二个字存放的是复位向量,也就是 Reset_Handler 的入口地址

说白了,CPU 上电后干了两件事:

  1. 从 0x00000000 或 0x08000000 读取栈顶指针,赋值给 SP
  2. 从 0x00000004 或 0x08000004 读取复位向量,赋值给 PC

然后 PC 就指向了 Reset_Handler,开始执行启动代码。

关键点:复位向量地址必须是奇数(因为 Thumb 指令要求 LSB 为 1)。如果写成偶数,CPU 会进入错误状态。我见过有人手写链接脚本时忘了加 1,结果芯片直接死掉。

5.2 Reset_Handler 里发生了什么

Reset_Handler 是启动代码的核心。它负责把芯片从「原始状态」带到「C 语言可运行状态」。

典型的 Reset_Handler 流程如下:

Reset_Handler   PROC
    EXPORT  Reset_Handler           [WEAK]
    IMPORT  __main
    IMPORT  SystemInit

    ; 1. 设置栈指针(其实硬件已经做了,但有些芯片会再设一次)
    LDR     R0, =__initial_sp
    MSR     MSP, R0

    ; 2. 调用 SystemInit 做底层时钟和硬件初始化
    BL      SystemInit

    ; 3. 跳转到 __main(C 库的入口,最终会调用 main)
    B       __main
    ENDP

你看,就这几行汇编。但每一行背后都有讲究。

5.3 SystemInit 到底干了什么

SystemInit 是芯片厂商提供的函数,一般在 system_stm32fxxx.c 这类文件里。它的任务很明确:

  • 配置系统时钟(HSE、PLL、SYSCLK、AHB、APB 分频)
  • 初始化 Flash 预取缓冲和等待周期
  • 设置中断向量表偏移(如果需要重映射)
  • 配置电源管理相关的寄存器

我举个例子,STM32F407 的 SystemInit 里有一段:

void SystemInit(void)
{
    /* 设置 Flash 等待周期 */
    FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS;

    /* 配置 PLL */
    RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (PLL_P << 16) | (PLL_Q << 24);

    /* 使能 PLL 并等待就绪 */
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));

    /* 切换系统时钟到 PLL */
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

这段代码看起来简单,但如果你把等待周期设少了,Flash 读取就会出错,程序跑飞。我调试一块 F429 的板子时,就因为没注意主频跑到了 180MHz 而 Flash 等待周期只设了 4,结果程序随机崩溃。查了两天才找到原因。

我的习惯:在 SystemInit 最后加一个 GPIO 翻转,用示波器看波形。如果波形正常,说明时钟配置没问题。这招帮我省了不少调试时间。

5.4 从 Reset_Handler 到 __main 的过渡

SystemInit 返回后,CPU 跳转到 __main。注意,这里的 __main 不是你的 main 函数,它是 C 库的入口。

__main 会做三件事:

  1. 数据段复制:把 .data 段从 Flash 拷贝到 RAM
  2. BSS 段清零:把 .bss 段全部写 0
  3. 调用 main:最后才跳转到你的 main 函数

所以,你的 main 函数执行时,全局变量已经初始化好了,未初始化的全局变量也已经是 0 了。

注意:不要在 SystemInit 里访问全局变量!因为此时 .data 段还没复制,.bss 段也没清零。你读到的全局变量可能是随机值。我曾经看到有人把系统时钟配置参数放在全局变量里,结果 SystemInit 读到的全是乱数,时钟配得一塌糊涂。

5.5 整个复位流程的时序图

下面我用一张 SVG 图把整个过程串起来,方便你理解:

复位处理流程时序图 时间 1. 上电复位 / 外部复位引脚触发 2. 硬件加载向量表:SP ← 0x00000000, PC ← 0x00000004 3. PC 跳转到 Reset_Handler(启动代码入口) 4. Reset_Handler 调用 SystemInit(时钟/硬件初始化) 5. 跳转 __main → 数据段复制 / BSS清零 → 调用 main() 硬件 自动 完成 启动 代码 阶段 C 库 初始化 阶段 用户 main

5.6 常见陷阱与避坑指南

复位流程看起来简单,但实际项目中踩坑的人不少。我总结几个常见的:

  • 向量表对齐问题:向量表必须按 256 字节或 512 字节对齐(看芯片手册)。如果没对齐,中断可能跳错地方。
  • SystemInit 里用了未初始化硬件:比如在配置 PLL 之前就去操作某个外设,那个外设的时钟还没开,直接死锁。
  • 忘记使能 FPU:Cortex-M4/M7 的 FPU 默认是关闭的。如果 SystemInit 里没使能,浮点运算直接进 HardFault。
  • 中断向量表偏移设错:如果你用了 Bootloader,需要把向量表重映射到应用程序的起始地址。偏移量算错一个字,所有中断都乱套。

我的调试技巧:在 Reset_Handler 开头放一个 GPIO 翻转,用逻辑分析仪抓。如果看到翻转,说明 CPU 确实跑到了启动代码。然后再在 SystemInit 入口和出口各放一个翻转,就能判断 SystemInit 是否卡死。这比看调试器里的 PC 寄存器直观多了。

复位流程是整个嵌入式系统的基石。你把它吃透了,后面遇到启动异常、中断异常、时钟配置问题,心里就有底了。


公众号:蓝海资料掘金营,微信deep3321