好了,现在,电源接通了,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),你需要做两件事:

  1. 链接脚本: 告诉 Linker,代码从 0x0801 0000 开始放。

  2. 向量表偏移 (VTOR): 告诉 CPU,中断向量表不从 0 开始了,而是偏移了 0x10000SCB->VTOR = 0x08010000; 如果你不做这一步,你的 App 能跑,但一旦发生中断,CPU 还是会去查 0 地址的老表,导致程序跑飞。


5. 归纳一下

上电复位的过程,就是一场接力赛:

  1. 硬件层: 电源稳定 -> 复位释放 -> 映射 0 地址 -> 读取 MSP 和 PC

  2. 汇编层: Reset_Handler 开始执行。

  3. 时钟层: 调用 SystemInit(),把心跳提速到最高。

  4. CRT 层: 跳转到 __main (C Runtime)。

现在的 CPU 已经全速运转了,但内存里的数据还是乱的。 Flash 里的 .data 还没搬到 RAM,.bss 还没清零。如果你这时候去读全局变量,你会得到错误的值。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐