第2章:MCU启动流程详解

从复位向量到main函数,这条路我走过无数遍。说实话,很多工程师写了几年嵌入式代码,却从来没仔细看过芯片上电后到底干了什么。今天咱们就把这层窗户纸捅破。

2.1 复位向量——芯片的第一口气

MCU上电或复位后,CPU会去一个固定地址取指令。这个地址就是复位向量。以Cortex-M系列为例,复位向量位于0x00000000(或通过VTOR重映射)。

这里存的是什么?两个关键值:

  • 初始栈指针(MSP)
  • 复位异常入口地址

我刚开始做项目时,有一次把栈指针设错了,结果芯片一上电就跑飞。查了两天才发现是链接脚本里栈地址算偏了一个字节。嗯,从那以后我对复位向量格外敏感。

核心要点:复位向量不是代码,是数据。CPU硬件自动加载这两个值,然后跳转到复位异常处理函数。

2.2 启动文件——真正的幕后英雄

启动文件(startup_xxx.s)是汇编写的。很多人觉得汇编过时了,其实不然。启动阶段C环境还没建立,只能用汇编。

启动文件干了这几件事:

  1. 定义中断向量表
  2. 初始化堆栈指针
  3. 调用SystemInit()配置时钟
  4. 搬运.data段到RAM
  5. 清零.bss段
  6. 跳转到main()

你想想看,如果没有启动文件,你的全局变量初始值从哪来?全都会是随机值。我在一个低功耗项目里遇到过,某个全局变量没初始化,结果在休眠唤醒后值变了,导致逻辑错乱。查了半天才发现是启动文件里.bss段清零被优化掉了。

; 典型的Cortex-M启动文件片段
Reset_Handler   PROC
                EXPORT  Reset_Handler
                IMPORT  SystemInit
                IMPORT  __main
                
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP

个人习惯:我一般会在SystemInit()里加个LED闪烁,这样一眼就能看出启动是否正常。调试初期特别管用。

2.3 堆栈初始化——别让局部变量没地方住

堆栈初始化看似简单,其实坑不少。栈(Stack)用于局部变量、函数调用、中断现场保存。堆(Heap)用于动态内存分配。

启动文件里会定义:

  • 栈顶地址(Stack_TOP)
  • 栈大小(Stack_Size)
  • 堆大小(Heap_Size)

我见过最典型的错误是栈溢出。有个同事在中断里定义了一个大数组,结果栈空间不够,程序跑着跑着就死机了。而且这种问题很难复现,因为栈溢出往往在特定调用路径下才触发。

参数 典型值 说明
Stack_Size 0x400 (1KB) 够用,但中断嵌套多时要加大
Heap_Size 0x200 (512B) 不用malloc可以设为0
Stack_TOP RAM起始+RAM大小 栈向下生长,顶在RAM末尾

我曾经踩过的坑:低功耗模式下,如果栈指针指向了保留的RAM区域,唤醒后栈内容会被破坏。所以休眠前一定要确保栈指针在有效范围内。

2.4 从复位到main的完整路径

咱们把整个过程串起来:

  1. 上电复位,CPU从0x00000000加载MSP
  2. 从0x00000004加载PC,跳转到Reset_Handler
  3. Reset_Handler里调用SystemInit()配置时钟
  4. 搬运.data段(初始化全局变量)
  5. 清零.bss段(未初始化全局变量)
  6. 调用__main(C库初始化)
  7. 最终跳转到main()

说白了,main函数只是冰山一角。在它之前,芯片已经默默干了一大堆活。我建议你在调试时,可以在SystemInit()和main()入口各放一个断点,看看中间发生了什么。

避坑指南:如果你的MCU支持从多个启动源启动(比如从RAM或System Flash),一定要确认复位向量指向正确。我有个项目因为Bootloader和App的向量表冲突,导致跳转后直接HardFault。后来加了VTOR重映射才解决。

2.5 低功耗模式下的启动差异

低功耗模式(比如待机模式)唤醒后,芯片会经历一次完整的复位流程吗?不一定。

  • 睡眠模式:CPU暂停,唤醒后从暂停处继续执行
  • 停止模式:时钟关闭,唤醒后重新配置时钟
  • 待机模式:相当于软复位,启动流程和上电类似

我做过一个电池供电的项目,用待机模式唤醒后,发现全局变量全被重新初始化了。因为待机模式会触发复位,启动文件重新搬运了.data段。解决方案是把需要保持的数据放在备份寄存器或RTC RAM里。

我的建议:在设计低功耗系统时,一定要搞清楚你的MCU在每种低功耗模式下,启动流程有什么不同。别等到产品量产了才发现唤醒后数据丢了。

好了,这一章的内容就到这。下一章咱们聊聊Bootloader的设计思路,看看怎么在启动流程里插入自己的代码。