先给结论:启动顺序由谁规定
下列流程不是 main.c、源文件排列顺序,甚至也不是某个 HAL 函数“自然发生”的结果。它由三个层次共同决定:
- Cortex-M 硬件架构规定:复位时从当前映射到
0x00000000的向量表读取初始MSP与复位入口PC。 - **启动文件(startup file)**定义:向量表内容、
Reset_Handler,以及Reset_Handler内部先后调用哪些初始化例程。 - 链接器脚本 / scatter file定义:向量表、
.text、.data、.bss、RAM 代码和栈分别位于哪里,.data/ RAM 代码的加载镜像从哪里取得。
可以将一份常见 C/C++ Cortex-M 镜像的启动链路写成:
复位事件
→ CPU 从 0x00000000 映射的向量表装载 MSP、PC
→ Reset_Handler(启动文件中定义)
→ 建立最小运行环境 / 可选早期芯片处理
→ 复制已初始化段:Flash LMA → RAM VMA
→ 清零 BSS:RAM → 0
→ 可选:复制或重定向中断向量表至 RAM
→ SystemInit:时钟、电源、芯片基础配置
→ C/C++ 运行库初始化(若工具链和工程使用)
→ main()
上图中的“谁先谁后”最终以项目实际启动文件为准。不同工具链可能把
SystemInit放在数据初始化前后,也可能由运行库入口先做数据初始化再调SystemInit。不要只凭某个厂商模板推断所有工程。
复位的固定硬件规则:MSP 与 PC 来自向量表前两项
Cortex-M 复位后并不会把 0x00000000 当作指令执行。它将该地址开始的两个 32-bit 表项当作数据读取:
MSP = *(uint32_t *)0x00000000 // 初始主栈指针
PC = *(uint32_t *)0x00000004 // Reset_Handler 地址
- 第 0 项必须是有效的 RAM 栈顶地址,并满足栈对齐要求。
- 第 1 项是
Reset_Handler的入口地址;Cortex-M 只执行 Thumb 指令,因此该地址最低位必须为1。 - 后续项是
NMI_Handler、HardFault_Handler、SysTick_Handler和外设中断入口。
当前映射到 0x00000000 的向量表
0x00000000 ┌──────────────────────────────┐
│ 初始 MSP,例如 0x20008000 │ → CPU 装入 MSP
0x00000004 ├──────────────────────────────┤
│ Reset_Handler | 1 │ → CPU 装入 PC 并开始取指
0x00000008 ├──────────────────────────────┤
│ NMI_Handler | 1 │
0x0000000C ├──────────────────────────────┤
│ HardFault_Handler | 1 │
└──────────────────────────────┘
这里的 0x0 / 0x4 是向量表的地址;它们对应的启动存储器是 Flash、BootROM 还是 SRAM,要看芯片当前的启动映射。

图:以常见 STM32 从用户 Flash 启动为例。启动时 0x00000000 区域通常别名映射到用户 Flash 基址 0x08000000。因此 CPU 读取的是 *(uint32_t *)0x00000000(等效于读取 0x08000000 处的第 0 个向量表项)并装载到 MSP,而不是把该数值“压入”MSP;再读取 *(uint32_t *)0x00000004(等效于 0x08000004 的第 1 项)并装载到 PC。图中 0x080001CD 是示例 Reset_Handler 地址;最低位为 1 表示 Thumb 状态,内核实际取指地址为 0x080001CC。Reset_Handler 完成数据段、BSS、系统和运行库初始化后,通常才调用 main();它不是复位后硬件直接跳转到的下一条应用代码。
用图中的地址走一遍,哪些表述需要修正
假设当前确实配置为“从主 Flash 启动”,且图中向量表内容如下:
逻辑访问地址 物理用户 Flash 中的内容 CPU 动作
0x00000000(别名 0x08000000) 0x20000788 MSP ← 0x20000788
0x00000004(别名 0x08000004) 0x080001CD PC ← 0x080001CD(Thumb 入口)
需要特别注意以下修正:
- 不是“执行
0x00000000/0x00000004这两个地址”。它们保存的是数据表项,硬件在复位序列中读取它们。 - 不是“将栈顶指针压入 MSP”。CPU 是把第 0 项的值直接装入 MSP;此时尚未发生一次正常的入栈操作。
- 对常见 STM32 用户 Flash 启动,
0x00000000 → 0x08000000是零地址别名映射。若芯片经 BOOT 配置改从 System Memory 或 SRAM 启动,这个映射关系不同。 0x080001CD的 bit0 是 Thumb 状态标志,向量表中应为奇数;汇编器 / 调试器显示的实际指令半字对齐地址通常是0x080001CC。不要将 bit0 当作 Flash 中独立的一字节指令地址。- Reset_Handler 不是“执行完成后自然开始 main”。启动文件或运行库要显式完成或调用
.data复制、.bss清零、SystemInit、C/C++ 初始化后,才以函数调用方式进入main();若main()返回,嵌入式启动代码通常进入死循环或复位处理,而不是返回给硬件。
启动文件实际负责什么
启动文件通常是 .s / .S 汇编文件,或由工具链提供的启动对象文件。它至少负责:
- 导出并放置中断向量表(常见段名
.isr_vector)。 - 在表第 0 项写入栈顶符号,如
_estack。 - 在表第 1 项写入
Reset_Handler。 - 提供弱定义的默认中断处理函数,使用户可以在 C 文件中覆盖具体 IRQ handler。
- 实现
Reset_Handler,调用数据段初始化、芯片初始化与运行库入口。
典型 GNU 工具链启动代码会表现为以下逻辑,符号名称因链接脚本而异:
Reset_Handler:
ldr sp, =_estack /* 某些启动文件会再次显式设置 SP */
ldr r0, =_sdata /* RAM .data 起点(VMA) */
ldr r1, =_edata /* RAM .data 终点(VMA) */
ldr r2, =_sidata /* Flash .data 镜像(LMA) */
/* copy [r2, ...) → [r0, r1) */
ldr r0, =_sbss
ldr r1, =_ebss
/* zero [r0, r1) */
bl SystemInit
bl __libc_init_array /* C++ 构造、部分 libc 初始化;按工程需要 */
bl main
loop: b loop
这段只是解释职责,不是可直接复制到所有工程的模板。比如 ARMCC/Keil 通常使用 scatter-loading 机制和 __main;它的 .data 复制、.bss 清零可能由运行库完成,而不是你在 Reset_Handler 中看到一段显式循环。
Flash、RAM 与段:谁运行在哪里,谁需要复制
| 区域 / 段 | 常见内容 | 加载地址 LMA | 运行地址 VMA | 启动动作 |
|---|---|---|---|---|
| 向量表 | 初始 MSP、Reset/IRQ 入口 | Flash / BootROM | 当前零地址映射对应区域 | 硬件直接读取前两项 |
.text | 普通函数、main | Flash | Flash | 通常直接 XIP,不复制 |
.rodata | 字符串、const、只读查找表 | Flash | Flash | 通常不复制 |
.data 镜像 | 有初值可写变量的初始值 | Flash | — | 复制到 RAM .data |
.data | 有初值全局/静态变量 | — | RAM | 接收 Flash 镜像 |
.bss | 初始值为 0 的全局/静态变量 | 不占初始化镜像 | RAM | 启动清零 |
.ramfunc / .code_ram 镜像 | RAM 执行函数的机器码 | Flash | — | 复制到 RAM 运行区 |
.ramfunc / .code_ram | 少数 RAM 执行函数 | — | RAM | 必须先复制再调用 |
| Stack | 返回地址、局部变量、异常现场 | — | RAM | 由初始 MSP 指向栈顶 |
普通代码默认在 Flash 就地执行(XIP)。栈存放的是调用与异常现场,不是程序代码。只有链接脚本明确把函数运行地址放进 RAM 的 .ramfunc / .code_ram,并由启动代码完成复制后,该函数才会从 RAM 执行。
为什么 .data、.bss 和 RAM 代码必须区别处理
.data:既要可写,也要有掉电后的初始值
static uint32_t baudrate = 115200U;
运行时该变量必须在 RAM 才能修改,但复位后又应恢复 115200。所以固件镜像在 Flash 保存一份初始值,启动时将其复制到 RAM。
.bss:零初始化不值得在 Flash 存一堆 0
static uint8_t rx_buffer[512];
C 语言语义要求其初始为 0。把 512 个零写进 Flash 没有价值,因此链接器只记录 RAM 范围,启动代码清零即可。
.ramfunc:在特定操作期间不能继续从忙碌 Flash 取指
部分 MCU 在擦写内部 Flash 时,受影响 bank 可能无法正常读取指令或读取延迟显著增加。此时 Flash 擦写例程、它调用的关键常量和必要依赖需要放入 RAM;仅把顶层函数标成 RAM 函数并不足够,调用链仍可能从忙碌 Flash 取指。
SystemInit、C 运行库与 main:避免把职责混为一谈
SystemInit 通常来自 CMSIS 或厂商 system_<device>.c,典型职责是:
- 复位或配置 RCC / 时钟树到一个已知状态。
- 设置 Flash wait state、时钟源、PLL、总线分频等基础配置。
- 处理芯片级的 FPU、缓存、TCM、外部存储接口等早期设置(依具体芯片)。
- 在部分工程中设置
SCB->VTOR指向应用向量表。
它不是 C 运行库,也不保证替你初始化 .data / .bss。C/C++ 运行库初始化则可能包含:
- C++ 全局 / 静态对象构造函数。
atexit、newlib 或半主机等库的必要初始化。- 工具链定义的其他 pre-init / init 钩子。
裸机 C 项目若没有 C++ 和复杂 libc,运行库动作很少;但不要因为“我的项目看起来简单”就随意跳过工具链入口,先确认启动文件和链接方式。
YTM32 与 STM32:核心相同,零地址映射必须以具体芯片手册为准
二者只要基于 Cortex-M,复位读取向量表前两项的规则完全相同。常见差异主要在 boot mode、存储器基址、零地址 alias 以及 Flash 控制器行为。
| 项目 | YTM32B1LE1x(参考笔记所述) | 常见 STM32(举例) | 工程含义 |
|---|---|---|---|
| 应用 Flash 基址 | 0x00000000 | 常见为 0x08000000 | 不能把某一型号地址硬套到另一型号 |
| 复位读取地址 | 0x00000000 | 0x00000000 | Cortex-M 固定规则 |
| 零地址映射 | 应用 PFlash 直接映射到零地址 | 由 BOOT 配置映射 Flash、System Memory 或 SRAM | 决定 CPU 实际从哪张向量表起步 |
| 普通代码执行 | Flash XIP | Flash XIP | .text 通常不复制 |
| 数据段初始化 | Flash → RAM / BSS 清零 | Flash → RAM / BSS 清零 | 由启动文件 + 链接布局实现 |
对 STM32 来说,从用户 Flash 启动时,0x00000000 区域通常是对 0x08000000 用户 Flash 的 alias;从 System Memory 或 SRAM 启动时,零地址会映射到相应启动区。对 YTM32,必须以对应系列参考手册、启动模式和链接文件确认是否真的使用 0x00000000 作为应用 PFlash 基址。
不要用“STM32 都是 0x08000000”或“YTM32 都从 0 开始”这样的绝对表述。启动映射是型号、boot 引脚/option byte 和厂商实现共同决定的。
向量表重定位:什么时候需要放到 RAM
默认情况下,异常和中断入口通过 Flash 向量表找到。以下场景可能需要重定位:
- Bootloader 跳转至非零地址的应用镜像;应用必须把
SCB->VTOR指向自己的向量表(内核支持 VTOR 的前提下)。 - 运行时需要动态修改中断 handler,例如某些 boot / patch / RTOS 场景;通常先复制向量表到 RAM,再改 RAM 表项。
- 需要区分多个镜像或安全域,且启动阶段选择不同向量表。
示意代码:
#include "core_cm4.h"
#define APP_VECTOR_BASE (0x08008000UL)
static void app_relocate_vector_table(void)
{
/* 地址必须满足该内核实现要求的对齐;以芯片 CMSIS 定义为准。 */
SCB->VTOR = APP_VECTOR_BASE;
__DSB();
__ISB();
}
重定位向量表和“把所有代码复制到 RAM”不是一件事;前者只改变异常入口表的位置。
链接器脚本:决定段地址,不决定运行时调用顺序
构建过程可概括为:
.S / .c / .cpp
→ 汇编 / 编译
.o:带有 .text、.data、.bss、.isr_vector 等 section
→ 链接器脚本(GNU ld .ld / ARM scatter .sct/.scf)
ELF / AXF / HEX / BIN:每个 section 获得最终 LMA 与 VMA
→ 烧录
启动存储器中的向量表、机器码、初始化镜像
- CPU 不识别
.c、.S、.ld或.scf;它只在复位后读取向量表并执行机器码。 - 链接器放置 section、解析符号、计算
.data的源/目的范围、生成 RAM 代码的加载镜像。 Reset_Handler内的调用关系(或工具链运行库入口)才决定初始化先后。- 汇编中的
DCD用于在当前位置写入 32-bit 常量,常用来构建向量表;IMPORT仅声明符号由外部对象或链接器提供,不会自行产生数据。
调试核对清单:从复位停住时该看什么
- 看 VTOR / 零地址映射: 当前 CPU 预期的向量表在哪里?Bootloader 是否已切换到应用表?
- 看前两项: 向量表第 0 项是否是有效 RAM 栈顶;第 1 项是否是 Thumb 格式的
Reset_Handler地址。 - 在 Reset_Handler 单步:
.data复制源/目的范围是否正确;.bss是否全部清零。 - 核对链接 map: Flash / RAM 容量和起始地址是否与目标具体型号一致;
.ramfunc的 LMA 与 VMA 是否合理。 - 检查 SystemInit: 时钟源是否就绪、Flash latency 是否匹配频率、外部晶振失败时是否有可诊断的降级路径。
- 检查栈与堆: 栈顶、栈大小、heap 边界是否会与
.bss/.data相撞;在 HardFault 中读取 MSP/PSP 和栈水位。 - 验证 RAM 函数: 复制是否发生在第一次调用前,且关键调用链、常量表或 ISR 不会继续访问忙碌 Flash。
结论
Cortex-M 启动的硬件起点只有两个字:向量表。硬件将它的前两项装入 MSP 和 PC;随后由启动文件中的 Reset_Handler 根据链接器布局建立 C/C++ 运行环境,调用芯片基础初始化和运行库,最后进入 main()。
理解这个边界后,YTM32 与 STM32 的启动差异就不再神秘:内核规则一致,真正需要逐型号核对的是零地址映射、Flash/RAM 地址、boot 模式、时钟与 Flash 控制器行为。