STM32启动流程详解:从复位到main函数完整拆解
发布时间:2026/8/31 5:04:00 作者:尧图编辑部 阅读量:1,286

在嵌入式单片机开发中有一个问题几乎每次面试都会被问到也是很多开发者容易“感觉懂、讲不透”的知识点STM32 的启动过程。很多人在简历上写“精通 STM32”但被问到“复位后芯片内部做了什么”“启动文件里那几段汇编是什么意思”“为什么要从 Flash 启动”时往往只能答出“从 Flash 开始执行代码”这样的表面内容。本文将完整拆解 STM32 的启动流程从硬件复位到进入 main 函数的全过程覆盖启动模式、启动文件分析、内存映射、中断向量表、常见启动失败排查以及面试高频追问。无论你是准备单片机岗位面试还是想系统理解 STM32 的底层运行机制这篇文章都值得收藏。1. 为什么面试官总喜欢问“STM32 启动过程”1.1 一个高频问题的背后考察点面试官问启动过程通常不是为了让你背出某一段固定答案而是通过这个问题考察以下几个方面是否真正理解单片机从复位到运行的整体流程是否知道 STM32 有三种启动模式以及它们有什么区别是否阅读过 startup 启动文件并理解中断向量表、栈顶地址、Reset_Handler 等概念是否具备调试能力比如程序跑飞、进不了 main 函数时如何定位问题是否了解 IAP、Bootloader、系统存储器等实际工程概念。换句话来说这道题考察的不是记忆而是整个嵌入式开发的底层认知体系。1.2 启动过程与日常开发的关系很多初学者会遇到这些现象下载程序后开发板没有任何反应仿真时程序停在启动文件里不进入 main修改了堆栈大小程序却出现了奇怪的跑飞现象使用 IAP 升级时不清楚 Bootloader 和 App 之间如何跳转。这些问题背后几乎都与启动过程有关。把启动过程搞明白不仅能应对面试也能在实际项目里少走弯路。1.3 本文讲解范围本文以常见的 STM32F103 系列为例展开分析同时说明启动过程中与芯片型号相关的差异。重点内容包括STM32 的三种启动模式从 Flash 启动时 CPU 的完整执行路径启动文件 startup 的逐段拆解栈顶地址、中断向量表、SystemInit、__main 的作用调试器观察启动过程的实操技巧面试追问与答题思路启动失败的排查思路和工程建议。说明不同系列如 STM32F4、STM32H7、STM32L0在时钟配置、启动文件细节上会有差异但整体框架是一致的。理解 F1 的启动流程后再迁移到其他系列会非常容易。2. STM32 的三种启动模式2.1 什么是启动模式STM32 允许用户通过 BOOT0 和 BOOT1 引脚的上下拉状态选择芯片复位后从哪里开始执行代码这就是启动模式。STM32F103 的启动模式配置如下表所示BOOT0BOOT1启动模式说明0X主 Flash 启动从内部 Flash 启动正常运行用户程序10系统存储器启动从系统存储器启动用于串口下载等 ISP 模式11SRAM 启动从内部 SRAM 启动常用于调试2.2 三种启动模式的作用主 Flash 启动这是最常见的启动方式。用户程序烧录到芯片内部的 Flash 中复位后 CPU 从 0x08000000 地址开始执行。正常产品运行时BOOT0 和 BOOT1 都应该保证芯片从 Flash 启动。系统存储器启动系统存储器是芯片出厂时固化的一段 Bootloader 程序主要用于串口 ISP 下载。当 BOOT01、BOOT10 时复位后会运行芯片出厂自带的固件用户可以通过 USART1 等接口下载程序到 Flash。很多开发板上的“一键下载电路”就是通过电路控制 BOOT0 引脚的状态在上电瞬间进入系统存储器启动模式配合串口工具完成程序下载。SRAM 启动从 SRAM 启动时代码在 RAM 中运行不需要写 Flash。这种模式常用于调试因为 SRAM 的读写速度比 Flash 更快也避免了反复擦写 Flash 的损耗。但 SRAM 掉电数据丢失所以只适合开发调试阶段使用。2.3 为什么需要多种启动模式简单来说启动模式的本质是“改变 CPU 复位后取指的位置”。不同场景下需要使用不同的启动来源产品发布后必须从 Flash 启动保证程序断电不丢首次烧录或量产烧录时可以利用系统存储器 Bootloader 走串口下载不需要额外的仿真器开发调试阶段可能希望从 SRAM 启动加快调试速度、保护 Flash 寿命。2.4 启动模式选择的小细节在 F1 系列中BOOT1 引脚与 PB2 复用。当从 Flash 启动时BOOT00BOOT1 的电平不影响启动方式因此 PB2 可以正常作为普通 GPIO 使用。但在某些特殊设计中如果 BOOT0 误被拉高就可能导致芯片进入非预期启动模式程序无法正常运行。这是硬件设计时需要留意的地方。3. 启动过程的整体流程3.1 从复位到 main 函数的完整链条STM32 的启动过程可以拆成两个大阶段硬件阶段和软件阶段。下面以从 Flash 启动为例列出完整的执行路径。整体流程如下系统上电或复位CPU 从 0x00000000 地址读取初始堆栈指针 SP 的值CPU 从 0x00000004 地址读取复位中断向量也就是 Reset_Handler 的入口地址CPU 跳转到 Reset_Handler 执行Reset_Handler 中首先调用 SystemInit 函数完成系统时钟等基础初始化随后跳转到 __main__main 完成 C 运行环境的初始化包括 RW 数据拷贝、ZI 数据清零、堆栈初始化等最后跳转到 main 函数进入用户 C 程序。3.2 为什么 CPU 从 0x00000000 开始取指这涉及 ARM Cortex-M 处理器的设计。Cortex-M3/M4 内核在复位后会从地址 0x00000000 获取栈顶地址从 0x00000004 获取复位向量。这个地址可以称为“别名映射”机制。在 STM32 上Flash 的起始地址是 0x08000000但芯片内部通过地址映射将 0x08000000 也映射到了 0x00000000 区域。因此从 Flash 启动时CPU 访问 0x00000000 实际上就是访问 0x08000000 处的数据。3.3 为什么启动文件必须放在 Flash 最开头因为 CPU 复位后必须从固定位置读取 SP 和 Reset_Handler 地址所以编译链接时必须把中断向量表放在 Flash 起始地址 0x08000000 处。Keil 工程中的 IROM1 起始地址默认就是 0x08000000这正好对应 Flash 的起始地址。如果代码中改变了中断向量表的偏移比如使用 IAP 时把 App 放在 0x08008000那还需要在 C 代码中调用SCB-VTOR 0x08008000;来重新设置向量表偏移否则中断无法正常跳转。3.4 一张图看懂启动流程不用画复杂的时序图用文字流程就能清晰表达上电/复位 ↓ 读取 0x00000000 → 得到初始 SP ↓ 读取 0x00000004 → 得到 Reset_Handler 地址 ↓ 跳转 Reset_Handler ↓ 调用 SystemInit配置时钟 ↓ 跳转 __mainC 环境初始化 ↓ 进入 main()4. 启动文件 startup 逐段拆解4.1 启动文件的位置与命名规则以 STM32F103 系列为例Keil 工程中通常会有一个汇编启动文件文件名类似startup_stm32f10x_hd.sstartup_stm32f10x_md.sstartup_stm32f10x_ld.s这里的 hd、md、ld 分别对应高密度、中等密度、低密度芯片不同容量芯片的 Flash 和 SRAM 大小不同启动文件里定义的堆栈大小、向量表长度也会略有区别。对 STM32F103C8T6 这种中等容量芯片使用 startup_stm32f10x_md.s 即可。4.2 栈和堆的定义启动文件开头会定义栈大小和堆大小例如下面这段经典代码; 文件路径工程目录/startup_stm32f10x_md.s ; 栈大小配置 Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN3 Stack_Mem SPACE Stack_Size __initial_sp ; 堆大小配置 Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit定义要点如下EQU相当于 C 语言里的#define定义一个常量Stack_Size EQU 0x00000400表示栈大小为 1KBSPACE用于分配一段连续内存空间__initial_sp是栈顶地址会放在中断向量表的第一个位置堆用于动态内存分配比如malloc函数就会用到堆空间。在 C 代码中可以通过修改Stack_Size和Heap_Size的值来调整栈和堆的大小但要注意不要超过芯片 SRAM 的总容量。4.3 中断向量表栈顶地址之后紧接着是中断向量表。启动文件里会列出芯片支持的所有中断入口每一个入口占用 4 字节存放对应中断服务函数的地址。中断向量表的前两行非常关键AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD __initial_sp ; 栈顶地址 DCD Reset_Handler ; 复位中断向量 DCD NMI_Handler ; NMI DCD HardFault_Handler ; 硬件错误 DCD MemManage_Handler ; 存储管理错误 DCD BusFault_Handler ; 总线错误 DCD UsageFault_Handler ; 用法错误 ; 后面还有更多中断向量DCD指令用于定义一个 32 位的数据。所以第一行DCD __initial_sp定义了栈顶地址第二行DCD Reset_Handler定义了复位中断入口地址。当 STM32 复位后CPU 会从这个向量表中取出前两个数据完成初始 SP 和 Reset_Handler 地址的加载。4.4 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 的地址并跳转执行。BLX指令会跳转到指定地址同时保存返回地址BX指令会跳转但不返回。SystemInit 由 STM32 标准外设库或 HAL 库提供它的主要任务是配置 Flash 等待周期、设置系统时钟、使能 FPU如果内核支持等。在 F1 系列中SystemInit 会把系统时钟初始化为 72MHz。__main并不是用户写的 main 函数而是 C 编译器提供的一段初始化代码。它负责把 RW 段已初始化全局变量从 Flash 拷贝到 SRAM把 ZI 段未初始化全局变量清零初始化堆栈调用用户 main 函数。因此用户 C 代码中的全局变量在 main 执行之前就已经完成了初始化。4.5 为什么启动文件需要使用汇编很多初学者会问为什么启动文件不用 C 语言编写主要原因有两个C 语言运行环境本身还没有建立。C 程序依赖全局变量初始化、栈指针设置等条件而这些条件需要在进入 C 环境之前准备好形成一个“先有鸡还是先有蛋”的问题。所以必须用一段不依赖 C 运行时的汇编代码来完成初始引导。中断向量表需要精确地按照 ARM 架构布局放在 Flash 起始地址汇编代码可以更直接地控制地址和段属性。5. 从 Flash 启动时的内存映射细节5.1 STM32F103 内核存储区域以 STM32F103C8T6 为例它内置 64KB Flash 和 20KB SRAM。在 STM32 的存储映射中这些外设和存储单元都有固定的地址区域地址范围用途0x00000000 - 0x0007FFFF别名映射区可通过 BOOT 引脚映射到不同存储介质0x08000000 - 0x0800FFFF内部 Flash 区0x1FFFF000 - 0x1FFFF7FF系统存储器区0x20000000 - 0x20004FFF内部 SRAM 区0x40000000 - 0x5FFFFFFF外设寄存器区0xE0000000 - 0xE00FFFFFCortex-M3 内核私有外设区当 BOOT00即从 Flash 启动时0x00000000 区域就会映射到 0x08000000。所以在调试器里查看 0x00000000 地址和查看 0x08000000 地址数据内容是相同的。5.2 Flash 中代码的排布方式编译链接后生成的 bin 或 hex 文件在 Flash 中的排布顺序大致如下Flash 偏移内容0x08000000初始栈顶地址0x08000004Reset_Handler 地址0x08000008NMI_Handler 地址0x0800000CHardFault_Handler 地址...其他中断向量向量表之后代码段、只读数据段后续区域RW 数据初始值、初始化数据拷贝源这个排布顺序是在链接脚本中定义的。Keil 工程中可以通过 Options for Target → Linker 配置起始地址和大小。5.3 SRAM 中数据的初始化过程当程序启动后RW 段数据会从 Flash 拷贝到 SRAMZI 段数据会被清零。这个过程由 __main 完成。举个简单例子// 已初始化全局变量存放在 RW 段 int g_count 100; // 未初始化全局变量存放在 ZI 段 int g_buffer[64];在 C 环境中g_count的初始值 100 保存在 Flash 的只读区域程序启动时被拷贝到 SRAM 中g_buffer的内存空间在启动时会被清零。因此main 函数第一行代码执行前这些变量已经处于可用状态。如果对启动过程不熟悉可能很难理解为什么“全局变量会自动初始化”实际上功劳就在 __main。6. 用调试器观察启动过程6.1 Keil 中的启动观察方式在 Keil MDK 中可以直接通过调试器观察启动过程的每一步。具体操作如下编译工程点击 Debug 进入仿真模式打开 Registers 窗口观察 R13SP和 R15PC寄存器打开 Disassembly 窗口查看反汇编代码打开 Memory 窗口输入0x08000000查看 Flash 开头的向量表内容。刚进入仿真时PC 指针通常停留在 Reset_Handler 附近。此时可以看到 SP 已经被赋为启动文件中的__initial_sp。接着单步执行可以依次看到跳转到 SystemInit跳转到 __main进入用户 main。6.2 查看向量表数据在 Keil 的 Memory 窗口中输入0x08000000前 8 个字节通常能看到类似下面的数据0x08000000: 00 04 00 20 CD 11 00 08解释0x20000400初始栈顶地址也就是 Stack_Size 定义后生成的 __initial_sp0x080011CDReset_Handler 的入口地址这个值会因工程不同而变化。如果你的工程编译后看到的地址不是这样可以对应调整分析思路。重点是要能确认 Flash 起始位置的第一个 32 位数据是合法的 SRAM 地址第二个 32 位数据是 Flash 区域内的代码地址。6.3 常见观察结果对应分析观察现象说明SP 等于启动文件定义的栈顶地址向量表第一个数据正确PC 停在 Reset_Handler已进入启动流程单步后 PC 进入 SystemInit正常执行时钟初始化单步后 PC 进入 __main正初始化 C 运行环境单步后 PC 进入 main启动流程完成如果 SP 不是 SRAM 地址或者 PC 没有进入正常流程说明向量表可能被破坏或者下载地址不正确。7. 启动过程中的常见问题与排查思路7.1 程序下载后没有运行现象问题现象常见原因解决思路程序下载成功但无反应BOOT0 引脚配置错误确认 BOOT0 为低电平从 Flash 启动程序下载成功但无反应芯片没有正确复位检查复位电路复位引脚是否被拉低程序下载成功但无反应代码编译地址错误检查 IROM1 起始地址是否为 0x08000000程序下载成功但无反应时钟配置死循环检查 SystemInit 中 PLL 配置是否超出芯片范围7.2 仿真时程序跑飞或者进入 HardFault如果是仿真时程序停在 HardFault_Handler常见原因有栈溢出导致堆栈指针指向非法地址函数指针调用错误访问了非法外设地址中断服务函数没有正确实现。排查顺序建议先查看 Call Stack 窗口定位进入 HardFault 之前的调用关系查看 Fault Reports 或寄存器中的 SCB-CFSR、BFAR、MMFAR 寄存器检查 Stack Size 是否满足当前工程的调用深度检查是否有数组越界写入。7.3 修改堆栈大小后程序异常有些工程中开发者调整了启动文件里的 Stack_Size 却仍然出问题原因可能是堆栈大小超过 SRAM 总容量修改后没有重新编译启动文件修改的启动文件并没有被工程实际使用。检查方法在工程中右键 startup 文件确认它参与了编译在 .map 文件中查看栈顶地址是否在 SRAM 范围内。7.4 基于 IAP 的程序无法跳转使用 IAP 时Bootloader 跳转到 App 后如果程序无法运行绝大多数原因是App 的中断向量表没有设置偏移跳转前没有关闭全局中断App 的起始地址与链接地址不一致。在 App 工程中main 函数最开始需要调用// 设置中断向量表偏移0x08008000 为 App 起始地址 SCB-VTOR 0x08008000;同时App 工程的 IROM1 起始地址也要改为 0x08008000。8. 面试高频追问与答题框架8.1 追问一STM32 复位后从哪里开始执行这是一个基础题但要注意回答层次。推荐回答STM32 复位后CPU 从 0x00000000 地址读取初始栈指针从 0x00000004 读取复位向量随后跳转到 Reset_Handler。Reset_Handler 会调用 SystemInit 初始化时钟然后跳转到 __main最终进入 main 函数。如果从 Flash 启动0x00000000 会映射到 0x08000000 区域。8.2 追问二SystemInit 函数做了什么回答核心点SystemInit 由 ST 的固件库提供主要配置系统时钟包括 HSE、PLL、Flash 等待周期等在标准库中SystemInit 会把 STM32F103 的时钟配置为 72MHz在 HAL 库中SystemInit 除了配置时钟还会配置 Flash 和 FPU 等相关选项。8.3 追问三__main 和 main 有什么区别这里是最容易踩坑的地方。推荐回答__main 是编译器提供的 C 运行时初始化入口不是用户 main 函数。它负责把 RW 段从 Flash 拷贝到 SRAM把 ZI 段清零然后才调用用户 main 函数。所以用户 main 函数开始执行时全局变量已经完成初始化。8.4 追问四如何查看堆栈使用了多少回答方法编译后生成 .map 文件查看 Stack 相关信息使用仿真器的 Stack View 窗口观察最大栈深度也可以在调试中设置栈区域填充固定模式检查被覆盖的水印。8.5 追问五启动文件能不能用 C 写这个问题考察对嵌入式底层原理的理解。推荐回答理论上启动的第一条指令不能依赖 C 运行时因为此时 C 环境尚未建立比如栈指针还没有正确设置。因此启动文件通常用汇编实现确保在 C 运行环境建立之前完成最小初始化。当 C 环境就绪后剩余工作就可以交给 C 代码了。8.6 面试答题框架总结如果面试官给你 2 到 3 分钟时间回答“STM32 启动过程”建议按以下顺序组织语言先说启动模式强调产品正常从 Flash 启动再讲复位后 CPU 取指过程说明 Reset_Handler 的作用说明 SystemInit 和 __main 的分工最后落到 main 函数。这种回答方式逻辑清晰从硬件到软件从底层到上层面试官很容易跟上思路。9. 工程实践建议9.1 合理设置堆栈大小启动文件中的 Stack_Size 和 Heap_Size 并不是越大越好。栈太小函数调用层级深时会溢出栈太大会浪费宝贵的 SRAM工程中如果大量使用递归、大型局部数组需要评估栈空间如果使用 RTOS每个任务都有自己的栈任务栈由 RTOS 配置但与系统栈相互独立。建议根据实际需求调整并留有安全余量而不是直接改成一个很大的数值。9.2 注意 BOOT 引脚设计在产品硬件设计中BOOT0 引脚最好通过电阻固定下拉到 GND避免因干扰导致进入非预期启动模式。如果产品需要支持串口 ISP 升级可以预留测试点或跳线帽便于工厂烧录和现场维护。9.3 软件复位与看门狗配合在启动过程中如果 SystemInit 中配置时钟等待 PLL 就绪的时间过长同时看门狗已经启动可能会在看门狗超时前无法完成初始化导致系统反复复位。需要合理设置看门狗喂狗时机。9.4 IAP 升级时的注意点使用 IAP 方案时Bootloader 和 App 的启动流程都需要单独理解Bootloader 启动后根据标志决定是否跳转 AppApp 启动后需要重新设置中断向量表偏移跳转前关中断、设置 MSP都是工程中常见的注意点。强烈建议先在开发板上验证 Bootloader 和 App 的最小跳转流程再考虑复杂功能。9.5 调试启动过程时善用断点和寄存器窗口遇到启动类问题时不要盲猜。优先做以下操作查看 PC 当前停在哪个函数查看栈顶地址是否合法查看 Flash 起始地址的前 8 字节单步执行观察跳转路径。这些操作能快速缩小问题范围避免浪费大量时间。10. 总结STM32 的启动过程并没有那么神秘核心就是“从固定地址取栈顶和复位向量跳转启动文件初始化时钟和 C 环境最后进入 main”。但它又不只是一段流程而是贯穿单片机整个运行机制的主线。理解了启动过程才能真正看懂中断向量表、链接脚本、IAP 跳转、堆栈分配这些嵌入式开发里的关键概念。面试中遇到这道题不需要背长篇幅的答案按“启动模式 → 取指过程 → Reset_Handler → SystemInit → __main → main”这个逻辑来讲基本可以覆盖面试官想考察的大部分内容。如果还能结合自己调试过程中遇到的启动失败场景会更有说服力。希望这篇文章能帮你把 STM32 启动过程彻底搞清楚。如果你正在准备面试建议把启动文件的每一段汇编都亲自读一遍再用调试器跑一遍很多疑问会在动手之后自然解开。