单片机上电后,是怎么一步步跑到 main 的?(术语详解版)
副标题:把 MSP、PC、向量表、.data/ .bss 一次讲清楚

开篇:main 不是「开机键」
很多人学 C 时被灌输一句:程序从 main 开始。
换到 单片机 (也称 MCU,Microcontroller Unit,把 CPU、存储器、外设集成在一颗芯片上的微型计算机)上,很容易产生错觉:烧录完、上电,芯片就会立刻执行你写的 main()。
更准确的说法是:
main是 C 应用程序的入口(你写业务逻辑的地方)- 芯片复位后真正先跑的,是 硬件复位序列 + 启动文件 + C 运行时初始化
这篇把整条链路拆开,并约定:每个专业名词、英文缩写第一次出现时,都用一句话说明它是什么。
先建立一张总地图

以 Cortex-M (ARM 公司的一类嵌入式 CPU 内核,STM32 多数型号基于它)/ STM32(意法半导体的 Cortex-M 单片机产品系列)典型路径为例:
| 步骤 | 动作 | 为何进入下一步 |
|---|---|---|
| 01 | 上电/复位释放 → 进入复位序列 | 架构规定先取向量表,不直接进 main |
| 02 | 读 0x00000000 → 加载初始 MSP | 没有有效栈,后面函数调用不安全 |
| 03 | 读 0x00000004 → 复位向量写入 PC | PC 已指向 Reset_Handler,取指必然跳转 |
| 04 | 执行 Reset_Handler | 启动例程先做系统级初始化 |
| 05 | 调用 SystemInit | 时钟等就绪后,才能准备 C 变量的 RAM 映像 |
| 06 | 复制 .data,清零 .bss | 变量语义满足后,才允许进应用入口 |
| 07 | 调用用户 main() | --- |
阶段划分很重要:
- 01--03:硬件自动完成
- 04--06:启动软件 / C 运行时
- 07:你的应用
一句话压舱:main 是应用入口,不是复位后第一条指令。
01 复位:从「已知状态」重来
复位是什么?
复位(Reset):让 CPU 中断当前一切执行,把关键状态拉回手册规定的初始值,再按固定流程重新启动。
常见触发源:
| 缩写/名称 | 是什么 |
|---|---|
| POR(Power-On Reset,上电复位) | 刚接上电源时的复位 |
| NRST | 芯片外部复位引脚,按复位键常走这条路径 |
| IWDG(Independent Watchdog,独立看门狗) | 软件「喂狗」超时,硬件强制复位,防止死机挂死 |
| 欠压复位等 | 供电电压异常时的保护性复位 |
「寄存器回到复位值」到底在说什么?
寄存器(Register):CPU 内部极快、容量很小的存储单元,用来临时保存正在用的数------比如「下一条指令在哪」「栈顶在哪」。
常见几个:
| 名称 | 全称/含义 | 一句话 |
|---|---|---|
| PC | Program Counter,程序计数器 | 指向「下一条要执行的指令地址」 |
| MSP | Main Stack Pointer,主栈指针 | 指向当前主栈的栈顶位置 |
| xPSR | Program Status Register,程序状态寄存器 | 保存运算结果标志、执行状态等位 |
复位时,硬件会把这些关键寄存器恢复为 架构/芯片手册规定的复位值(已知、可控的默认状态)。
注意两件事:
- 清的是 CPU 内部临时状态,不是删掉你烧进芯片的程序。
- Flash(非易失存储器,掉电不丢,常用来存程序代码)里的程序映像还在。
类比:重启电脑会清掉内存里乱跑的临时状态,但硬盘上的安装文件还在。
关键:为什么复位完不直接进 main?
因为 Cortex-M 的 复位序列 写死了下一步:去固定位置读 向量表。硬件不会替你「猜」main 在哪。
02--03 向量表:先拿栈顶,再拿入口

向量表是什么?
向量表(Vector Table) :一张放在存储器开头附近的「入口地址表」。
发生复位、异常、中断时,CPU 查这张表,知道该跳到哪个处理函数。
对启动来说,最关键的是前两项:
| 偏移地址 | 内容 | 硬件动作 |
|---|---|---|
| 0x00000000 | 初始栈顶地址 | 读出后写入 MSP |
| 0x00000004 | 复位向量(通常是 Reset_Handler 地址) | 读出后写入 PC,并开始取指执行 |
「固定地址」指什么?从哪来?
- 指什么 :复位后,CPU 硬规定 从逻辑地址
0x00000000开始读向量表。 - 规则从哪来 :写在 CPU 内核架构 (如 Cortex-M)说明书里,芯片出厂就遵守------不是你在 C 代码里随便定的。
- 表里的具体数字从哪来 :由 启动文件 (startup,常是
.s汇编)和 链接脚本(告诉链接器:代码/数据各放哪段地址)生成,再烧录进 Flash 映像开头。
STM32 为啥常听到 0x08000000?
STM32 片上 Flash 的物理起始地址常常是 0x08000000 。
芯片内部会做 地址映射/别名 :让 CPU「读逻辑地址 0」时,实际读到主 Flash 开头(具体是否如此,还受 BOOT 引脚------选择从 Flash / 系统存储器 / SRAM 启动的配置引脚------影响)。
所以:
- 读哪里(逻辑 0)→ 架构规定
- 0 映射到哪块物理存储器→ 芯片与 BOOT 配置
- 表里写什么地址→ 你的工程启动文件与链接结果
因果链
- 先装 MSP:后面一旦调用函数,就要往栈里压返回地址;栈无效,后面全不稳。
- 再装 PC :PC 已是
Reset_Handler地址,CPU 取指就会跳进去。
栈(Stack) 是什么?一块在 RAM(随机访问存储器,可读可写但掉电丢失,用来放运行中的变量和栈)里预留的区域,函数调用、局部变量、中断现场都靠它「暂存」。
04 Reset_Handler:软件启动真正开始

Reset_Handler 是什么?
Reset_Handler :启动文件里的 复位处理函数,是复位后软件真正开始跑的入口(相对硬件自动取向量而言)。
典型路径(Keil/ARM Compiler 常见写法;GCC 工程细节略有不同,但职责类似):
- (可选)做一些 SRAM 相关准备
- 调用
SystemInit - 跳转到 C 库入口(常见符号名
__main,注意:这不是你写的那个main)
__main 是什么?编译器/C 库提供的运行时入口,负责把 C 语言需要的内存环境准备好,最后再调用你的 main()。
关联
硬件已经把 PC 指到这里 → 启动代码约定:先做系统级初始化,再进入 C 运行时 。
若 MSP 无效,这里一开始调用 C 函数就可能触发 HardFault(Cortex-M 的一种严重故障异常,常表示访问了非法地址、栈坏了等)。
05 SystemInit:把硬件跑到「能干活」

SystemInit 是什么?
SystemInit :芯片厂商提供的系统初始化函数(常见于 system_stm32xxx.c),由 Reset_Handler 调用。
它通常做这些事:
| 工作 | 白话 |
|---|---|
| 配置 时钟树 | 决定 CPU 和外设用多快的时钟跑 |
| 可选设置 VTOR | Vector Table Offset Register,向量表偏移寄存器:告诉 CPU 向量表现在在哪个基地址(Bootloader 场景常用) |
| 其他底层参数 | 如 Flash 等待周期、部分总线/缓存相关设置等(因型号而异) |
几个时钟相关缩写:
| 缩写 | 含义 |
|---|---|
| HSI | High Speed Internal,片内高速内部振荡器 |
| HSE | High Speed External,外部高速晶振时钟 |
| PLL | Phase-Locked Loop,锁相环,用来倍频得到更高主频 |
关联
为什么 SystemInit 之后才搬 .data?
因为要先让系统时钟等基础硬件处于合理状态,再去做存储器初始化;顺序反了,轻则时序不稳,重则后续访问异常。
06 .data / .bss:让 C 变量「合法」
C 语言有两条默认约定:
- 写了初值的全局/静态变量,启动后应等于那个初值
- 没写初值的,应等于 0
但芯片世界更「物理」:
- Flash:适合长期保存代码和初值,运行时不宜当普通可写变量区
- RAM:可读写,适合变量,但上电内容不确定(可能是残留垃圾值)
所以 C 运行时要做两件标准动作:
.data 段
.data :存放「有非零初值」的已初始化全局/静态变量。
动作 :把 Flash 里的初值 复制 到 RAM 的执行地址。
例:int count = 10; ------ 10 跟着程序存在 Flash;运行期要改 count,就必须在 RAM 里有一份可写副本。
.bss 段
.bss (历史上称 Block Started by Symbol):存放「未初始化或初值为 0」的全局/静态变量。
动作 :把对应 RAM 区域 整段清零。
关联
只有这两步完成,C 的变量语义才站得住,启动流程才「允许」把控制权交给 main()。
(实现路径因工具链而异:ARMCC 常经 __main / __rt_entry;GCC 常在启动汇编里直接循环拷贝/清零。殊途同归。)
07 进入 main:应用逻辑开场
当栈可用、系统初始化完成、.data/.bss 就绪后,启动路径会完成最后一点 C/C++ 运行时准备(例如全局对象构造,常见于调用 __libc_init_array 一类函数),然后:
调用你写的 main()。
从这一刻起,执行权才从「启动与运行时」正式交给「应用程序」。
你熟悉的初始化外设、while(1) 循环,才真正开始。
术语速查表(可收藏)
| 术语 | 一句话 |
|---|---|
| MCU / 单片机 | 集成了 CPU、存储器、外设的微型控制芯片 |
| Cortex-M | ARM 嵌入式内核家族,STM32 常用 |
| 复位 Reset | 让 CPU 回到手册规定的初始状态并重跑启动流程 |
| 寄存器 | CPU 内部高速小存储,如 PC、MSP |
| PC | 程序计数器:下一条指令地址 |
| MSP | 主栈指针:主栈栈顶 |
| 向量表 | 异常/中断/复位入口地址表 |
| Reset_Handler | 启动文件中的复位处理函数 |
| SystemInit | 厂商系统初始化(时钟等) |
| VTOR | 向量表基址偏移寄存器 |
| Flash | 掉电不丢的程序存储器 |
| RAM | 可读写、掉电丢失的运行内存 |
| .data | 有初值的全局/静态变量段(需从 Flash 拷到 RAM) |
| .bss | 未初始化全局/静态变量段(需清零) |
| HardFault | 严重故障异常,常与非法访问/坏栈相关 |
你可以怎么做
- 打开工程里的
startup_xxx.s,找到向量表、Reset_Handler、__initial_sp(初始栈顶符号)。 - 打开
system_stm32xxx.c,看SystemInit配了哪些时钟。 - 下次「一上电就 HardFault」,优先怀疑:向量表、MSP、链接地址、时钟,而不是先怪
main里第三行业务代码。
写在最后
整条链可以背成一句:
复位 → 取向量表(MSP,再 PC)→
Reset_Handler→SystemInit→ 复制.data/ 清零.bss→main