4. ARM Cortex-M 启动流程(上电到 Main)

ARM Cortex-M 系列处理器(M0/M3/M4/M7/M33)采用专为微控制器设计的 M-profile 架构。其启动流程与 Cortex-A 系列有本质区别:无需 MMU 初始化、无复杂异常等级切换,而是依赖硬件固化的向量表与堆栈指针自动完成上电序列。本章深入剖析从复位到 main() 的完整路径,涵盖向量表偏移、堆栈初始化以及 __main 构建 C 运行时环境的关键步骤。

4.1 M-profile 启动序列

Cortex-M 处理器复位后,硬件自动执行以下固定序列,无需任何软件指令:

  1. 读取初始堆栈指针(MSP):从地址 0x00000000 加载 4 字节作为主堆栈指针(MSP)初始值。
  2. 读取复位向量:从地址 0x00000004 加载 4 字节作为复位异常入口地址。
  3. 设置 PC 与 MSP:将 PC 指向复位向量地址,MSP 设为第一步读取的值。
  4. 开始执行复位处理程序:CPU 从复位向量地址取指执行,通常跳转到启动代码(Reset_Handler)。
关键区别: Cortex-M 的向量表包含异常/中断入口地址,而非指令。复位后硬件自动加载前两个字(SP 和 PC),这是 M-profile 启动的核心硬件机制。
Cortex-M 上电硬件自动序列 复位释放 PC = 0x00000000 加载 MSP 从 0x00000000 读 4 字节 加载复位向量 从 0x00000004 读 4 字节 跳转 Reset_Handler PC = 复位向量地址 硬件自动完成 无需软件干预

4.2 向量表与向量表偏移

向量表是 Cortex-M 启动的核心数据结构,存放所有异常和中断的服务函数入口地址。默认向量表基址为 0x00000000,但可通过 VTOR(Vector Table Offset Register) 重定位。

偏移地址 异常/中断号 内容 说明
0x0000 - MSP 初始值 主堆栈指针初始值
0x0004 1 (Reset) Reset_Handler 复位异常入口
0x0008 2 (NMI) NMI_Handler 不可屏蔽中断
0x000C 3 (HardFault) HardFault_Handler 硬错误处理
0x0010 - 0x003C 4-15 系统异常入口 MemManage/BusFault/UsageFault/SVC 等
0x0040 + 16+ 外部中断入口 芯片厂商定义的外设中断
向量表偏移注意事项: 当使用 Bootloader 跳转到应用程序时,必须通过 VTOR 寄存器将向量表基址重定位到应用程序的起始地址(如 0x08010000)。否则,中断发生后 CPU 会从 0x00000000 读取错误的服务函数地址,导致系统崩溃。
Cortex-M 向量表结构(默认基址 0x00000000) 偏移地址 异常/中断号 内容 说明 0x0000 - MSP 初始值 主堆栈指针 0x0004 1 (Reset) Reset_Handler 复位入口 0x0008 2 (NMI) NMI_Handler 不可屏蔽中断 0x000C 3 (HardFault) HardFault_Handler 硬错误处理 0x0010-0x003C 4-15 系统异常入口 MemManage/BusFault 等 0x0040 + 16+ 外部中断入口 芯片厂商定义 VTOR 寄存器可重定位向量表基址,用于 Bootloader 跳转后中断正确响应

4.3 堆栈初始化

Cortex-M 支持两种堆栈模式:主堆栈(MSP) 和 进程堆栈(PSP)。复位后默认使用 MSP。堆栈初始化在启动代码中完成,包括:

  • 设置堆栈边界:定义 __stack_top 和 __stack_bottom 符号。
  • 填充堆栈哨兵:可选,在堆栈区域写入固定模式(如 0xDEADBEEF)用于溢出检测。
  • 切换堆栈指针:如果使用 PSP(如运行 RTOS 任务),在 __main 之前或之后切换。
; 典型 Cortex-M 启动代码堆栈初始化片段
; 文件: startup_stm32f407xx.s

.section .isr_vector, "a", %progbits
  .word __stack_top          ; 0x00000000: MSP 初始值
  .word Reset_Handler        ; 0x00000004: 复位向量

.section .text.Reset_Handler
Reset_Handler:
  ; 设置 MSP(硬件已加载,此处可选重新设置)
  ldr   r0, =__stack_top
  msr   msp, r0

  ; 可选:填充堆栈区域用于溢出检测
  ldr   r0, =__stack_bottom
  ldr   r1, =__stack_top
  ldr   r2, =0xDEADBEEF
fill_stack:
  cmp   r0, r1
  itt   lo
  strlo r2, [r0], #4
  blo   fill_stack

  ; 跳转到 __main
  bl    __main
堆栈大小选择建议: 对于裸机程序,MSP 大小通常为 1KB-4KB;对于 RTOS 环境,每个任务分配独立 PSP,MSP 用于中断服务。堆栈溢出是嵌入式系统最难排查的问题之一,建议在开发阶段启用堆栈哨兵填充。
堆栈初始化流程 硬件加载 MSP 从向量表首字 软件重设 MSP Reset_Handler 中 填充堆栈哨兵 可选调试用 跳转 __main 进入 C 运行时 堆栈区域布局 __stack_bottom → __stack_top

4.4 __main 与 C 运行时环境

__main 是 ARM 编译器提供的 C 运行时初始化入口,位于 libc 库中。它负责在调用用户 main() 之前完成以下关键任务:

阶段 函数/操作 说明
1. 数据段复制 __scatterload 将已初始化数据(.data)从 ROM 复制到 RAM
2. BSS 段清零 __zero_init 将未初始化数据(.bss)清零
3. 堆初始化 __heap_init 设置堆起始地址和大小,供 malloc 使用
4. 库函数初始化 __rt_entry 初始化浮点库、信号量、多线程支持等
5. 调用 main main() 跳转到用户应用程序入口
重要: 如果使用 GCC 工具链(如 arm-none-eabi),对应的入口是 _start,功能类似。无论哪种工具链,在 main() 执行前必须确保 .data 和 .bss 段正确初始化,否则全局变量和静态变量将处于未定义状态。
__main C 运行时环境初始化流程 __scatterload 复制 .data 段到 RAM __zero_init 清零 .bss 段 __heap_init 初始化堆区域 __rt_entry 库函数初始化 main() 用户应用程序入口 链接脚本中的段定义示例 .data : { *(.data) } > RAM AT> ROM /* 加载域在 ROM,运行域在 RAM */ .bss : { *(.bss) } > RAM /* 零初始化段 */ __stack_top = ORIGIN(RAM) + LENGTH(RAM); /* 堆栈顶在 RAM 末尾 */

4.5 完整启动流程总结

从复位到 main() 的完整路径可归纳为以下步骤:

  1. 硬件阶段:CPU 从 0x00000000 加载 MSP,从 0x00000004 加载 PC,跳转到 Reset_Handler。
  2. 汇编阶段:Reset_Handler 中设置堆栈、初始化异常处理、配置 VTOR(如需要)、调用 __main。
  3. C 运行时阶段:__main 完成 .data 复制、.bss 清零、堆初始化、库初始化。
  4. 用户阶段:调用 main(),应用程序开始执行。
调试建议: 在启动代码中每个阶段设置断点,观察 MSP、PC、VTOR 寄存器的值变化。使用 info registers(GDB)或调试器的寄存器窗口,可以清晰验证启动流程是否正确执行。
Cortex-M 完整启动流程总览 硬件阶段 ← 上一章 📖 返回目录 下一章 →