3. 启动流程第一阶段:从复位向量到汇编入口(Reset_Handler)
好,咱们正式开始聊启动代码的第一阶段。
芯片上电后,CPU 做的第一件事是什么?不是初始化外设,也不是跳转到 main 函数。它做的第一件事,是去固定的地址拿一个值,然后跳过去执行。这个固定的地址,就是 复位向量。
3.1 复位向量:芯片的“出生证明”
ARM Cortex-M 系列,复位后默认从 0x00000000 开始取向量表。向量表的第一个字(4字节)是栈顶指针,第二个字才是复位向量——也就是 Reset_Handler 的入口地址。
我刚开始做车规项目时,有个同事把向量表放错了位置,结果芯片死活跑不起来。查了半天,发现是链接脚本里 VECT_TAB_OFFSET 没对齐。嗯,这种坑踩过一次就记住了。
关键点:
- 向量表必须 256 字节对齐(Cortex-M 要求)
- 第一个字:栈顶地址(
__initial_sp) - 第二个字:复位函数地址(
Reset_Handler)
看一个典型的向量表定义:
__Vectors DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位向量
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬错误
; ... 其他中断向量
说白了,这就是一张“地址表”。CPU 上电后,硬件自动从这个表里取出前两个值,完成初始跳转。
3.2 Reset_Handler:一切从这里开始
复位向量指向的 Reset_Handler,才是我们真正要写的启动代码入口。这个函数通常用汇编写,为什么?因为这时候 C 环境还没准备好——栈指针可能不对,全局变量还没初始化,你连个局部变量都不敢用。
我个人习惯,Reset_Handler 里只做三件事:
- 关中断——防止任何意外中断打断初始化流程
- 设栈指针——让 C 代码能正常调用函数
- 清 BSS、拷贝数据段——为 C 运行时做准备
先看一个最简的 Reset_Handler 实现:
Reset_Handler PROC
EXPORT Reset_Handler
; 第一步:关中断
CPSID I
; 第二步:设置栈指针
LDR sp, =__initial_sp
; 第三步:调用 SystemInit(可选)
BL SystemInit
; 第四步:跳转到 main
BL main
; 如果 main 返回,死循环
B .
ENDP
你可能会问:关中断真的有必要吗?
我告诉你,非常有必要。车规级芯片,上电瞬间各种复位源可能同时触发——看门狗复位、电压监测复位、外部复位引脚抖动。如果不先关中断,Reset_Handler 执行到一半,一个 NMI 进来,栈指针还没设好,直接就 HardFault 了。我在项目里见过这种 case,现场分析时波形一抓,复位引脚毛刺一大堆。
3.3 关闭中断:CPSID 指令的妙用
Cortex-M 提供了 CPSID I 指令,用来全局关中断。这个指令操作的是 PRIMASK 寄存器,置位后屏蔽所有可屏蔽中断(除了 NMI 和 HardFault)。
对应的,CPSIE I 用来开中断。
小技巧:
有些芯片在复位后,默认中断就是关闭的。但我建议你 显式执行一次 CPSID I。为什么?因为不同芯片厂商的复位行为可能不同,显式关中断可以保证行为一致。这是车规开发中“防御性编程”的典型做法。
另外,如果你用的是 Cortex-R 系列(比如 TMS570、RH850 这类),关中断的方式略有不同——需要通过 CPSR 寄存器操作。但思路是一样的:在环境准备好之前,别让任何中断进来捣乱。
3.4 设置栈指针:给 C 代码一个“落脚点”
栈指针 SP 是 C 语言运行的基础。函数调用、局部变量、参数传递,全得靠它。
设置栈指针就一行汇编:
LDR sp, =__initial_sp
__initial_sp 是链接脚本里定义的一个符号,指向 RAM 的最高地址(栈是向下生长的)。
这里有个细节:栈指针必须 8 字节对齐。Cortex-M 的硬件要求 SP 必须对齐到 8 字节边界,否则某些指令会触发异常。我在调试一个电机控制项目时,就因为栈指针没对齐,导致浮点运算偶尔出错。查了两天才发现是链接脚本里栈地址算错了 4 个字节。
注意:
有些芯片支持双堆栈(MSP 和 PSP)。复位后默认使用 MSP(主栈指针)。如果你要用 PSP(进程栈指针),记得在切换到线程模式后再设置。车规级 RTOS 里经常这么干——ISR 用 MSP,任务用 PSP。
3.5 第一阶段流程图
下面我用一张流程图,把第一阶段的核心逻辑串起来:
3.6 常见问题与避坑
这部分我总结几个实际项目中踩过的坑:
- 向量表放错位置:有些芯片支持从 BootROM 或 Flash 启动,向量表地址可能不是默认的 0x00000000。记得检查
VTOR寄存器或者芯片的启动模式选择引脚。 - 栈大小不够:车规代码里中断嵌套很常见。如果栈只分配了 1KB,一个深层嵌套就把栈干爆了。我建议至少 4KB 起步,具体看你的中断深度和局部变量大小。
- 复位后外设状态不确定:关中断只是第一步,有些外设(比如看门狗)复位后可能默认就是开启的。如果不在
Reset_Handler里尽早喂狗或关狗,芯片会反复复位。嗯,这个坑我帮别人查过好几次。
个人建议:
在 Reset_Handler 开头加一小段“黄金延时”——用循环空转几百个周期。为什么?因为某些电源芯片上电后需要几微秒才能稳定。不加这个延时,直接访问外设寄存器可能会读到随机值。我在一个 BMS 项目里吃过这个亏,后来所有车规项目都加了这个“保命延时”。
好了,第一阶段的核心内容就这些。说白了就是三件事:关中断、设栈指针、为 C 环境做准备。下一阶段我们会聊 BSS 段清零和数据段拷贝——那是让全局变量能正常工作的关键步骤。