【第36期】启动流程(一):从Reset Vector到SystemInit
摘要: ARM Cortex-M处理器上电复位后,硬件首先从0x00000000加载栈指针(MSP),再从0x00000004加载复位向量(PC)跳转至Reset_Handler。STM32通过内存映射将Flash/系统存储器镜像到0地址,具体由BOOT引脚决定。软件接管后,Reset_Handler调用SystemInit()初始化时钟,再跳转至编译器提供的__main完成.data段搬运和.b
好了,现在,电源接通了,Reset 按钮松开了。 在 0.000001 秒的瞬间,MCU 内部发生了一场剧烈的“开天辟地”。 如果你以为程序是从 main() 开始跑的,那你就错过了 99% 的底层真相。
让我们进入本期,看看处理器上电后的第一口呼吸。
对于 ARM Cortex-M 处理器(STM32/GD32/nRF52 等),上电复位后的行为是硬件固化的。这不仅仅是软件的事,而是芯片内部逻辑门电路的铁律。
1. 硬件复位序列 (The Hardware Sequence)
当复位引脚 (NRST) 从低电平变回高电平,CPU 内核会立即做两件且仅做这两件事:
动作一:加载栈顶指针 (Load MSP)
CPU 会去访问内存地址 0x0000 0000。
-
它会读出这 4 个字节的内容,并把它赋值给 MSP (Main Stack Pointer) 寄存器。
-
意义: 在执行任何代码之前,必须先有栈。否则连函数调用都做不了。
-
值通常是: RAM 的末尾地址(例如
0x2000 5000)。这是链接脚本里算的_estack。
动作二:加载复位向量 (Load Reset Vector)
CPU 会去访问内存地址 0x0000 0004。
-
它会读出这 4 个字节的内容,并把它赋值给 PC (Program Counter) 寄存器。
-
意义: PC 指向哪里,CPU 下一步就去哪里取指令。
-
值通常是:
Reset_Handler函数的地址(例如0x0800 0125)。
专家冷知识: 注意那个 0x...125 的末尾是奇数 5。 在 Cortex-M 架构中,跳转地址的最低位(LSB)必须是 1,这代表目标代码是 Thumb 指令集。如果是 0,CPU 会试图切换到 ARM 指令集(Cortex-M 不支持),从而触发 HardFault。
2. 内存映射的魔术 (Memory Remap)
你可能会问:
“我的代码明明烧录在 Flash (0x0800 0000),为什么 CPU 去读 0x0000 0000?”
这就是 STM32 的 Boot 引脚 发挥作用的时候了。
-
从 Flash 启动 (BOOT0=0): 芯片内部的总线矩阵 (Bus Matrix) 会玩一个“障眼法”:它把
0x0800 0000处的 Flash 物理地址,镜像 (Alias) 映射到0x0000 0000。 所以,CPU 以为它在读 0 地址,实际上读的是 Flash 的开头。 -
从 System Memory 启动 (BOOT0=1): 映射的是 ST 出厂固化的 Bootloader (ISP程序),用于串口下载。
-
从 RAM 启动: 映射的是 RAM 地址。用于调试。
3. 软件接管:复位中断服务函数 (Reset_Handler)
一旦 PC 指针拿到了地址,硬件的任务就结束了,软件正式接管。 执行的第一段代码,通常在启动文件 startup_stm32xxxx.s (汇编) 中。
; 这是一个标准的 Keil/ARM 汇编启动代码片段
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
; 1. 初始化系统时钟 (这是C函数)
LDR R0, =SystemInit
BLX R0
; 2. 跳转到 C 库的初始化入口 (这是编译器提供的)
LDR R0, =__main
BX R0
ENDP
这里有两个关键步骤:
步骤一:SystemInit() —— 心脏起搏
在进入 main 之前,为什么非要先调这个函数?
-
现状: 刚复位时,MCU 跑在内部低速时钟 (HSI) 上,通常只有 8MHz 或 16MHz。
-
目标: 我们的程序可能需要 170MHz (STM32G4) 或 72MHz (F1)。
-
动作:
SystemInit负责开启 HSE (外部晶振),启动 PLL (锁相环),把倍频系数拉满,配置 Flash 的等待周期 (Latency)。
为什么要这么早做? 因为接下来的步骤是 搬运数据 (.data 段的拷贝)。 如果在 8MHz 下搬运 10KB 数据,需要几十毫秒; 如果在 170MHz 下搬运,只需要微秒。 磨刀不误砍柴工。
步骤二:跳转 __main
注意,这里的 __main 不是 你写的 int main()! 它是 C 语言编译器(Keil/IAR/GCC)提供的一个标准库入口函数。 它负责干那些“脏活累活”(搬运数据、清零 BSS)。
4. 中断向量表 (The Vector Table)
在 startup 文件的最开头,你会看到一张长长的表格。这就是向量表,它必须被放在 Flash 的最起始位置(0 地址)。
__Vectors DCD __initial_sp ; Top of Stack (0x000)
DCD Reset_Handler ; Reset Handler (0x004)
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; HardFault Handler
; ... 后面跟着 SysTick, UART, DMA 等中断入口
Bootloader 的核心考点: 如果你在做一个 IAP 升级程序(App 放在 Flash 的 0x0801 0000),你需要做两件事:
-
链接脚本: 告诉 Linker,代码从
0x0801 0000开始放。 -
向量表偏移 (VTOR): 告诉 CPU,中断向量表不从 0 开始了,而是偏移了
0x10000。SCB->VTOR = 0x08010000;如果你不做这一步,你的 App 能跑,但一旦发生中断,CPU 还是会去查 0 地址的老表,导致程序跑飞。
5. 归纳一下
上电复位的过程,就是一场接力赛:
-
硬件层: 电源稳定 -> 复位释放 -> 映射 0 地址 -> 读取 MSP 和 PC。
-
汇编层:
Reset_Handler开始执行。 -
时钟层: 调用
SystemInit(),把心跳提速到最高。 -
CRT 层: 跳转到
__main(C Runtime)。
现在的 CPU 已经全速运转了,但内存里的数据还是乱的。 Flash 里的 .data 还没搬到 RAM,.bss 还没清零。如果你这时候去读全局变量,你会得到错误的值。
更多推荐


所有评论(0)