引言:为什么深入理解STM32启动流程很重要?
STM32F103C8T6作为嵌入式开发中最常用的单片机之一,其内部执行原理 和启动流程是理解嵌入式系统底层运行机制的核心。无论是开发Bootloader、调试HardFault异常,还是优化系统启动速度,都需要对这两部分有深入掌握。本文将从硬件架构到软件执行,全方位解析STM32F103C8T6的工作原理,配合代码示例和实战技巧,帮助你彻底搞懂单片机从"上电"到"运行main函数"的全过程。
一、STM32F103C8T6内部执行原理
1.1 核心架构:Cortex-M3内核与哈佛结构
STM32F103C8T6基于ARM Cortex-M3内核 ,采用哈佛架构 ( Harvard Architecture ),将指令存储和 数据存储分离为两条独立总线:
• I-Code总线 :专门用于取指(32位宽),可一次读取两条16位Thumb指令
• D-Code总线 :专门用于数据访问(32位宽),支持字节/半字/字操作
这种架构的优势在于指令读取和数据访问可并行执行 ,大幅提升运行效率。相比传统51单片机的冯·诺依 曼结构(指令和数据共享总线),哈佛结构在高主频下的性能优势尤为明显。

1.2 三级流水线: 指令执行的 " 工厂流水线 "
Cortex-M3内核采用三级流水线 设计,将指令执行分为三个阶段并行处理:
-
取指(Fetch ) :从Flash或指令缓存读取指令,由预取单元(Prefetch Unit)完成,支持指令预取缓冲
-
解码(Decode ) :解析指令操作码和操作数,生成控制信号
-
执行(Execute ) :由ALU(算术逻辑单元)、乘法器等执行运算,访问寄存器或存储器
流水线工作时序 :
• 时钟周期1:指令1取指
• 时钟周期2:指令1解码,指令2取指
• 时钟周期3:指令1执行,指令2解码,指令3取指
这种并行处理使Cortex-M3在72MHz主频下可实现1.25 DMIPS/MHz 的性能(约90 DMIPS )。

1.3 存储器系统: Flash 、 RAM 与地址映射
STM32F103C8T6的存储器资源如下:
• Flash :64KB(地址范围:0x08000000~0x0800FFFF),用于存储程序代码和常量
• SRAM :20KB(地址范围:0x20000000~0x20004FFF),用于存储变量和堆栈
存储器映射规则 :
Cortex-M3支持4GB地址空间,STM32将其划分为多个区域:
• 0x00000000~0x1FFFFFFF:代码区(Flash/系统存储器/SRAM,通过启动模式映射)
• 0x20000000~0x3FFFFFFF:SRAM区
• 0x40000000~0x5FFFFFFF:外设寄存器区(APB1/APB2/AHB外设)
• 0xE0000000~0xE00FFFFF: 内核外设区(NVIC、SysTick等)
1.4 总线架构: AHB 与 APB 总线矩阵
STM32采用多级总线架构 ,通过总线矩阵( Bus Matrix )协调各主设备( CPU 、DMA)对从设备 ( Flash 、SRAM、外设)的访问:
• AHB总线(Advanced High-performance Bus ) :最高72MHz,连接高性能外设(Flash、SRAM、 DMA、LCD控制器等)
• APB1总线 :最高36MHz,连接低速外设(USART2/3、I2C、SPI2等)
• APB2总线 :最高72MHz,连接高速外设(GPIO、USART1、SPI1、ADC等)

1.5 时钟系统: 从晶振到外设的 " 时间管理者 "
STM32的时钟系统是最复杂也最核心的部分之一,支持5种时钟源:
• HSI :内部高速RC振荡器(8MHz,精度±1%)
• HSE :外部高速晶振(4~16MHz,通常接8MHz)
• LSI :内部低速RC振荡器(40kHz,用于独立看门狗)
• LSE :外部低速晶振(32.768kHz,用于RTC)
• PLL :锁相环倍频器(输入可接HSI/2、HSE或HSE/2,倍频2~16倍,最高输出72MHz)
典型时钟树配置 ( HSE=8MHz ):
HSE → PLL输入(不分频)→ PLL倍频9倍 → PLL输出72MHz → 作为SYSCLK(系统时钟)
• AHB分频1 → HCLK=72MHz(CPU主频)
• APB1分频2 → PCLK1=36MHz
• APB2分频1 → PCLK2=72MHz

二、 STM 32F103C8T6 启动流程详解
2.1 启动模式: BOOT 引脚如何决定程序从哪里启动?
STM32的启动模式由BOOT0 和BOOT1 引脚的电平决定,共三种模式:
|---------------|---------------|--------------|--------------|------------------|
| BOOT1 | BOOT0 | 启动模式 | 映射地址 | 用途 |
| X | 0 | 主Flash启动 | 0x08000000 | 正常运行用户程 序(默认模式) |
| 0 | 1 | 系统存储器启动 | 0x1FFFF000 | 通过串口下载程 序(ISP模式) |
| 1 | 1 | SRAM启动 | 0x20000000 | 调试临时程序 (掉电不保存) |
关键细节 :
• 复位时BOOT引脚电平被锁存,修改后需复位生效
• 主Flash启动时,0x00000000地址被映射到0x08000000(Flash首地址)
• 系统存储器启动时,执行ST出厂预置的Bootloader(支持USART1下载)

2.2 复位序列: 上电后 CPU 首先做什么?
当STM32上电或复位( NRST引脚拉低)后,硬件自动执行以下步骤:
-
初始化状态寄存器 :清除中断标志,设置默认优先级
-
读取向量表前两项 :
◦ 从0x00000000读取MSP初始值 (栈顶指针)
◦ 从0x00000004读取复位向量 (Reset_Handler函数地址)
- 跳转执行Reset_Handler :CPU将PC指针设置为复位向量地址,开始执行启动代码
2.3 启动文件解析: startup _ stm 32f10x_ md .s 的秘密
启动文件(汇编编写)是连接硬件复位和C语言环境的桥梁,以 startup_stm32f10x_md .s
(中等容量 设备)为例,主要完成以下工作:
2.3.1 堆栈定义
cs
Stack_Size EQU 0x00000400 ; 栈大小1KB
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size ; 分配栈空间
__initial_sp ; 栈顶地址(栈从高地址向低地址生长)
Heap_Size EQU 0x00000200 ; 堆大小512B
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base ; 堆起始地址
Heap_Mem SPACE Heap_Size ; 分配堆空间
__heap_limit ; 堆结束地址
2.3.2 中断向量表
cs
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; 0: 栈顶指针
DCD Reset_Handler ; 1: 复位中断
DCD NMI_Handler ; 2: NMI中断
DCD HardFault_Handler ; 3: 硬件错误中断
; ... 其他中断向量(共68个)
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors ; 向量表大小
2.3.3 复位处理函数( Reset _ Handler )
复位后执行的第一个函数,负责初始化硬件和C环境:
cs
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit ; 引入系统初始化函数
IMPORT __main ; 引入C库初始化函数
; 1. 设置栈指针(已由硬件读取__initial_sp完成)
; 2. 初始化数据段(.data)
ldr r0, =_sdata ; 数据段目标地址(RAM)
ldr r1, =_edata ; 数据段结束地址
ldr r2, =_sidata ; 数据段源地址(Flash)
movs r3, #0
LoopCopyDataInit:
cmp r0, r1 ; 复制未完成?
ittt lt
ldrlt r4, [r2], #4 ; 从Flash读取数据
strlt r4, [r0], #4 ; 写入RAM
blt LoopCopyDataInit ; 循环复制
; 3. 初始化BSS段(清零)
ldr r2, =_sbss ; BSS段起始地址
ldr r4, =_ebss ; BSS段结束地址
movs r3, #0
LoopFillZerobss:
cmp r2, r4 ; 清零未完成?
itt lt
strlt r3, [r2], #4 ; 写入0
blt LoopFillZerobss ; 循环清零
; 4. 调用SystemInit配置系统时钟
bl SystemInit
; 5. 调用__main初始化C库,最终跳转到main函数
bl __main
ENDP
关键步骤解析 :
• 数据段(.data)复制 :将Flash中存储的已初始化全局变量复制到RAM
• BSS段清零 :将未初始化全局变量在RAM中清零
• SystemInit :配置系统时钟(默认使用HSI,可修改为HSE+PLL=72MHz)
• __main :C库函数,初始化堆和栈,调用全局构造函数(C++),最终跳转到用户
2.4 SystemInit 函数: 时钟配置的核心
SystemInit函数(位于system_stm32f10x.c)负责系统时钟初始化,默认配置如下:
cs
void SystemInit(void) {
/* 复位RCC寄存器到默认状态 */
RCC->CR |= 0x00000001U; // 使能HSI
RCC->CFGR &= 0xF8FF0000U; // 复位时钟配置寄存器
RCC->CR &= 0xFEF6FFFFU; // 关闭HSE、CSS、PLL
RCC->CR &= 0xFFFBFFFFU; // 关闭HSE旁路
RCC->CFGR &= 0xFF80FFFFU; // 复位PLL配置
/* 配置向量表偏移(默认在Flash) */
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; // 向量表在SRAM
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;// 向量表在Flash(0x08000000)
#endif
}
**默认时钟:**HSI(8MHz)作为系统时钟,未启用PLL,如需72MHz需修改SetSysClock函数:
cs
static void SetSysClockTo72(void) {
RCC->CR |= RCC_CR_HSEON; // 使能HSE
while((RCC->CR & RCC_CR_HSERDY) == 0); // 等待HSE就绪
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE; // PLL输入=HSE
RCC->CFGR |= RCC_CFGR_PLLMULL9; // PLL倍频9倍(8MHz*9=72MHz)
RCC->CR |= RCC_CR_PLLON; // 使能PLL
while((RCC->CR & RCC_CR_PLLRDY) == 0); // 等待PLL就绪
FLASH->ACR |= FLASH_ACR_PRFTBE; // 使能Flash预取
FLASH->ACR &= ~FLASH_ACR_LATENCY;
FLASH->ACR |= FLASH_ACR_LATENCY_2; // Flash等待周期=2(72MHz时)
RCC->CFGR |= RCC_CFGR_SW_PLL; // 系统时钟=PLL输出
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); // 等待切换完成
}
三、代码示例与实战解析
3.1 启动流程验证: 通过 LED 观察启动阶段
硬件连接 : LED接PC13(低电平点亮)
代码实现 :
cs
// main.c
#include "stm32f10x.h"
// 延时函数
void Delay(__IO uint32_t nCount) {
while(nCount--) {}
}
int main(void) {
// 使能GPIOC时钟(APB2外设)
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 配置PC13为推挽输出
GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
GPIOC->CRH |= GPIO_CRH_MODE13_0; // 输出模式,最大速度10MHz
while (1) {
GPIOC->ODR ^= GPIO_ODR_ODR13; // 翻转PC13电平
Delay(0xFFFFF); // 延时
}
}
启动阶段分析 :
-
上电后LED不亮(系统初始化阶段)
-
SystemInit执行完毕后,LED开始闪烁(main函数执行)
-
若LED不闪烁,可能是时钟配置错误或启动文件选择不当(需使用 startup_stm32f10x_md .s)
3.2 中断向量表重映射: 从 RAM 启动时的配置
当使用SRAM启动或IAP升级时,需将向量表重映射到RAM:
cs
// 在SystemInit或main中配置
void VectorTableRemap(void) {
// 将Flash中的向量表复制到RAM(0x20000000)
uint32_t *pSrc = (uint32_t*)0x08000000; // Flash向量表起始地址
uint32_t *pDest = (uint32_t*)0x20000000; // RAM向量表起始地址
uint32_t i;
for(i = 0; i < 68; i++) { // 复制68个中断向量
pDest[i] = pSrc[i];
}
// 配置VTOR寄存器(向量表偏移)
SCB->VTOR = 0x20000000; // 向量表基地址=RAM起始地址
}
3.3 启动时间优化: 从 72 ms 到 15 ms 的实战技巧
默认启动时间 :约72ms(含Flash擦写、C库初始化等)
优化方法 :
- 关闭不必要的外设时钟 :
在SystemInit中仅使能必要外设时钟(如GPIO、USART)
- 优化Flash访问 :
使能预取缓冲区(FLASH_ACR_PRFTBE=1),设置正确等待周期(72MHz时=2)
- 跳过C库初始化 :
若不使用全局构造函数,可在启动文件中直接跳转到main:
cs
; 在Reset_Handler中替换bl __main为bl main
bl SystemInit
bl main ; 直接调用main,跳过__libc_init_array
- 使用HSI快速启动 :
若对时钟精度要求不高,使用HSI(8MHz)可避免HSE晶振启动延时
四、常见问题与调试技巧
4.1 启动失败排查清单
|-------------|-----------------------|---------------------------|
| 现象 | 可能原因 | 解决方案 |
| 程序无反应 | BOOT0引脚接高电平(进入 ISP模式) | 将BOOT0接地,复位芯片 |
| HardFault异常 | 栈溢出或非法内存访问 | 增大栈大小(Stack_Size),检 查指针操作 |
|------------|-------------------|-------------------------|
| 现象 | 可能原因 | 解决方案 |
| 时钟配置后死机 | PLL倍频过高(超过72MHz) | 重新计算PLL参数,确保输出 ≤72MHz |
| 全局变量初始化失败 | .data或.bss段地址配置错误 | 检查链接脚本( .ld)中的RAM 地址和大小 |
4.2 使用 ST - Link 调试启动过程
- 查看寄存器状态 :
复位后暂停,查看 SP 是否等于 ___initial_sp, PC 是否指向 Reset_Handler
- 设置断点 :
在 Reset_Handler 、SystemInit、main 函数设置断点,观察执行流程
- 查看内存 :
检查 0x20000000(RAM起始地址)是否已复制.data段数据,.bss段是否清零
总结与扩展
本文详细解析了STM32F103C8T6的内部执行原理(哈佛架构、三级流水线、存储器映射、时钟系统)和启动流程(复位序列、启动模式、启动文件、SystemInit),并通过代码示例展示了实战应用。掌握这些知识后,你可以:
- 开发自定义Bootloader,实现IAP固件升级
- 优化系统启动速度,满足实时性要求
- 快速定位HardFault等底层异常
推荐扩展阅读:
- 《STM32F103参考手册(RM0008)》:深入了解寄存器配置
- 《Cortex-M3权威指南》:理解内核架构和异常处理
- STM32CubeMX:图形化配置时钟和外设,自动生成初始化代码
如果觉得本文对你有帮助,欢迎点赞+关注,后续将带来更多STM32底层开发实战内容!如有疑问,可在评论区留言讨论~
附录:关键地址速查表
|------------|--------------------------------|------------|
| 名称 | 地址范围 | 用途 |
| Flash | 0x08000000~0x0800FFFF | 程序代码存储 |
| SRAM | 0x20000000~0x20004FFF | 变量和堆栈 |
| 向量表(默认) | 0x08000000~0x08000107 | 中断服务函数地址数组 |
| RCC寄存器 | 0x40021000~0x400213FF | 时钟控制寄存器 |
| GPIO寄存器 | 0x40010800~0x40010BFF (GPIOC) | GPIO控制寄存器 |