STM32F103C8T6单片机内部执行原理及启动流程详解
引言:为什么深入理解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内核采用三级流水线设计,将指令执行分为三个阶段并行处理:
1. 取指(Fetch):从Flash或指令缓存读取指令,由预取单元(Prefetch Unit)完成,支持指令预取缓冲
2. 解码(Decode):解析指令操作码和操作数,生成控制信号
3. 执行(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
二、 STM32F103C8T6启动流程详解
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引脚拉低)后,硬件自动执行以下步骤:
1. 初始化状态寄存器:清除中断标志,设置默认优先级
2. 读取向量表前两项:
◦ 从0x00000000读取MSP初始值(栈顶指针)
◦ 从0x00000004读取复位向量(Reset_Handler函数地址)
3. 跳转执行Reset_Handler:CPU将PC指针设置为复位向量地址,开始执行启动代码
2.3 启动文件解析: startup_stm32f10x_md.s的秘密
启动文件(汇编编写)是连接硬件复位和C语言环境的桥梁,以 startup_stm32f10x_md .s
(中等容量 设备)为例,主要完成以下工作:
2.3.1 堆栈定义
Stack_Size EQU 0x00000400 ; 栈大小1KBAREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size ; 分配栈空间
__initial_sp ; 栈顶地址(栈从高地址向低地址生长)Heap_Size EQU 0x00000200 ; 堆大小512BAREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base ; 堆起始地址
Heap_Mem SPACE Heap_Size ; 分配堆空间
__heap_limit ; 堆结束地址
2.3.2 中断向量表
AREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __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环境:
Reset_Handler PROCEXPORT 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 ltldrlt r4, [r2], #4 ; 从Flash读取数据strlt r4, [r0], #4 ; 写入RAMblt LoopCopyDataInit ; 循环复制; 3. 初始化BSS段(清零)ldr r2, =_sbss ; BSS段起始地址ldr r4, =_ebss ; BSS段结束地址movs r3, #0
LoopFillZerobss:cmp r2, r4 ; 清零未完成?itt ltstrlt r3, [r2], #4 ; 写入0blt LoopFillZerobss ; 循环清零; 4. 调用SystemInit配置系统时钟bl SystemInit; 5. 调用__main初始化C库,最终跳转到main函数bl __mainENDP
关键步骤解析:
• 数据段(.data)复制:将Flash中存储的已初始化全局变量复制到RAM
• BSS段清零:将未初始化全局变量在RAM中清零
• SystemInit:配置系统时钟(默认使用HSI,可修改为HSE+PLL=72MHz)
• __main:C库函数,初始化堆和栈,调用全局构造函数(C++),最终跳转到用户
2.4 SystemInit函数: 时钟配置的核心
SystemInit函数(位于system_stm32f10x.c)负责系统时钟初始化,默认配置如下:
void SystemInit(void) {/* 复位RCC寄存器到默认状态 */RCC->CR |= 0x00000001U; // 使能HSIRCC->CFGR &= 0xF8FF0000U; // 复位时钟配置寄存器RCC->CR &= 0xFEF6FFFFU; // 关闭HSE、CSS、PLLRCC->CR &= 0xFFFBFFFFU; // 关闭HSE旁路RCC->CFGR &= 0xFF80FFFFU; // 复位PLL配置/* 配置向量表偏移(默认在Flash) */
#ifdef VECT_TAB_SRAMSCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; // 向量表在SRAM
#elseSCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;// 向量表在Flash(0x08000000)
#endif
}
默认时钟:HSI(8MHz)作为系统时钟,未启用PLL,如需72MHz需修改SetSysClock函数:
static void SetSysClockTo72(void) {RCC->CR |= RCC_CR_HSEON; // 使能HSEwhile((RCC->CR & RCC_CR_HSERDY) == 0); // 等待HSE就绪RCC->CFGR |= RCC_CFGR_PLLSRC_HSE; // PLL输入=HSERCC->CFGR |= RCC_CFGR_PLLMULL9; // PLL倍频9倍(8MHz*9=72MHz)RCC->CR |= RCC_CR_PLLON; // 使能PLLwhile((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(低电平点亮)
代码实现:
// 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; // 输出模式,最大速度10MHzwhile (1) {GPIOC->ODR ^= GPIO_ODR_ODR13; // 翻转PC13电平Delay(0xFFFFF); // 延时}
}
启动阶段分析:
1. 上电后LED不亮(系统初始化阶段)
2. SystemInit执行完毕后,LED开始闪烁(main函数执行)
3. 若LED不闪烁,可能是时钟配置错误或启动文件选择不当(需使用 startup_stm32f10x_md .s)
3.2 中断向量表重映射: 从RAM启动时的配置
当使用SRAM启动或IAP升级时,需将向量表重映射到RAM:
// 在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 启动时间优化: 从72ms到15ms的实战技巧
默认启动时间:约72ms(含Flash擦写、C库初始化等)
优化方法:
1. 关闭不必要的外设时钟:
在SystemInit中仅使能必要外设时钟(如GPIO、USART)
2. 优化Flash访问:
使能预取缓冲区(FLASH_ACR_PRFTBE=1),设置正确等待周期(72MHz时=2)
3. 跳过C库初始化:
若不使用全局构造函数,可在启动文件中直接跳转到main:
; 在Reset_Handler中替换bl __main为bl main
bl SystemInit
bl main ; 直接调用main,跳过__libc_init_array
4. 使用HSI快速启动:
若对时钟精度要求不高,使用HSI(8MHz)可避免HSE晶振启动延时
四、常见问题与调试技巧
4.1 启动失败排查清单
现象 | 可能原因 | 解决方案 |
程序无反应 | BOOT0引脚接高电平(进入 ISP模式) | 将BOOT0接地,复位芯片 |
HardFault异常 | 栈溢出或非法内存访问 | 增大栈大小(Stack_Size),检 查指针操作 |
现象 | 可能原因 | 解决方案 |
时钟配置后死机 | PLL倍频过高(超过72MHz) | 重新计算PLL参数,确保输出 ≤72MHz |
全局变量初始化失败 | .data或.bss段地址配置错误 | 检查链接脚本( .ld)中的RAM 地址和大小 |
4.2 使用ST-Link调试启动过程
1. 查看寄存器状态:
复位后暂停,查看 SP 是否等于 ___initial_sp, PC 是否指向 Reset_Handler
2. 设置断点:
在 Reset_Handler 、SystemInit、main 函数设置断点,观察执行流程
3. 查看内存:
检查 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控制寄存器 |