MCU 启动流程详解:从复位向量到 main 函数

2026年8月15日 0 By admin

MCU 启动流程详解:从复位向量到 main 函数

很多嵌入式工程师写 C 代码很熟,但被问到”程序是怎么从复位运行到 main 函数的”时却答不上来。这段”幕后流程”虽然大部分由启动文件和链接脚本自动完成,但搞懂它对理解内存布局、排查启动崩溃、做 Bootloader 和 OTA 都至关重要。

本文以 Cortex-M 内核 + STM32 为例,把启动流程一步步拆开。

一、复位那一刻发生了什么

MCU 上电或复位后,程序计数器(PC)并不会直接跳到 main,而是先由硬件完成几件事:

  1. 向量表的首地址(通常是 0x0000 0000 或可通过 VTOR 重映射)加载两个初始值;
  2. 第一个字是栈顶指针(MSP)的初始值,写入 SP;
  3. 第二个字是复位向量(Reset_Handler 的地址),加载到 PC。

这两个值就是启动的”原点”。Flash 的前两个 4 字节,一个决定栈在哪里,一个决定代码从哪里开始。

二、向量表:中断的”地址簿”

Cortex-M 的向量表是一张连续存放的地址表,前面是系统异常,后面是外设中断:

偏移 内容
0x00 初始栈顶指针(MSP)
0x04 Reset_Handler
0x08 NMI_Handler
0x0C HardFault_Handler
其他系统异常
0x40 起 外设中断(IRQ)

中断发生时,硬件会根据中断号自动从向量表取对应的处理函数地址。所以向量表的位置必须正确,这也是 Bootloader 跳转需要重定位向量表的原因。

三、启动文件做了什么

以 STM32 标准启动文件 startup_stm32f407xx.s 为例,核心流程如下:

/* Reset_Handler 的伪代码 */
void Reset_Handler(void)
{
    /* 1. 把 .data 段从 Flash 复制到 RAM */
    CopyDataSection();

    /* 2. 把 .bss 段(未初始化全局变量)清零 */
    ZeroBssSection();

    /* 3. 调用系统初始化(时钟、FPU 等) */
    SystemInit();

    /* 4. 调用 C 运行时库初始化 */
    __libc_init_array();

    /* 5. 跳转到用户 main 函数 */
    main();
}

这段逻辑对应汇编文件里的 Reset_Handler 标签,它会调用 SystemInit 初始化时钟,然后进入 __main(C 库启动代码),最终才轮到你的 main()

四、.data 与 .bss:全局变量为什么能用

写程序时,全局变量的初值保存在哪里?答案分两种情况:

  • 已初始化的全局变量:初值存在 Flash 的 .data 段,运行时由启动代码复制到 RAM
  • 未初始化的全局变量:在 RAM 的 .bss 段,启动代码负责清零
int g_init_val = 100;   /* 初值 100 存在 Flash .data 段,运行时复制到 RAM */
int g_uninit_val;       /* .bss 段,启动时清零 */

如果不理解这一点,遇到”全局变量初值不对””变量莫名清零”这类问题就很难解释。

五、链接脚本:内存地图的”图纸”

启动代码和链接脚本(.ld 文件)一一对应。链接脚本决定了各段放在 Flash 还是 RAM:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 192K
}

SECTIONS
{
  .text  : { *(.text) } > FLASH    /* 代码 */
  .rodata: { *(.rodata) } > FLASH  /* 只读数据 */
  .data  : { *(.data) } > RAM AT> FLASH  /* 已初始化变量 */
  .bss   : { *(.bss) } > RAM      /* 未初始化变量 */
}

改链接脚本是做 Bootloader、分配自定义内存区域、固件加密等高级功能的必要技能。

六、常见启动问题排查

  1. 上电不跑:查复位电路、时钟配置(外部晶振没起振)、BOOT 引脚。
  2. HardFault:常见于栈溢出、数组越界写坏栈、外设时钟没开就访问寄存器。
  3. 进不了 main:可能是 SystemInit 里卡住(等待时钟就绪超时),或链接脚本段配置错误。
  4. Bootloader 跳转失败:跳转前没重设 MSP、没关中断、没重定位向量表。

总结

从复位到 main,本质上是这样一条链:

硬件取复位向量 → 启动代码初始化数据段 → SystemInit 配时钟 → 跑 C 运行时 → 进入 main

搞懂这条链,你就真正理解了”单片机程序是怎么跑起来的”,也为 Bootloader、OTA、异常定位打下了地基。这是从”会用库”到”理解系统”的关键一跃。