STM32 的启动流程本质是硬件复位→启动模式选择→启动文件执行→系统初始化→应用程序执行 的递进过程,我们按阶段拆解:
阶段 1:硬件上电复位(硬件层)
这是 STM32 启动的第一步,完全由硬件电路和芯片内部逻辑控制,无代码参与。
-
上电与复位触发
- 当 STM32 的 VDD/VCORE 等电源域电压达到稳定值后,芯片内部的电源监控器(PVD)确认电源正常。
- 复位信号(NRST 引脚)有效(低电平)时,芯片进入复位状态,所有寄存器恢复默认值。
-
BOOT 引脚决定启动地址 STM32 通过上电时BOOT0、BOOT1 引脚的电平 选择程序的启动存储介质,核心规则(以 STM32F103 为例):
BOOT1 BOOT0 启动模式 启动地址 0 0 闪存(Flash)启动(常用) 0x08000000 0 1 系统存储器(ISP 下载) 0x1FFFF000 1 1 SRAM 启动(调试) 0x20000000 - 复位完成后,芯片会从选定的启动地址 读取第一个数据(栈顶地址)和第二个数据(复位中断向量)。
阶段 2:启动文件执行(汇编层)
启动文件(如startup_stm32f103xe.s)是连接硬件和 C 语言程序的桥梁,由汇编编写,核心作用是初始化运行环境,最终跳转到main函数。以下是启动文件的核心逻辑(关键片段 + 解释):
armasm
; 1. 定义栈大小(Stack_Size)和堆大小(Heap_Size)
Stack_Size EQU 0x00000400 ; 栈大小1KB
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp ; 栈顶地址(复位后第一个读取的地址)
; 2. 定义堆(用于malloc动态内存)
Heap_Size EQU 0x00000200 ; 堆大小512B
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
; 3. 中断向量表(Vector Table)
AREA RESET, DATA, READONLY
__Vectors ; 向量表起始地址(与启动地址对应)
DCD __initial_sp ; 0: 栈顶地址
DCD Reset_Handler ; 1: 复位中断处理函数(核心)
DCD NMI_Handler ; 2: 不可屏蔽中断
DCD HardFault_Handler ; 3: 硬件错误中断
; ... 省略其他中断向量(如定时器、串口等)
; 4. 复位中断处理函数(Reset_Handler)------ 启动流程核心
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
; 步骤1: 复制数据段(Data段)从Flash到SRAM
LDR R0, =__initial_sp
LDR R1, =__data_start
LDR R2, =__data_end
MOVS R3, #0
B LoopCopyDataInit
CopyDataInit:
LDR R4, [R0], #4
STR R4, [R1], #4
LoopCopyDataInit:
CMP R1, R2
BCC CopyDataInit
; 步骤2: 清零BSS段(未初始化的全局变量)
LDR R1, =__bss_start
LDR R2, =__bss_end
MOVS R3, #0
B LoopFillZerobss
FillZerobss:
STR R3, [R1], #4
LoopFillZerobss:
CMP R1, R2
BCC FillZerobss
; 步骤3: 调用系统初始化函数(SystemInit)
BL SystemInit
; 步骤4: 调用__main(CMSIS标准函数,最终跳转到main)
BL __main
ENDP
; 5. 弱定义默认中断处理函数(用户未定义时执行)
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . ; 死循环
ENDP
; ... 其他中断处理函数(如HardFault_Handler)同理
启动文件关键步骤解释:
- 栈 / 堆初始化 :定义栈的大小和栈顶地址(复位后芯片首先加载栈顶地址到 SP 寄存器),堆用于 C 语言的动态内存分配。
- 中断向量表 :核心是 "栈顶地址 + 复位中断函数地址",芯片复位后会跳转到
Reset_Handler执行。 - Reset_Handler 执行逻辑 :
- 复制 Data 段:Flash 中的初始化全局变量(如
int a=10;)复制到 SRAM(因为 SRAM 可读写,Flash 只读)。 - 清零 BSS 段:未初始化的全局变量(如
int b;)或初始化为 0 的变量,全部置 0(BSS 段默认无数据,只需清零)。 - 调用
SystemInit:初始化系统时钟、向量表偏移等核心配置。 - 调用
__main:CMSIS 标准函数,最终跳转到用户编写的main函数。
- 复制 Data 段:Flash 中的初始化全局变量(如
阶段 3:SystemInit 函数执行(库 / 固件层)
SystemInit函数在system_stm32f1xx.c中实现,是 STM32 的系统级初始化,核心作用:
- 时钟树配置:将系统时钟(SYSCLK)从默认的内部 8MHz RC 振荡器(HSI)切换到外部晶振(HSE)或倍频后的高频时钟(如 72MHz)。
- 向量表偏移配置:如果程序不是从默认 Flash 地址启动(如 Bootloader 场景),调整中断向量表的地址偏移。
- 其他底层配置:如总线分频、外设时钟使能等(不同系列略有差异)。
阶段 4:进入应用程序(C 语言层)
当SystemInit执行完成后,__main函数会最终跳转到你编写的main函数,此时:
- SRAM 中的 Data 段、BSS 段已初始化完成;
- 系统时钟已配置到目标频率;
- 栈 / 堆已就绪;
- 芯片进入用户逻辑执行阶段(如初始化 GPIO、串口、定时器,实现业务功能)。
中断向量表详细介绍
一、中断向量表(Vector Table)的核心作用
中断向量表是 STM32(基于 ARM Cortex-M 内核)中硬件与软件之间的 "中断 / 异常入口索引表",本质是一段存储在指定地址的连续内存(通常在 Flash 起始地址),核心作用有 3 个:
1. 存储所有中断 / 异常的处理函数入口地址
STM32 运行中会遇到两类 "需要特殊处理的事件":
- 异常:芯片核心层面的事件(如复位、硬件错误、栈溢出等);
- 中断:外设触发的事件(如串口接收数据、定时器超时、按键外部中断等)。
中断向量表把每一种事件(中断 / 异常)和对应的处理函数地址一一对应,比如:
| 向量表偏移 | 对应事件 | 存储的地址 | 作用 |
|---|---|---|---|
| 0x00 | 栈顶地址 | __initial_sp |
给 SP 寄存器加载栈顶位置 |
| 0x04 | 复位异常 | Reset_Handler |
复位后执行的第一个函数 |
| 0x08 | 不可屏蔽中断 | NMI_Handler |
处理最高优先级的硬件异常 |
| 0x0C | 硬件错误中断 | HardFault_Handler |
处理核心运行错误 |
| 0x10 | 串口 1 中断 | USART1_IRQHandler |
处理串口 1 的收发事件 |
简单理解:中断向量表就像 "事件 - 处理函数" 的字典,硬件能快速查到 "发生某个事件该执行哪个函数"。
2. 硬件级快速响应中断 / 异常
当某个中断 / 异常发生时,Cortex-M 内核会自动完成以下操作(无需软件干预):
- 根据中断 / 异常的编号,计算其在向量表中的偏移地址(如复位异常是编号 1,偏移 0x04);
- 从该偏移地址读取对应的函数入口地址;
- 自动跳转到该地址执行处理函数。
这个过程是硬件级的,速度极快(无需软件循环查表),保证了中断响应的实时性 ------ 这也是嵌入式系统对中断的核心要求。
3. 复位时的 "启动入口"(最核心的初始化作用)
对 STM32 启动流程来说,中断向量表的第一个作用不是处理普通中断,而是作为复位后的启动入口:
- 芯片复位后,首先读取向量表第一个位置的值(栈顶地址),加载到 SP(栈指针)寄存器,完成栈的初始化(没有栈,C 语言函数无法调用);
- 接着读取向量表第二个位置 的值(
Reset_Handler地址),跳转到该函数执行 ------ 这是整个启动流程的起点。
二、为什么复位后必须跳转到Reset_Handler
1. 硬件逻辑的强制要求
Cortex-M 内核的复位机制规定:复位完成后,内核会自动读取向量表第二个位置的地址并跳转------ 而这个位置在启动文件中被固定设置为Reset_Handler(见之前的启动文件代码:DCD Reset_Handler)。
2. Reset_Handler是 C 语言程序运行的 "前置条件"
如果跳过Reset_Handler直接执行main函数,你的 C 语言程序会立刻崩溃,因为它完成了两个核心初始化:
- 初始化内存区域:
- 把 Flash 中存储的初始化全局变量(Data 段,如
int a=10;)复制到 SRAM(Flash 只读,必须放到 SRAM 才能读写); - 把未初始化的全局变量(BSS 段,如
int b;)全部清零(否则这些变量会是随机值,导致程序逻辑混乱)。
- 把 Flash 中存储的初始化全局变量(Data 段,如
三、举个通俗的例子
把 STM32 比作一台电脑:
- 中断向量表 = 电脑的 "快捷方式列表"(系统崩溃点 "重启"、软件报错点 "任务管理器"、外设触发点 "弹窗处理" 都对应固定的处理程序);
- 复位操作 = 电脑按 "电源键" 开机;
Reset_Handler= 电脑开机后的 "BIOS 初始化"(加载内存、配置硬件、检查系统);main函数 = 电脑 BIOS 完成后进入的 "Windows 系统"(用户操作的界面)。
不可能跳过 BIOS 直接进入 Windows,就像 STM32 不能跳过Reset_Handler直接执行main------ 而 BIOS 的入口地址,就存在于 "快捷方式列表"(中断向量表)中,是开机时硬件自动读取的。
STM32 为什么要把 Flash 中存储的带初始值的全局变量复制到 SRAM 中
一、Flash 和 SRAM 的核心差异
STM32 有两个核心存储区域,它们的特性决定了 "必须复制 Data 段",用通俗的比喻理解:
| 存储区域 | 硬件特性 | 类比(电脑) | 程序运行中的角色 |
|---|---|---|---|
| Flash | 只读(擦写需特殊操作)、掉电数据不丢、访问速度较慢 | 电脑硬盘 | 存储 "固化的程序和数据"(烧录后永久保存) |
| SRAM | 可读写、掉电数据丢失、访问速度极快 | 电脑内存(RAM) | 程序运行时 "实时操作数据" 的区域 |
二、Data 段是什么?为什么会存在于 Flash 中?
Data段(数据段)是编译器为有初始值的全局 / 静态变量分配的存储区域,比如:
// 属于Data段:有初始值的全局变量
int a = 10;
static float b = 3.14;
当你把程序烧录到 STM32 时:
- 这些变量的初始值(10、3.14) 会被一起烧录到 Flash 中(因为 Flash 掉电不丢,能保存初始值);
- 但 Flash 是只读 的 ------ 你可以读取 10 这个值,但无法直接修改
a的值(比如执行a = 20;),因为硬件不允许对 Flash 进行随机写操作(Flash 写需要先擦除,且速度极慢,不适合程序运行时的实时修改)。
三、为什么必须把 Data 段从 Flash 复制到 SRAM?
程序运行的核心需求是 "能读写变量",而 Flash 的只读特性满足不了这个需求,因此Reset_Handler必须做这一步复制:
场景 1:不复制的后果(程序崩溃 / 逻辑错误)
如果直接在 Flash 中操作int a=10;:
- 你执行
a = a + 5;时,CPU 试图往 Flash 的地址写 15,但 Flash 只读,硬件会触发硬件错误中断(HardFault),程序直接卡死; - 即使没触发中断,
a的值也永远是 10,无法修改,你的业务逻辑(比如用a计数)完全失效。
场景 2:复制后的正常运行
Reset_Handler把 Flash 中a的初始值 10,复制到 SRAM 的一个指定地址(比如 0x20000000):
- 程序运行时,所有对
a的操作(读、写、运算)都是针对SRAM 中的副本; - 执行
a = 20;时,修改的是 SRAM 地址 0x20000000 的值,速度快且可正常读写,变量的功能完全符合你的预期。
四、可视化理解 Data 段复制过程
用具体的地址和数值,直观看这个复制操作:
-
烧录后(Flash 中):
- Flash 地址 0x08000100:存储
a的初始值 10(二进制:00001010); - 编译器记录:
a的运行地址(SRAM)是 0x20000100。
- Flash 地址 0x08000100:存储
-
复位后(Reset_Handler 执行):
- CPU 读取 Flash 0x08000100 的值(10);
- 把 10 写入 SRAM 0x20000100 的地址;
- 此后程序中所有对
a的操作,都指向 SRAM 0x20000100。
-
程序运行时:
- 执行
a = a + 5;→ SRAM 0x20000100 的值变为 15; - 执行
printf("%d", a);→ 读取 SRAM 0x20000100 的 15,输出正确结果。
- 执行
五、补充:和 BSS 段的对比(帮你区分)
很多新手会混淆 Data 段和 BSS 段,这里简单对比,加深理解:
| 段名 | 变量类型 | 存储处理方式 | 原因 |
|---|---|---|---|
| Data 段 | 有初始值的全局 / 静态变量 | 从 Flash 复制到 SRAM | 初始值非 0,需要从 Flash 读取 |
| BSS 段 | 无初始值 / 初始值为 0 的变量 | 直接在 SRAM 中清零(无需从 Flash 复制) | 初始值是 0,直接置 0 即可 |
比如:
int a = 10; // Data段:从Flash复制10到SRAM
int b; // BSS段:SRAM中直接置0
int c = 0; // BSS段:SRAM中直接置0(无需复制)
Bootloader导致中断向量表迁移的问题
一、为什么需要 "向量表偏移"?(Bootloader 场景的核心矛盾)
Bootloader 是嵌入式中常见的 "引导程序",作用是先运行一小段程序(比如实现固件升级),再跳转到真正的应用程序(App)运行。此时 Flash 中会存在两个程序:
| 程序 | 占用 Flash 地址范围 | 向量表位置(程序自身的) |
|---|---|---|
| Bootloader | 0x08000000 ~ 0x08008000 | 0x08000000(自身起始地址) |
| App 程序 | 0x08008000 ~ 0x0807FFFF | 0x08008000(自身起始地址) |
此时会出现一个核心问题:
- Bootloader 运行时,内核默认从
0x08000000读向量表,能正常响应中断; - 但当 Bootloader 跳转到 App 程序(0x08008000)运行后,App 程序的中断向量表在
0x08008000,而内核仍然会去默认的 0x08000000 地址找向量表------ 这里存的是 Bootloader 的向量表,不是 App 的!
后果:App 程序触发任何中断(比如串口、定时器)时,内核会找到错误的处理函数地址,要么执行错误代码,要么触发 HardFault 崩溃。
举个通俗例子:
- 你家小区默认门牌号从 1 号楼开始(0x08000000),Bootloader 住在 1 号楼,App 住在 2 号楼(0x08008000);
- 快递员(内核)默认只去 1 号楼找 "收快递的人"(中断处理函数),但 App 的快递收件人在 2 号楼,不告诉快递员地址偏移,快递就送错了。
二、向量表偏移配置的本质
"向量表偏移配置" 的核心是:告诉 STM32 内核 "中断向量表不在默认的 0x08000000 了,现在要去新的地址找"。
这个配置是通过修改 ARM Cortex-M 内核的SCB->VTOR(向量表偏移寄存器)实现的:
SCB:系统控制块(System Control Block),是内核的核心配置寄存器组;VTOR:Vector Table Offset Register(向量表偏移寄存器),存储 "向量表起始地址相对于默认基地址的偏移量"。
公式:
plaintext
实际向量表地址 = 向量表基地址 + VTOR寄存器的值
(注:STM32 中 Flash 的基地址固定为 0x08000000,所以只需配置 VTOR 为偏移量即可)
三、实际配置示例(Bootloader 跳转到 App 后)
假设 App 程序的向量表起始地址是0x08008000,在 App 程序的初始化代码中,必须先配置向量表偏移,再开启中断:
c
运行
#include "stm32f1xx.h"
// App程序的向量表起始地址(根据实际分配的地址修改)
#define APP_VECTOR_TABLE_ADDR 0x08008000
int main(void)
{
// 第一步:配置向量表偏移(核心操作)
// 1. 解锁SCB寄存器(部分STM32系列需要,F1系列可省略)
// 2. 设置VTOR寄存器为App向量表的偏移量
SCB->VTOR = APP_VECTOR_TABLE_ADDR; // 直接赋值新的向量表起始地址
// 第二步:正常初始化外设、开启中断
HAL_UART_Init(&huart1);
HAL_TIM_Base_Start_IT(&htim2);
// 后续业务逻辑...
while(1)
{
// App程序的主循环
}
}
配置后的效果 :内核再触发中断时,会从0x08008000地址读取 App 的向量表,找到对应的中断处理函数(比如USART1_IRQHandler),中断就能正常响应了。