单片机的启动过程
参考链接
嵌入式开发系列教程(二) MCU启动过程 - 简书 (jianshu.com)
STM32启动之旅:从上电到main函数的奇妙历程 (qq.com)
启动流程
启动过程如下:
sequenceDiagram
participant 电源
participant CPU核心
participant BOOT引脚
participant 存储器
participant 启动代码
participant 应用程序
电源->>CPU核心: 上电供电
CPU核心->>CPU核心: POR复位初始化
CPU核心->>BOOT引脚: 读取BOOT引脚状态
BOOT引脚-->>CPU核心: 返回启动模式
CPU核心->>存储器: 读取向量表(地址0x00)
存储器-->>CPU核心: 返回栈指针SP
CPU核心->>存储器: 读取向量表(地址0x04)
存储器-->>CPU核心: 返回复位向量PC
CPU核心->>启动代码: 跳转到Reset_Handler
启动代码->>启动代码: 数据初始化
启动代码->>启动代码: 调用SystemInit配置时钟
启动代码->>应用程序: 跳转到main函数
应用程序->>应用程序: 用户程序开始运行
上电复位
硬件的"晨间仪式“
就像你早上被闹钟叫醒,芯片的"觉醒"也需要一个过程。当电源接通的瞬间,芯片并不会立即开始工作,而是要经历一个称为**上电复位(Power-On Reset, POR)**的过程。这个过程就像人从深度睡眠中醒来:
- 睁开眼睛:电源电压逐渐升高,达到工作阈值(通常是 2.0V 左右)
- 意识清醒:内部复位电路工作,确保所有寄存器都回到初始状态
- 大脑启动:内部 RC 振荡器开始工作,提供最基本的 8MHz 时钟信号(HSI)
sequenceDiagram
participant 电源电压
participant POR电路
participant 内部振荡器
participant CPU核心
Note over 电源电压,POR电路: 上电阶段
电源电压->>POR电路: 电压上升到阈值
POR电路->>POR电路: 检测电压稳定性
Note over POR电路,CPU核心: 复位释放阶段
POR电路->>CPU核心: 释放复位信号
CPU核心->>CPU核心: 寄存器初始化为默认值
Note over 内部振荡器,CPU核心: 时钟启动阶段
CPU核心->>内部振荡器: 启动内部时钟
内部振荡器->>CPU核心: 提供8MHz时钟
CPU核心->>CPU核心: CPU准备就绪,等待执行
此时的 STM32 就像一个刚醒来的人,处于最基本的工作状态:
- • ⏰ 时钟:使用内部 8MHz RC 振荡器(HSI)
- • 🧠 CPU 状态:所有寄存器清零,程序计数器 PC 等待赋值
- • 💾 存储器:还未映射到具体位置
- • ⚡ 功耗:基本工作模式,等待进一步配置
这个阶段耗时:约 1-5 毫秒
启动方式
三岔路口的抉择
复位释放后,CPU 面临一个重要选择:**从哪里开始执行代码?**这就是 BOOT 引脚的作用——它就像路口的指示牌,告诉 CPU 该走哪条路。
芯片上有 BOOT0(部分型号还有 BOOT1)引脚,通过硬件电平配置,可以选择三种不同的"起点":
- flash启动(最常用):stm32的flash能够擦写数十万次,用户通过JTAG或SWD模式,将程序下载至此,重新启动从此处启动
- sytem memory (系统存储器启动):系统存储器是芯片内的一块特定的区域,系统存储器中预置了一段bootloader,bootloder将程序下载到flash区,通过flash启动
- 内嵌SRAM启动:从内存中直接启动代码,避免因小修改反复擦写flash内存,一般用于高速调试
graph TD
A[CPU复位释放] --> B{读取BOOT0引脚}
B -->|BOOT0 = 0| C[路径1: 从Flash启动]
C --> D[地址: 0x08000000]
D --> E[正常运行模式]
style E fill:#90EE90,stroke:#333
B -->|BOOT0 = 1| F{读取BOOT1引脚}
F -->|BOOT1 = 0| G[路径2: 从系统存储器启动]
G --> H[地址: 0x1FFF0000]
H --> I[Bootloader模式]
style I fill:#FFFF99,stroke:#333
F -->|BOOT1 = 1| J[路径3: 从SRAM启动]
J --> K[地址: 0x20000000]
K --> L[调试模式]
style L fill:#87CEFA,stroke:#333
style A fill:#DDA0DD,stroke:#333
style B fill:#DDA0DD,stroke:#333,stroke-width:2px
style C fill:#DDA0DD,stroke:#333
style F fill:#DDA0DD,stroke:#333,stroke-width:2px
style G fill:#DDA0DD,stroke:#333
style J fill:#DDA0DD,stroke:#333
中断向量
神秘的"任务清单"
当 CPU 确定了起点(比如 0x08000000),接下来要做的第一件事就是查看这个位置的中断向量表(Vector Table)。
把向量表想象成一本"任务清单"或"地址簿",里面记录了各种重要信息。而在这本"清单"的最开头,有两个最关键的条目–第一个字和第二个字。
graph TD
subgraph 中断向量表结构 ["中断向量表结构"]
direction LR
A[地址 0x00<br/><span style='color:#FF6347'>第一个字<br/>初始栈指针 SP</span>] -->|指向 RAM 顶部<br/>(例:0x20005000)| A1["栈空间起始位置<br/><small>('仓库在这里')</small>"]
B[地址 0x04<br/><span style='color:#32CD32'>第二个字<br/>复位向量地址</span>] -->|指向 Reset_Handler 函数<br/>(例:0x08000189)| B1["程序执行入口<br/><small>('从这里开始干活')</small>"]
C[地址 0x08<br/>NMI 中断]
D[地址 0x0C<br/>HardFault 中断]
E[地址 0x10~0xFFF<br/>其他中断和异常...]
end
style 中断向量表结构 fill:#FFFACD,stroke:#333,stroke-width:1px
style A fill:#FF6347,stroke:#333,stroke-width:1px,color:white
style B fill:#32CD32,stroke:#333,stroke-width:1px,color:white
style C fill:white,stroke:#333
style D fill:white,stroke:#333
style E fill:white,stroke:#333
style A1 fill:#F0F8FF,stroke:#6495ED,stroke-width:1px
style B1 fill:#F0F8FF,stroke:#6495ED,stroke-width:1px
CPU 硬件会自动完成以下两步(不需要任何软件代码):
-
加载栈指针
1
SP = *((uint32_t*)0x00000000); // 从地址0x00读取值,加载到栈指针寄存器
栈指针就像是告诉 CPU:“你的工作仓库在 RAM 的这个位置,所有临时数据都放这里”。
-
加载程序计数器
1
PC = *((uint32_t*)0x00000004); // 从地址0x04读取值,加载到程序计数器
程序计数器装载的是Reset_Handler 函数的地址,CPU 会立即跳转到这里开始执行第一条指令。
[!note]
为什么是这两个值?
想象你要开始一天的工作:
- • 栈指针(SP):告诉你办公桌在哪里(工作空间)
- • 复位向量(PC):告诉你今天第一个任务是什么(从哪开始)
有了这两个信息,CPU 就可以愉快地开始工作了!
启动代码
Reset_Handler:幕后英雄
当 CPU 跳转到 Reset_Handler 函数时,真正的"准备工作"才开始。这个函数通常定义在启动文件(s文件中有具体的操作步骤)中,它的任务就像搬家公司——把所有东西摆放到正确的位置。
graph TD
A[Reset_Handler 开始] --> B[第一步: 数据搬家<br/>复制.data段]
B --> C[第二步: 打扫房间<br/>清零.bss段]
C --> D[第三步: 调整装备<br/>调用SystemInit]
D --> E[第四步: 环境准备<br/>C库初始化]
E --> F[第五步: 出发!<br/>跳转到main函数]
-
任务一:数据搬家(复制.data 段)
在 C 语言中,如果你写了:
1
int counter = 100; // 已初始化的全局变量
这个变量的初始值(100)存储在 Flash 中,但程序运行时需要在RAM中读写它。所以启动代码要做的第一件事就是:
把 Flash 中的初始数据复制到 RAM 中
用伪代码表示:
1
2
3
4
5
6// 把初始化数据从Flash搬到RAM
uint8_t* src = &_sidata; // Flash中的数据起点
uint8_t* dst = &_sdata; // RAM中的数据起点
while (dst < &_edata) {
*dst++ = *src++; // 逐字节复制
}比喻:就像你搬家时,把储物柜里的行李搬到新家的房间里。
-
任务二:打扫房间(清零.bss 段)
如果你写了:
1
int buffer[100]; // 未初始化的全局变量
C 语言标准规定,未初始化的全局变量必须自动清零。启动代码负责把这块 RAM 区域全部填充为 0:
1
2
3
4
5// 把未初始化变量的内存清零
uint8_t* dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0; // 逐字节清零
}比喻:就像搬进新家前,先把空房间打扫干净。
-
任务三:调整装备(调用 SystemInit)
数据准备好后,启动代码会调用**SystemInit()**函数,这是用户可以自定义的配置函数,主要用于:
- • 配置系统时钟(稍后详细讲解)
- • 设置 Flash 访问参数
- • 配置总线分频器
1
2
3// 启动代码中的调用
SystemInit(); // 系统初始化
__main(); // C库初始化,最终会跳转到main
配置时钟
还记得第一站提到的内部 8MHz 时钟吗?这个频率对于现代应用来说太慢了!这就像一辆汽车还在用起步档行驶——虽然能动,但效率低下。体现真正性能需要更高的时钟频率:
- • STM32F1 系列:最高 72MHz
- • STM32F4 系列:最高 168MHz
- • STM32H7 系列:最高 480MHz
时钟的"换挡加速"过程:SystemInit 函数会执行一系列时钟配置,把系统从"起步档"切换到"高速档":
graph TD
subgraph 时钟源 ["时钟源选择"]
A[HSI<br/><small>内部8MHz<br/>启动默认时钟</small>]:::hsi
B[HSE<br/><small>外部晶振<br/>8-25MHz 更稳定</small>]:::hse
end
subgraph 时钟倍频 ["PLL倍频"]
C[PLL<br/><small>锁相环倍频<br/>×9倍频</small>]:::pll
end
subgraph 系统时钟与总线分配 ["SYSCLK系统时钟与总线分配"]
D[SYSCLK<br/><small>系统时钟<br/>72MHz / 168MHz</small>]:::sysclk
E[AHB总线]:::bus
F[APB1总线]:::bus
G[APB2总线]:::bus
end
A -->|可选| C
B -->|可选| C
C --> D
D --> E
D --> F
D --> G
SystemInit 函数的典型操作:
- 启用外部晶振(HSE):切换到更稳定的外部时钟源
- 配置 PLL 倍频:比如 8MHz × 9 = 72MHz
- 选择系统时钟源:从 PLL 获取时钟
- 配置总线分频:为不同外设分配合适的时钟频率
- 设置 Flash 延迟:高速运行需要调整 Flash 读取时序
比喻:就像赛车手在起跑后,迅速从 1 档换到 5 档,释放引擎的全部性能!
进入主函数
当所有准备工作完成后,启动代码最终会调用你熟悉的main()函数:
1 | int main(void) { |
从按下复位键到进入 main 函数,整个过程通常只需要5-20 毫秒——就在你眨眼的瞬间,STM32 已经完成了这场精彩的"启动之旅"!
| 阶段 | 典型耗时 | 主要工作 |
|---|---|---|
| 上电复位(POR) | 1-5ms | 电源稳定、复位电路 |
| 读取向量表 | <1μs | 硬件自动完成 |
| 启动代码执行 | 1-3ms | 数据初始化 |
| 时钟配置 | 2-10ms | HSE/PLL 稳定 |
| 总计 | 5-20ms | 完整启动流程 |
不同IDE的启动过程
IAR
IAR使用ICF文件链接
启动过程
在IAR的启动文件中会定义一个__iar_program_start的handler,这个handler实际上就是Reset Handler。
1 | __vector_table |
当程序启动时,会从0x0000_0000中读取读取msp的值,向后偏移4得到PC的值,此时开始从PC值开始运行。
在调试过程中__iar_program_start中存放了启动地址,也就是0x00004725。
1 | Disassembly |
接下来跳转到0x00004725的位置
1 | __iar_program_start: |
Keil
MAP文件
MAP文件是MDK编译代码后,产生的集程序、数据及IO空间的一种映射列表文件,简单说就是包括了:各种.c文件、函数、符号等的地址、大小、引用关系等信息,分析各.c文件占用FLASH和RAM的大小,方便优化代码。
| 文件类型 | 简介 |
|---|---|
| .o | 可重定向对象文件,每个.c/.s文件都对应一个.o文件 |
| .axf | 可执行对象文件,由.o文件链接生成,仿真的时候需要用到此文件 |
| .hex | INTEL Hex格式文件,用于下载到MCU运行,由.axf转换而来 |
| .map | 连接器生成的列表文件,对分析程序存储占用情况非常有用 |
| 其他 | .crf、.d、.dep、.lnp、.lst、.htm、.build_log.htm等一般用不到 |
map文件的组成部分
| 组成部分 | 简介 |
|---|---|
| 程序段交叉引用关系 | 描述各文件之间函数调用关系 |
| 删除映像未使用的程序段 | 描述工程中未用到而被删除的冗余程序段(函数/数据) |
| 映像符号表 | 描述各符号(程序段/数据)在存储器中的地址、类型、大小等 |
| 映像内存分布图 | 描述各个程序段(函数)在存储器中的地址及占用大小 |
| 映像组件大小 | 给出整个映像代码(*.o)占用空间汇总信息 |





