ARM的启动流程

前言

每一个 C 语言程序员的旅程都是从 main() 函数开始的。但在嵌入式世界里,当电源接通的那一刻,距离 CPU 执行 main() 的第一行代码,中间其实隔着一段惊心动魄的旅程。
这段“真空期”发生了什么?堆栈是谁设置的?全局变量的初值是怎么从 Flash 飞到 RAM 里的?本文将抛开具体的芯片型号,从 Cortex-M 架构(M0/M3/M4/M7)的底层视角,还原这段不为人知的“启动秘史”。


画流程图,按这个顺序画:
Power On(上电)
Fetch MSP & PC(硬件取栈顶和复位地址)
Reset_Handler (汇编入口)
SystemInit() -> 配置时钟
Copy Data -> Flash搬运变量到RAM
Clear BSS -> RAM清零
_start / __libc_init (C库环境)
main() (你的代码)
vTaskStartScheduler (作系统接管)


启动时间轴:

硬件/CPU内核 启动代码(Reset_Handler) 用户应用(main) 上电 / 复位触发 (Reset) 硬件自动序列 从地址 0x00 获取 MSP (栈顶) 1 从地址 0x04 获取 PC (复位入口) 2 跳转执行 Reset_Handler 3 系统基础初始化 调用 SystemInit() (配置时钟等) 4 C运行时环境搭建(CRT) **Data Copy** (把.data段初始值从Flash复制到RAM) 5 **BSS Clear** (把RAM中的.bss段全部清零) 6 跳转调用 main() 7 用户代码开始... 硬件/CPU内核 启动代码(Reset_Handler) 用户应用(main)

第一阶段:硬件的自动动作(The Hardware Sequence)

在软件介入之前,硬件电路会先执行一套固化的“硬逻辑”。当芯片上电复位(POR)或按下复位键后,Cortex-M 内核处于“懵懂”状态,它严格遵守以下两个步骤来寻找“人生的起点”:

1. 取栈顶地址 (Fetch MSP)

内核会自动读取内存地址 0x0000 0000 处的内容。

  • 这个地址存放的值,被赋值给 MSP (Main Stack Pointer,主堆栈指针)
  • 为什么? 就像盖房子先打地基,CPU 执行指令需要压栈(函数调用、保护现场),所以第一件事必须告诉 CPU:RAM 的顶端在哪里。
2. 取复位入口 (Fetch PC)

紧接着,内核读取内存地址 0x0000 0004 处的内容。

  • 这个地址存放的值,被赋值给 PC (Program Counter,程序计数器)
  • 这个值就是 Reset_Handler (复位中断服务函数) 的入口地址。
  • 此时: 硬件工作结束,PC 指针跳转,软件代码正式接管系统。

技术彩蛋: 这就是为什么向量表(Vector Table)必须放在 Flash 的起始位置(或者通过 VTOR 寄存器重映射),因为硬件只认这两个固定的偏移量。


第二阶段:汇编启动代码(The Assembly Boot)

PC 指针跳转到了 Reset_Handler。这通常是一段汇编代码(.s 文件),因为此时 C 语言运行环境还没建立,无法执行复杂的 C 代码。这段汇编主要完成两大任务:

1. 系统时钟初始化 (System Clock Init)
  • 刚上电时,为了确保稳定,MCU 通常使用内部的低速时钟(HSI,例如 8MHz)。
  • 启动代码通常会调用一个类似 SystemInit 的函数(虽然是汇编调 C,但只做寄存器操作)。
  • 目的: 开启外部晶振、配置 PLL(锁相环),把系统主频从“散步模式”拉到“赛车模式”(如 72MHz/160MHz),否则后面的代码跑起来会极慢。
2. 堆栈指针的最终确认

虽然硬件第一步已经读取了 MSP,但在某些编译器或安全设置下,启动代码可能会再次强制校准一下 SP 指针,确保 8 字节对齐(ARM AAPCS 标准要求)。


第三阶段:C 语言环境构建(The C Runtime Construction)

这是面试中最核心的考点,也是启动流程中最“忙碌”的阶段。
C 语言中的全局变量分两种:已初始化的int a=10;)和未初始化的int b;)。在 Flash 里,它们只是静止的数据,我们需要把它们搬运到 RAM 中,程序才能动态修改它们。

1. 数据搬运 (Data Copy)
  • 对象: .data 段(已初始化的全局变量/静态变量)。
  • 现状: 变量的初值(如 10)必须保存在 Flash 里(掉电不丢失),但变量的地址是在 RAM 里。
  • 动作: 启动代码通过循环指令,将 Flash 里的初值逐个字节拷贝到 RAM 对应的地址中。
  • 结果: a 变量在内存里真的变成了 10
2. 内存清零 (BSS Clear)
  • 对象: .bss 段(未初始化或初始化为 0 的变量)。
  • 现状: C 标准规定,未初始化的全局变量默认值为 0。但刚上电的 RAM 数据是随机的乱码。
  • 动作: 启动代码将这块 RAM 区域全部清零
  • 结果: b 变量在内存里变成了 0

通俗比喻:

  • Data Copy 就像搬家,把家具(初值)从卡车(Flash)搬进新房(RAM)。
  • BSS Clear 就像打扫卫生,把新房里原本的垃圾(随机值)清扫干净,让房间空空如也(全0)。

内存搬运视图:
在这里插入图片描述

第四阶段:库初始化与跳转(Library Init & Jump)

1. C 库初始化 (__libc_init_array)

在进入用户代码前,编译器通常会插入标准库的初始化函数。

  • 如果你用 C++,全局对象的构造函数在这里执行。
  • 初始化堆(Heap)管理器(如果要用 malloc)。
2. 惊险一跃 (BL main)

万事俱备,汇编代码执行最后一条跳转指令:

BL main
  • BL (Branch with Link): 跳转并记录返回地址。虽然实际上 main 函数一旦退出,系统通常会进入死循环,没有回头的路。
  • 此时,PC 指针指向了你亲手写的 main() 函数的第一行。

总结:为什么要懂这个?

很多嵌入式工程师会觉得:“我只要会写 main 里的逻辑不就好了吗?”
但理解启动流程是初级工程师向高级进阶的分水岭

  1. Bootloader 开发: 假如你要写 OTA 升级功能,你需要手动模拟这个过程(重定位向量表、跳转 PC)。
  2. Crash 分析: 如果程序一上电就死机,连 main 都进不去,通常就是堆栈溢出或者时钟配置错误导致 Reset_Handler 没跑完。
  3. 内存优化: 理解了 .data.bss,你通过查看 .map 文件,就能精确控制 RAM 的占用。

下次当你按下复位键时,请记得:在那电光火石的几毫秒里,你的芯片已经为你跑完了一场马拉松。


Logo

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

更多推荐