STM32 启动到运行的完整流程

STM32 的启动流程本质是硬件复位→启动模式选择→启动文件执行→系统初始化→应用程序执行 的递进过程,我们按阶段拆解:

阶段 1:硬件上电复位(硬件层)

这是 STM32 启动的第一步,完全由硬件电路和芯片内部逻辑控制,无代码参与。

  1. 上电与复位触发

    • 当 STM32 的 VDD/VCORE 等电源域电压达到稳定值后,芯片内部的电源监控器(PVD)确认电源正常。
    • 复位信号(NRST 引脚)有效(低电平)时,芯片进入复位状态,所有寄存器恢复默认值。
  2. 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)同理

启动文件关键步骤解释

  1. 栈 / 堆初始化 :定义栈的大小和栈顶地址(复位后芯片首先加载栈顶地址到 SP 寄存器),堆用于 C 语言的动态内存分配。
  2. 中断向量表 :核心是 "栈顶地址 + 复位中断函数地址",芯片复位后会跳转到Reset_Handler执行。
  3. Reset_Handler 执行逻辑
    • 复制 Data 段:Flash 中的初始化全局变量(如int a=10;)复制到 SRAM(因为 SRAM 可读写,Flash 只读)。
    • 清零 BSS 段:未初始化的全局变量(如int b;)或初始化为 0 的变量,全部置 0(BSS 段默认无数据,只需清零)。
    • 调用SystemInit:初始化系统时钟、向量表偏移等核心配置。
    • 调用__main:CMSIS 标准函数,最终跳转到用户编写的main函数。
阶段 3:SystemInit 函数执行(库 / 固件层)

SystemInit函数在system_stm32f1xx.c中实现,是 STM32 的系统级初始化,核心作用:

  1. 时钟树配置:将系统时钟(SYSCLK)从默认的内部 8MHz RC 振荡器(HSI)切换到外部晶振(HSE)或倍频后的高频时钟(如 72MHz)。
  2. 向量表偏移配置:如果程序不是从默认 Flash 地址启动(如 Bootloader 场景),调整中断向量表的地址偏移。
  3. 其他底层配置:如总线分频、外设时钟使能等(不同系列略有差异)。
阶段 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. 根据中断 / 异常的编号,计算其在向量表中的偏移地址(如复位异常是编号 1,偏移 0x04);
  2. 从该偏移地址读取对应的函数入口地址;
  3. 自动跳转到该地址执行处理函数。

这个过程是硬件级的,速度极快(无需软件循环查表),保证了中断响应的实时性 ------ 这也是嵌入式系统对中断的核心要求。

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;)全部清零(否则这些变量会是随机值,导致程序逻辑混乱)。

三、举个通俗的例子

把 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 时:

  1. 这些变量的初始值(10、3.14) 会被一起烧录到 Flash 中(因为 Flash 掉电不丢,能保存初始值);
  2. 但 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 段复制过程

用具体的地址和数值,直观看这个复制操作:

  1. 烧录后(Flash 中)

    • Flash 地址 0x08000100:存储a的初始值 10(二进制:00001010);
    • 编译器记录:a的运行地址(SRAM)是 0x20000100。
  2. 复位后(Reset_Handler 执行)

    • CPU 读取 Flash 0x08000100 的值(10);
    • 把 10 写入 SRAM 0x20000100 的地址;
    • 此后程序中所有对a的操作,都指向 SRAM 0x20000100。
  3. 程序运行时

    • 执行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),中断就能正常响应了。

相关推荐
yuanmenghao几秒前
CAN系列 — (4) Radar Header 报文:为什么它是 MCU 感知周期的“锚点”
网络·单片机·自动驾驶·信息与通信
飞睿科技9 分钟前
乐鑫ESP32-S3-BOX-3,面向AIoT与边缘智能的新一代开发套件
人工智能·嵌入式硬件·esp32·智能家居·乐鑫科技
Y1rong17 分钟前
STM32之SPI
stm32·单片机·嵌入式硬件
p666666666819 分钟前
STM32(基于 ARM Cortex-M 内核)中函数调用栈帧的开辟 销毁过程
arm开发·stm32·嵌入式硬件
码咔吧咔27 分钟前
DMA1和DMA2是什么?DMA总线与Dcode总线有区别?SDIO又是干嘛的,system干嘛的?总线矩阵干嘛的?
stm32·单片机·嵌入式硬件
小郭团队31 分钟前
未来PLC会消失吗?会被嵌入式系统取代吗?
c语言·人工智能·python·嵌入式硬件·架构
Aaron158831 分钟前
全频段SDR干扰源模块设计
人工智能·嵌入式硬件·算法·fpga开发·硬件架构·信息与通信·基带工程
The_superstar638 分钟前
视觉模块与STM32进行串口通讯(匠心制作)
stm32·嵌入式硬件·mongodb·计算机视觉·串口通讯·视觉模块
Dillon Dong1 小时前
STM32嵌入式:如何使用VSCode EIDE来获取flash块数据并转换成可视化的数据 来判断源头数据是否错误
vscode·stm32·嵌入式硬件
恒锐丰小吕1 小时前
屹晶微 EG3113 600V高压、2A/2.5A驱动、自举半桥栅极驱动芯片技术解析
嵌入式硬件·硬件工程