2、启动流程分析:MCU上电启动流程、中断向量表与复位向量、从Flash到RAM的代码搬运
好,咱们接着聊。上一章我们把Bootloader的架构搭起来了,这一章要深入到底层——MCU上电之后到底发生了什么?说白了,就是芯片从“死”到“活”的那一瞬间,CPU在干嘛。
我个人习惯,做任何嵌入式项目,第一件事就是看启动流程。不看启动流程,后面出了问题你都不知道从哪查起。我记得刚入行那会儿,有一次板子死活跑不起来,折腾了两天,最后发现是中断向量表放错位置了……嗯,从那以后我再也不敢跳过这一步。
2.1 MCU上电启动流程
MCU上电后,CPU会执行一系列固定的动作。这些动作是芯片设计时就定死的,我们改不了。但理解它,能帮你避免很多坑。
大致流程是这样的:
- 上电复位:VDD达到稳定电压,内部POR(上电复位)电路释放复位信号。
- 硬件初始化:CPU内部寄存器恢复到默认值,比如PC指针指向复位向量地址。
- 读取复位向量:CPU从固定的地址(通常是0x00000000或0x08000000)读取栈指针和复位入口地址。
- 跳转到复位入口:PC指针指向复位处理函数,开始执行用户代码。
- 系统初始化:关闭看门狗、配置时钟、初始化堆栈、搬运数据段等。
- 跳转到main():一切准备就绪,调用main函数。
关键点:上电后CPU执行的第一个指令,不是main(),而是复位向量指向的代码。这个代码通常由启动文件(startup_xxx.s)提供。
你想想看,如果复位向量指向了一个错误地址,CPU就会跑飞。我在项目中遇到过,有人把Bootloader和App的向量表搞混了,结果一上电就进HardFault。排查起来特别痛苦。
2.2 中断向量表与复位向量
中断向量表,说白了就是一个地址表。每个中断源对应一个表项,表项里存的是中断服务函数的入口地址。CPU收到中断后,自动从这个表里找到对应的函数去执行。
对于Cortex-M系列内核,中断向量表的前两个表项是固定的:
| 偏移地址 | 内容 | 说明 |
|---|---|---|
| 0x00000000 | 栈顶地址(MSP初始值) | 上电后CPU自动加载到SP寄存器 |
| 0x00000004 | 复位向量(Reset_Handler) | 上电后CPU跳转到这里执行 |
| 0x00000008 | NMI中断服务函数地址 | 不可屏蔽中断 |
| 0x0000000C | HardFault中断服务函数地址 | 硬件错误处理 |
| ... | ... | 其他外设中断 |
小技巧:调试时如果发现程序跑飞了,先检查SP和PC寄存器的值。SP如果是0xFFFFFFFF或者0x00000000,大概率是向量表没配好。
复位向量为什么这么重要?因为它决定了CPU从哪里开始执行。在Bootloader设计中,我们经常需要修改复位向量的指向。比如,Bootloader在Flash的起始地址,App在后面的地址。那么App的复位向量就要偏移。
我曾经踩过一个坑:App的向量表放在0x08010000,但忘了修改SCB->VTOR寄存器。结果中断一来,CPU还是去0x08000000找向量表,自然就找不到App的中断服务函数了。嗯,这个坑我印象特别深。
2.3 从Flash到RAM的代码搬运
代码为什么要搬运?直接放在Flash里跑不行吗?
其实大部分代码确实可以直接在Flash里跑。但有些场景不行:
- 速度要求:Flash读取速度比RAM慢,尤其是高频MCU,Flash有等待周期。关键算法搬到RAM里跑能快好几倍。
- 可修改性:Flash不能随便写,但RAM可以。比如Bootloader需要更新自身,就得把更新代码搬到RAM里执行。
- 中断响应:中断服务函数放在RAM里,响应速度更快。
代码搬运通常由启动文件中的__main或SystemInit完成。但如果你自己写Bootloader,就得手动实现这个搬运过程。
搬运的核心逻辑很简单:
// 伪代码:从Flash搬运代码到RAM
void code_copy(void)
{
uint32_t *src = (uint32_t *)FLASH_ADDR; // Flash源地址
uint32_t *dst = (uint32_t *)RAM_ADDR; // RAM目标地址
uint32_t len = CODE_SIZE; // 代码长度(字节)
for (uint32_t i = 0; i < len / 4; i++)
{
dst[i] = src[i]; // 逐字拷贝
}
// 搬运完成后,修改向量表偏移
SCB->VTOR = (uint32_t)RAM_ADDR;
// 跳转到RAM中的复位向量
void (*reset_handler)(void) = (void (*)(void))RAM_ADDR[1];
reset_handler();
}
注意:搬运过程中不能开中断!否则中断来了,向量表还没更新,CPU会去Flash里找中断服务函数,而Flash里的函数可能已经被覆盖了。后果就是——HardFault。
我建议在搬运前先关总中断,搬运完成后再开。另外,搬运代码本身最好放在RAM里执行,否则你搬着搬着把自己给搬没了……
实际项目中,代码搬运的细节比这个复杂。比如要考虑:
- 代码段、数据段、BSS段分别处理
- 只搬运需要加速的部分,不是全部代码
- 搬运完成后要刷新指令缓存(如果有的话)
我记得有一次做OTA升级,App代码需要从Flash搬运到RAM里执行,因为Flash里要写入新的固件。结果搬运完忘了刷新ICache,CPU执行的还是Flash里的旧指令。排查了半天才发现是Cache没清……
2.4 总结一下
这一章我们聊了三个核心点:
- 上电启动流程:从复位到main(),每一步都有固定动作。
- 中断向量表:CPU的中断导航地图,复位向量是入口。
- 代码搬运:从Flash到RAM,为了速度和可修改性。
下一章我们会深入Bootloader的跳转逻辑,看看怎么从Bootloader安全地跳转到App。嗯,那里面也有不少坑等着我们。