2、启动流程分析:MCU上电启动流程、中断向量表与复位向量、从Flash到RAM的代码搬运

好,咱们接着聊。上一章我们把Bootloader的架构搭起来了,这一章要深入到底层——MCU上电之后到底发生了什么?说白了,就是芯片从“死”到“活”的那一瞬间,CPU在干嘛。

我个人习惯,做任何嵌入式项目,第一件事就是看启动流程。不看启动流程,后面出了问题你都不知道从哪查起。我记得刚入行那会儿,有一次板子死活跑不起来,折腾了两天,最后发现是中断向量表放错位置了……嗯,从那以后我再也不敢跳过这一步。

2.1 MCU上电启动流程

MCU上电后,CPU会执行一系列固定的动作。这些动作是芯片设计时就定死的,我们改不了。但理解它,能帮你避免很多坑。

大致流程是这样的:

  1. 上电复位:VDD达到稳定电压,内部POR(上电复位)电路释放复位信号。
  2. 硬件初始化:CPU内部寄存器恢复到默认值,比如PC指针指向复位向量地址。
  3. 读取复位向量:CPU从固定的地址(通常是0x00000000或0x08000000)读取栈指针和复位入口地址。
  4. 跳转到复位入口:PC指针指向复位处理函数,开始执行用户代码。
  5. 系统初始化:关闭看门狗、配置时钟、初始化堆栈、搬运数据段等。
  6. 跳转到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里,响应速度更快。

代码搬运通常由启动文件中的__mainSystemInit完成。但如果你自己写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 总结一下

这一章我们聊了三个核心点:

  1. 上电启动流程:从复位到main(),每一步都有固定动作。
  2. 中断向量表:CPU的中断导航地图,复位向量是入口。
  3. 代码搬运:从Flash到RAM,为了速度和可修改性。

下一章我们会深入Bootloader的跳转逻辑,看看怎么从Bootloader安全地跳转到App。嗯,那里面也有不少坑等着我们。