
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 内核不一样它的取指规则是CPU 从地址0x00000000读取初始栈顶地址写入主堆栈指针 MSP。CPU 从地址0x00000004读取复位向量值写入程序计数器 PC。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 下载引导内置 Bootloader11内嵌 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, ALIGN3 Stack_Mem SPACE Stack_Size __initial_sp这段代码定义了一块大小为 0x4001KB的栈空间并把栈顶地址暴露为__initial_sp。这个地址会被放在向量表首位置。堆的定义类似Heap_Size EQU 0x200 AREA HEAP, NOINIT, READWRITE, ALIGN3 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 内核的行为从0x00000000取出栈顶地址写入 MSP。从0x00000004取出复位向量值写入 PC。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 振荡器频率一般为 8MHzF1 系列或 16MHzF4 系列。如果不切换或者配置 PLLCPU 直接跑内部高速时钟外设的波特率、定时器时间基准、PWM 频率全都对不上。第二main函数里所有外设初始化代码都默认前提是系统时钟已经配置到预期频率。如果SystemInit没有正确执行UART 波特率计算、定时器分频、CAN 位时序全部错乱。从代码逻辑看SystemInit做的事情大致是设置 Flash 等待周期。使能外部高速晶振 HSE。等待 HSE 就绪。配置 PLL 倍频系数。使能 PLL 并等待锁定。切换系统时钟源到 PLL。更新全局变量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 工程里叫分散加载文件.sctGCC 工程里叫链接脚本.ldIAR 里是.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。如果项目使用 BootloaderApp 的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 入口进入 Debug 模式。打开寄存器窗口观察 SP 和 PC 初值。单步执行观察 R0 寄存器值变化。逐步进入 SystemInit、__main、main。在main入口处暂停时打开内存窗口查看0x08000000的值应该等于程序栈顶地址。查看0x08000004的值应该等于 Reset_Handler 地址。查看 SRAM 中 RW 段和 ZI 段是否正常。也可以用 ST-Link Utility 或 STM32CubeProgrammer 直接读取 Flash 起始地址内容验证向量表。调试时重点观察这几项观察项预期值SPMSP等于向量表首地址中的值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 之间发生了什么回答思路分四层硬件层上电复位后 CPU 从 0x00000000 取栈顶地址从 0x00000004 取复位向量。启动文件层跳入 Reset_Handler。时钟层调用 SystemInit 配置时钟或者等待上层配置。C 运行环境层跳入 __main拷贝 RW 段、清零 ZI 段最后进入 main。问为什么启动文件要定义中断向量表回答Cortex-M 内核处理中断时要把中断号映射为向量表偏移再从表中取出服务函数地址。没有向量表中断机制无从谈起。问全局变量初始化的值是谁赋的回答不是 C 编译器在编译期直接写入 SRAM而是启动代码在进入 main 之前把 Flash 中的 RW 段数据拷贝到 SRAM。如果启动文件缺失全局变量初始化会失败。问从 Bootloader 跳转 App应该注意什么回答必答三点关闭或重新初始化全局中断。设置新的 MSP。设置 SCB-VTOR 偏移到 App 向量表地址。问能不能不修改 VTOR 也完成跳转回答可以把新建工程时 App 的向量表放置在固定地址配合链接脚本和编译选项实现。但这种做法不适合所有芯片系列也不推荐在带中断的工程中使用。问栈大小设多少合适回答没有标准答案。要结合最大局部变量、函数嵌套深度、中断嵌套层数和 RTOS 任务栈综合评估。可以用栈填充法实测最大占用。14. 从启动过程看嵌入式学习路径启动过程不是一个孤立知识点。把它理解透彻后像链接脚本、内存布局、中断优先级、看门狗复位、IAP 跳转、RTOS 任务切换这些内容都会串联起来。建议的学习路径是先精读对应芯片的启动文件逐行注释弄清楚每段汇编的作用。打开调试器在 Reset_Handler 和 __main 各下断点看汇编单步执行。阅读分散加载文件.sct理解代码段、数据段的存放位置。写一个简单的 Bootloader实现 Flash 启动跳转 App亲手验证 VTOR 的作用。用压栈法检测任务栈和系统栈使用情况加深对栈空间分配的认识。对面试来说能结合自己的实践经历说出“我在做 IAP 升级时因为忘记设置 VTOR 导致跳转后中断死机后来通过检查 SCB-VTOR 发现问题”这种回答远比背题库更有说服力。15. 总结从 BOOT 引脚电平到main里的第一条用户代码STM32 的启动过程是一条完整清晰的链路引脚决定启动介质。向量表决定初始 SP 和 PC。Reset_Handler 完成时钟和 C 环境初始化。__main准备好内存和运行环境。最终进入main用户代码才开始执行。面试要能讲清楚这条链路开发时要知道怎么查问题做 Bootloader 时要知道怎么处理二次跳转。建议收藏备用并在 Keil 中把startup_stm32f10x_hd.s打开逐行对照这篇文章梳理一遍。真正动手调一次启动流程比背十遍面试题都管用。