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 处理器复位后,硬件自动执行以下固定序列,无需任何软件指令:
- 读取初始堆栈指针(MSP):从地址
0x00000000加载 4 字节作为主堆栈指针(MSP)初始值。 - 读取复位向量:从地址
0x00000004加载 4 字节作为复位异常入口地址。 - 设置 PC 与 MSP:将 PC 指向复位向量地址,MSP 设为第一步读取的值。
- 开始执行复位处理程序:CPU 从复位向量地址取指执行,通常跳转到启动代码(Reset_Handler)。
关键区别: Cortex-M 的向量表包含异常/中断入口地址,而非指令。复位后硬件自动加载前两个字(SP 和 PC),这是 M-profile 启动的核心硬件机制。
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 读取错误的服务函数地址,导致系统崩溃。
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 用于中断服务。堆栈溢出是嵌入式系统最难排查的问题之一,建议在开发阶段启用堆栈哨兵填充。
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 段正确初始化,否则全局变量和静态变量将处于未定义状态。
4.5 完整启动流程总结
从复位到 main() 的完整路径可归纳为以下步骤:
- 硬件阶段:CPU 从 0x00000000 加载 MSP,从 0x00000004 加载 PC,跳转到 Reset_Handler。
- 汇编阶段:Reset_Handler 中设置堆栈、初始化异常处理、配置 VTOR(如需要)、调用
__main。 - C 运行时阶段:
__main完成 .data 复制、.bss 清零、堆初始化、库初始化。 - 用户阶段:调用
main(),应用程序开始执行。
调试建议: 在启动代码中每个阶段设置断点,观察 MSP、PC、VTOR 寄存器的值变化。使用
info registers(GDB)或调试器的寄存器窗口,可以清晰验证启动流程是否正确执行。