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

二、 STM 32F103C8T6 启动流程详解

2.1 启动模式: BOOT 引脚如何决定程序从哪里启动?

STM32的启动模式由BOOT0BOOT1 引脚的电平决定,共三种模式:

|---------------|---------------|--------------|--------------|------------------|
| 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函数地址)

  1. 跳转执行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);                 // 延时
  }
}

启动阶段分析

  1. 上电后LED不亮(系统初始化阶段)

  2. SystemInit执行完毕后,LED开始闪烁(main函数执行)

  3. 若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库初始化等)

优化方法

  1. 关闭不必要的外设时钟

在SystemInit中仅使能必要外设时钟(如GPIO、USART)

  1. 优化Flash访问

使能预取缓冲区(FLASH_ACR_PRFTBE=1),设置正确等待周期(72MHz时=2)

  1. 跳过C库初始化

若不使用全局构造函数,可在启动文件中直接跳转到main:

cs 复制代码
; 在Reset_Handler中替换bl __main为bl main
bl SystemInit
bl main        ; 直接调用main,跳过__libc_init_array
  1. 使用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

  1. 设置断点

在 Reset_Handler 、SystemInit、main 函数设置断点,观察执行流程

  1. 查看内存

检查 0x20000000(RAM起始地址)是否已复制.data段数据,.bss段是否清零

总结与扩展

本文详细解析了STM32F103C8T6的内部执行原理(哈佛架构、三级流水线、存储器映射、时钟系统)和启动流程(复位序列、启动模式、启动文件、SystemInit),并通过代码示例展示了实战应用。掌握这些知识后,你可以:

  1. 开发自定义Bootloader,实现IAP固件升级
  2. 优化系统启动速度,满足实时性要求
  3. 快速定位HardFault等底层异常

推荐扩展阅读:

  1. 《STM32F103参考手册(RM0008)》:深入了解寄存器配置
  2. 《Cortex-M3权威指南》:理解内核架构和异常处理
  3. STM32CubeMX:图形化配置时钟和外设,自动生成初始化代码

如果觉得本文对你有帮助,欢迎点赞+关注,后续将带来更多STM32底层开发实战内容!如有疑问,可在评论区留言讨论~

附录:关键地址速查表

|------------|--------------------------------|------------|
| 名称 | 地址范围 | 用途 |
| Flash | 0x08000000~0x0800FFFF | 程序代码存储 |
| SRAM | 0x20000000~0x20004FFF | 变量和堆栈 |
| 向量表(默认) | 0x08000000~0x08000107 | 中断服务函数地址数组 |
| RCC寄存器 | 0x40021000~0x400213FF | 时钟控制寄存器 |
| GPIO寄存器 | 0x40010800~0x40010BFF (GPIOC) | GPIO控制寄存器 |

相关推荐
Wangshanjie_989 小时前
【STM32】-SPI通讯
stm32
qq_4112624210 小时前
整体无需占用任何硬件 UART,即可新增一条全双工软串口
单片机·嵌入式硬件
XINVRY-FPGA11 小时前
XCZU47DR-2FFVG1517I Xilinx FPGA AMD ZynqUltraScale+ RFSoC
人工智能·嵌入式硬件·fpga开发·信息与通信·信号处理·射频工程·fpga
Cyrus_柯12 小时前
单片机基础(STM32-DAY2(GPIO))
单片机·嵌入式硬件
吃货界的硬件攻城狮12 小时前
【STM32 学习笔记】SPI通信协议
笔记·stm32·学习
努力的小帅12 小时前
STM32单片机_3
stm32·单片机·嵌入式硬件·学习·stm32c8t6
逼子格13 小时前
开关电源和线性电源Multisim电路仿真实验汇总——硬件工程师笔记
嵌入式硬件·硬件工程·硬件工程师·开关电源·multisim电路仿真·稳压电源·线性电源
SKYDROID云卓小助手13 小时前
无人设备遥控器之无线电频率篇
服务器·网络·单片机·嵌入式硬件·算法
逼子格15 小时前
振荡电路Multisim电路仿真实验汇总——硬件工程师笔记
笔记·嵌入式硬件·硬件工程·硬件工程师·硬件工程师真题·multisim电路仿真·震荡电流