☰
STM32启动过程全解析:从复位向量到main函数的完整链路
2026/10/14 20:58:46 网站建设 项目流程

STM32 上电后到底先干什么?相信很多写了两三年单片机代码的同学,被面试官问一句“请描述 STM32 的启动过程”,都会卡在那里。这段过程不是简单一句“从 main 开始跑”就能回答的,它涉及硬件复位时序、启动模式引脚配置、启动文件汇编逻辑、中断向量表加载、C 运行时环境初始化、时钟系统切换和分散加载文件等一整条链路。这篇文章把 STM32 启动过程完整拆开,从 Boot 引脚电平一路看到 main 函数里第一条用户代码,再补上启动过程相关的常见面试题和排查手段。

1. 核心知识点地图

项目项说明
适用芯片STM32F1/F4 系列为主,也适用大部分 Cortex-M3/M4 内核芯片
核心脉络上电复位 → 读取向量表 → 设置栈顶 SP → 跳转 Reset_Handler → SystemInit → __main → main
启动文件startup_stm32f10x_hd.s 或 startup_stm32f4xx.s
三要素初始 SP、初始 PC、中断向量表
启动模式由 BOOT0/BOOT1 引脚电平决定
常见工具Keil MDK、STM32CubeIDE、IAR、STM32CubeProgrammer
易错点向量表偏移、堆栈大小设置、BOOT 引脚误配置、SystemInit 未执行
适用场景嵌入式求职面试、裸机开发、Bootloader/IAP 开发、低功耗唤醒分析

2. 为什么面试官必问启动过程

STM32 的启动过程不是单纯考察记忆力,它是判断一个人有没有建立“嵌入式系统整体观”的典型考点。一个长期只调用 HAL 库函数写业务逻辑的人,很难回答清楚下面这些连环问题:

  • 芯片上电后从哪里取第一条指令?
  • 栈顶地址是谁告诉 CPU 的?
  • 为什么不是直接进 main?
  • 进入 main 之前,全局变量为什么已经被初始化好了?
  • 改变 BOOT 引脚电平为什么能决定程序从哪个地址启动?

这些问题背后对应的是三个嵌入式开发核心能力:看芯片数据手册的能力、看汇编启动文件的能力、理解链接脚本的能力。

启动过程还直接关系到实际开发和调试中的疑难问题:

  • 程序下载后不运行,怀疑 BOOT 引脚配置错误。
  • 调试器一连接就报No target connected,怀疑复位时序和向量表异常。
  • 自己写了 Bootloader 做 IAP 升级,跳转到 App 后死机,本质就是二次启动过程没有处理对。
  • 低功耗唤醒后程序跑飞,可能就是对复位向量和中断向量理解不深。

所以,这不是背一道八股题,而是嵌入式底层的必备知识。

3. 上电之后,STM32 的第一条指令在哪里

要理解 STM32 启动过程,先要确立一个和 51 单片机完全不同的内核模型。

51 单片机上电后 CPU 直接从固定地址 0x0000 开始取指令,程序计数器 PC 被硬件设置为 0x0000,后面就是一锅端顺序执行。

Cortex-M 内核不一样,它的取指规则是:

  1. CPU 从地址0x00000000读取初始栈顶地址,写入主堆栈指针 MSP。
  2. CPU 从地址0x00000004读取复位向量值,写入程序计数器 PC。
  3. CPU 从 PC 指向的地址开始取指执行。

这里有个容易搞混的点:0x00000004存放的不是指令,而是复位向量的地址值。CPU 不是跳到 0x00000004 执行,而是把 0x00000004 里的内容当作一个地址,跳转过去。

为了把问题说得更具体,用 STM32F103 系列举例。这款芯片的内部 Flash 起始地址是0x08000000,而 CPU 上电后默认从0x00000000读取向量表。

那问题来了:0x00000000地址上的内容哪里来的?

这里涉及 STM32 的物理映射机制。从系统存储角度看,启动时芯片会依据 BOOT 引脚电平选择存储介质,然后把选中的存储介质映射到0x00000000。也就是说,0x00000000不是一个真实存储设备,而是一个映射窗口。

如果从 Flash 启动,芯片内部把0x08000000镜像到0x00000000,所以:

  • 地址0x00000000的内容等于 Flash 起始内容。
  • 地址0x00000004的内容等于 Flash 偏移 4 字节处的内容。

大多数情况下,这两个地址存放的就是启动文件开头定义的__initial_sp和Reset_Handler。

4. 三种启动模式与 BOOT 引脚

STM32 上电时,BOOT0和BOOT1引脚的电平组合决定了从哪个物理存储介质启动。

BOOT0BOOT1启动介质说明
0任意主 Flash正常程序启动模式,最常用
10系统存储器用于串口 ISP 下载引导(内置 Bootloader)
11内嵌 SRAM调试模式,程序掉电即丢

注意,BOOT 引脚对启动模式的影响是在上电复位瞬间被锁存的。在程序运行过程中,即使你强制把 BOOT0 引脚拉高,程序也不会立刻切换启动来源。

这里需要重点强调 Flash 启动和系统存储器启动的本质区别:

  • Flash 启动:CPU 执行用户自己烧录到主 Flash 里的程序。
  • 系统存储器启动:CPU 执行 STM32 出厂固化在 ROM 区的一段 Bootloader 程序,这段程序的功能是接收串口或 USB 数据,并把用户程序写入主 Flash。这就是 ISP 下载的基础原理。
  • SRAM 启动:CPU 直接在 SRAM 里执行程序,适合调试和验证,掉电后程序消失。

在做 Bootloader/IAP 开发时,经常需要处理启动模式切换。常见做法是:Bootloader 运行在 Flash 启动模式下,收到升级包后写入 Flash 的 App 分区,最后通过修改 PC 跳转地址完成“软件启动切换”,而不是强行改变 BOOT 引脚。

5. 启动文件源码级拆解

Keil MDK 新建 STM32F103 工程时,会自动添加一个startup_stm32f10x_hd.s汇编文件。不要把这个文件当成可有可无的模板,它承担了 Cortex-M3 上电后整个启动逻辑。

5.1 栈和堆的定义

启动文件开头一般是这样:

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

这段代码定义了一块大小为 0x400(1KB)的栈空间,并把栈顶地址暴露为__initial_sp。这个地址会被放在向量表首位置。

堆的定义类似:

Heap_Size EQU 0x200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 Heap_Mem SPACE Heap_Size

栈和堆的大小直接决定程序可用内存。如果局部变量过多、递归层数太深、或者中断嵌套频繁,栈溢出会导致程序跑飞甚至 HardFault。面试常问的“栈溢出怎么排查”,根源就在这里。

5.2 中断向量表

向量表是启动文件最重要的部分:

AREA RESET, DATA, READONLY __Vectors DCD __initial_sp ; 栈顶地址 DCD Reset_Handler ; 复位向量 DCD NMI_Handler DCD HardFault_Handler DCD MemManage_Handler DCD BusFault_Handler DCD UsageFault_Handler ... DCD WWDG_IRQHandler DCD PVD_IRQHandler DCD TAMPER_IRQHandler DCD RTC_IRQHandler DCD FLASH_IRQHandler DCD RCC_IRQHandler ...

向量表的第一项是__initial_sp,第二项是Reset_Handler。后面的每一项都对应一个中断服务函数入口地址,顺序必须和芯片手册中中断向量分配表完全一致。这就是为什么不能随意改启动文件中断项顺序的原因。

需要注意DCD不是一条指令,它是一段数据定义伪指令,作用是在当前地址分配 4 字节空间并填入指定值。

5.3 Reset_Handler 做了什么

先看 Cortex-M3 内核的行为:

  1. 从0x00000000取出栈顶地址写入 MSP。
  2. 从0x00000004取出复位向量值写入 PC。
  3. CPU 跳转到复位向量指向的地址,也就是进入Reset_Handler。

Reset_Handler是启动阶段真正干活的函数:

Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP

这段汇编完成两件事:

  • 调用SystemInit函数配置系统时钟。
  • 跳转到__main,进入 C 运行环境初始化,最终调用main()。

这里注意:__main不是 main 函数本尊,而是 C 库提供的入口函数,它负责后续一堆初始化工作。

6. 时钟初始化 SystemInit 与启动过程的关系

很多初学者第一次看到SystemInit()时,以为这只是个普通配置函数。实际上,它必须在main之前执行,原因有二:

第一,芯片上电默认使用内部 HSI 振荡器,频率一般为 8MHz(F1 系列)或 16MHz(F4 系列)。如果不切换或者配置 PLL,CPU 直接跑内部高速时钟,外设的波特率、定时器时间基准、PWM 频率全都对不上。

第二,main函数里所有外设初始化代码,都默认前提是系统时钟已经配置到预期频率。如果SystemInit没有正确执行,UART 波特率计算、定时器分频、CAN 位时序全部错乱。

从代码逻辑看,SystemInit做的事情大致是:

  1. 设置 Flash 等待周期。
  2. 使能外部高速晶振 HSE。
  3. 等待 HSE 就绪。
  4. 配置 PLL 倍频系数。
  5. 使能 PLL 并等待锁定。
  6. 切换系统时钟源到 PLL。
  7. 更新全局变量SystemCoreClock。

F1 系列中,典型配置是 8MHz HSE 倍频到 72MHz 主频。F4 系列则多一步,需要把主频倍频到 168MHz 甚至 180MHz。

这里要特别提醒一点:如果你用 STM32CubeMX 生成工程,SystemClock_Config()是在main函数里执行的,这是标准库和 HAL 库的一个差异。HAL 库模式下,启动文件同样会调用SystemInit,但时钟树的具体配置延后到 main 里完成。面试时如果说“启动阶段配置时钟”,要补充说明标准库和 HAL 库的差异,回答会更完整。

7. __main 与 C 运行环境初始化

从Reset_Handler跳转进入__main后,代码正式由汇编过渡到 C 库运行时。__main主要完成以下工作:

7.1 加载 RW 段

场景是这样的:可执行文件被烧录到 Flash 里,但程序中的已初始化全局变量如uint8_t g_flag = 1;存放在 Flash 的只读区域。运行后这些变量必须在 SRAM 里读写,所以启动阶段要把这段数据从 Flash 拷贝到 SRAM。这个过程称为 RW 段加载。

7.2 清零 ZI 段

未初始化全局变量如static uint8_t buffer[1024];默认值必须为 0。启动代码会把对应内存区域全部清 0。这就是为什么 C 语言标准规定全局变量默认值为 0 的底层原因,也是启动阶段不需要用户干预但是必须完成的内存准备工作。

7.3 调用析构函数或构造函数相关初始化

在 C 工程里这部分基本没有实质内容。如果使用 C++,__main会调用全局对象的构造函数,然后才进入main。

完成以上工作后,__main通过跳转指令进入main(),用户程序才真正开始执行。

段名存储位置运行位置内容
RO 段FlashFlash代码、常量
RW 段FlashSRAM已初始化全局变量
ZI 段无初始内容SRAM未初始化全局变量,运行时清零

8. 分散加载文件与启动地址映射

启动过程能正常运行,还离不开链接脚本的帮助。Keil 工程里叫分散加载文件.sct,GCC 工程里叫链接脚本.ld,IAR 里是.icf。

F1 系列在 Keil MDK 下典型的分散加载配置:

LR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } }

关键点:

  • 复位向量被放在+First位置,保证启动文件生成的向量表在 Flash 起始地址。
  • 运行起始地址0x08000000对应主 Flash 起始地址。
  • RAM 起始地址0x20000000对应 SRAM。
  • 如果项目使用 Bootloader,App 的LR_IROM1起始地址要改为0x08010000或0x08008000等偏移地址,App 向量表也要跟随偏移,否则中断跳转会错乱。

App 侧跳转时需要设置向量表偏移:

#define APP_ADDR 0x08010000 void jump_to_app(void) { uint32_t app_sp = *(volatile uint32_t *)APP_ADDR; uint32_t app_pc = *(volatile uint32_t *)(APP_ADDR + 4); typedef void (*app_func)(void); app_func jump = (app_func)app_pc; // 关闭全局中断,清理外设状态 __disable_irq(); // 设置主栈指针 __set_MSP(app_sp); // 跳转前设置向量表偏移(仅限 Cortex-M3/M4) SCB->VTOR = APP_ADDR; // 跳转 jump(); }

这段代码体现了对启动过程的深层理解:跳转 App 本质上是人为构造了一次复位向量加载过程,手动设置 MSP 和 PC。

9. 中断向量表重定向

大多数 STM32 应用不使用 Bootloader,向量表固定放在 Flash 起始地址,无需处理。但一旦涉及 IAP、OTA、二次引导,中断向量表重定向就是必考题。

Cortex-M3/M4 内核提供一个VTOR寄存器,位于系统控制块 SCB 中。通过修改它可以把中断向量表从0x08000000移动到0x08010000。这样,中断发生后 CPU 会从新的向量表位置读取对应的中断服务函数地址。

F1 系列和 F4 系列对VTOR的要求略有不同:

  • F4 系列直接写SCB->VTOR = APP_ADDR即可。
  • F1 系列某些芯片和库版本存在对齐限制,需要按 0x100 对齐。

HAL 库中,常用:

SCB->VTOR = APP_ADDR & (uint32_t)0x1FFFFF80;

或者用宏封装:

#define FLASH_BASE_ADDR 0x08000000 #define APP_START_ADDR 0x08010000 void set_vector_table_offset(uint32_t addr) { SCB->VTOR = addr & (uint32_t)0x1FFFFF80; }

启动向量表偏移设置错误时,最典型的症状是:程序能跳转到 App,但任何中断一触发,CPU 就跳到一个错误地址,最终进入 HardFault。现象表现为串口能打印、GPIO 能翻转,但定时器中断一开就死机。

10. 硬件复位、看门狗复位与软件复位的启动差异

启动过程并不只出现在上电瞬间。一次完整的复位包括多种来源:

复位源触发方式启动后的异同
上电复位 POR芯片供电上升完整启动流程
外部复位 NRST拉低复位引脚流程同上
看门狗复位 IWDG/WWDG看门狗超时复位标志不同
软件复位 NVIC_SystemReset写 AIRCR 寄存器流程同上
低功耗唤醒Standby/Stop 唤醒从唤醒向量或复位流程进入

不同复位源在 RCC 控制寄存器中留下不同的复位标志位,Bootloader 可以通过这些标志判断本次启动原因。比如:

if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST) != RESET) { // 独立看门狗复位 } __HAL_RCC_CLEAR_RESET_FLAGS();

Standby 唤醒则不完全等价于复位,CPU 从唤醒向量恢复执行,RAM 内容保留。这点在低功耗产品设计中很关键。

11. 启动过程的实测验证方法

如果只是想看现象,理解启动过程最直接的方法是仿真调试。

在 Keil MDK 中,把断点打在 Reset_Handler 入口:

  1. 进入 Debug 模式。
  2. 打开寄存器窗口,观察 SP 和 PC 初值。
  3. 单步执行,观察 R0 寄存器值变化。
  4. 逐步进入 SystemInit、__main、main。

在main入口处暂停时,打开内存窗口:

  • 查看0x08000000的值,应该等于程序栈顶地址。
  • 查看0x08000004的值,应该等于 Reset_Handler 地址。
  • 查看 SRAM 中 RW 段和 ZI 段是否正常。

也可以用 ST-Link Utility 或 STM32CubeProgrammer 直接读取 Flash 起始地址内容验证向量表。

调试时重点观察这几项:

观察项预期值
SP(MSP)等于向量表首地址中的值
PC等于 Reset_Handler 地址
向量表偏移0x08000000 或 Bootloader 配置的 App 地址
时钟寄存器PLL 就绪且为主时钟源
main 是否执行断点能进入

12. 启动过程常见问题与排查方法

启动阶段出问题,程序可能完全没有任何现象。这时候与其反复改业务代码,不如按下面表格排查。

问题现象可能原因排查方式解决方案
下载后程序没运行BOOT0/BOOT1 引脚配置不当测量 BOOT 引脚电平BOOT0 接 GND,从 Flash 启动
调试器连接失败芯片处于睡眠状态、复位电路异常检查 NRST 波形、供电电压手动复位后快速连接
跳转 App 后 HardFault向量表偏移未设置或设置错误查看 VTOR 寄存器设置SCB->VTOR为 App 地址
程序启动后无中断响应中断使能前未设置向量表偏移检查 SCB->VTOR 值Bootloader 跳转前配置 VTOR
全局变量初值不是预设值RW 段加载失败检查分散加载文件和启动文件确认启动文件被正确链接
局部变量多导致栈溢出Stack_Size 设置偏小查看栈指针位置增大栈空间,减少递归深度
时钟配置不对导致外设乱码SystemInit 或时钟配置未执行检查系统时钟寄存器确认 SystemInit 被调用,PLL 配置正确
从 SRAM 启动后程序不工作SRAM 掉电丢失,程序未重烧确认启动模式正常烧录到 Flash 后改回 Flash 启动
Keil 工程提示找不到启动文件文件未添加或路径错误检查工程文件列表重新添加 startup 文件

13. 面试考点与标准回答思路

下面是几个启动过程最常见的面试串问:

问:STM32 上电后到 main 之间发生了什么?

回答思路分四层:

  1. 硬件层:上电复位后 CPU 从 0x00000000 取栈顶地址,从 0x00000004 取复位向量。
  2. 启动文件层:跳入 Reset_Handler。
  3. 时钟层:调用 SystemInit 配置时钟,或者等待上层配置。
  4. C 运行环境层:跳入 __main,拷贝 RW 段、清零 ZI 段,最后进入 main。

问:为什么启动文件要定义中断向量表?

回答:Cortex-M 内核处理中断时,要把中断号映射为向量表偏移,再从表中取出服务函数地址。没有向量表,中断机制无从谈起。

问:全局变量初始化的值是谁赋的?

回答:不是 C 编译器在编译期直接写入 SRAM,而是启动代码在进入 main 之前把 Flash 中的 RW 段数据拷贝到 SRAM。如果启动文件缺失,全局变量初始化会失败。

问:从 Bootloader 跳转 App,应该注意什么?

回答:必答三点:

  1. 关闭或重新初始化全局中断。
  2. 设置新的 MSP。
  3. 设置 SCB->VTOR 偏移到 App 向量表地址。

问:能不能不修改 VTOR 也完成跳转?

回答:可以,把新建工程时 App 的向量表放置在固定地址,配合链接脚本和编译选项实现。但这种做法不适合所有芯片系列,也不推荐在带中断的工程中使用。

问:栈大小设多少合适?

回答:没有标准答案。要结合最大局部变量、函数嵌套深度、中断嵌套层数和 RTOS 任务栈综合评估。可以用栈填充法实测最大占用。

14. 从启动过程看嵌入式学习路径

启动过程不是一个孤立知识点。把它理解透彻后,像链接脚本、内存布局、中断优先级、看门狗复位、IAP 跳转、RTOS 任务切换这些内容都会串联起来。

建议的学习路径是:

  1. 先精读对应芯片的启动文件,逐行注释,弄清楚每段汇编的作用。
  2. 打开调试器,在 Reset_Handler 和 __main 各下断点,看汇编单步执行。
  3. 阅读分散加载文件.sct,理解代码段、数据段的存放位置。
  4. 写一个简单的 Bootloader,实现 Flash 启动跳转 App,亲手验证 VTOR 的作用。
  5. 用压栈法检测任务栈和系统栈使用情况,加深对栈空间分配的认识。

对面试来说,能结合自己的实践经历说出“我在做 IAP 升级时,因为忘记设置 VTOR 导致跳转后中断死机,后来通过检查 SCB->VTOR 发现问题”这种回答,远比背题库更有说服力。

15. 总结

从 BOOT 引脚电平到main里的第一条用户代码,STM32 的启动过程是一条完整清晰的链路:

  • 引脚决定启动介质。
  • 向量表决定初始 SP 和 PC。
  • Reset_Handler 完成时钟和 C 环境初始化。
  • __main准备好内存和运行环境。
  • 最终进入main,用户代码才开始执行。

面试要能讲清楚这条链路,开发时要知道怎么查问题,做 Bootloader 时要知道怎么处理二次跳转。建议收藏备用,并在 Keil 中把startup_stm32f10x_hd.s打开,逐行对照这篇文章梳理一遍。真正动手调一次启动流程,比背十遍面试题都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询