2. 固件基础与存储架构:MCU启动流程、Flash分区设计、Bootloader与App的隔离机制

大家好,我是你们的嵌入式讲师。今天咱们聊点硬核的——MCU的启动流程、Flash怎么分区,还有Bootloader和App怎么做到“井水不犯河水”。

说实话,很多工程师做了好几年固件开发,对启动流程还是一知半解。我见过有人把Bootloader和App放在同一个分区,结果OTA升级失败后设备直接变砖。嗯,这种坑我踩过,所以今天必须给你们讲透。

2.1 MCU启动流程:从复位到main()

MCU上电后,到底发生了什么?我习惯把它分成三步:

  1. 硬件复位:CPU从0x00000000取向量表,拿到栈指针和复位向量
  2. 启动代码:汇编代码初始化C运行时环境(清零BSS、拷贝数据段)
  3. 跳转main():调用用户主函数

你想想看,如果向量表地址错了,程序直接跑飞。我在项目中遇到过,某次改Flash映射忘了更新链接脚本,结果设备上电就死机。查了两天才发现是向量表偏移没设对。

核心要点:MCU启动时,第一个取的是栈指针,第二个才是复位向量。这两个值必须正确,否则连main()都进不去。

2.2 Flash分区设计:怎么分才合理?

Flash分区,说白了就是把存储空间切成几块,各干各的。我建议至少分三个区:

分区名称 起始地址 大小 用途
Bootloader区 0x08000000 32KB 启动引导、固件校验、升级管理
App区 0x08008000 256KB 主应用程序
备份区 0x08048000 256KB OTA下载的新固件
参数区 0x08088000 16KB 设备配置、校准数据

为什么Bootloader要单独分区?因为它是最后一道防线。我见过有人把Bootloader和App混在一起,结果App刷坏了,Bootloader也跟着完蛋。设备直接变砖,只能拆机用烧录器救。

我的经验:Bootloader分区最好用写保护。很多MCU支持硬件写保护,一旦使能,App再怎么乱写也伤不到Bootloader。这个功能我强烈建议开启。

2.3 Bootloader与App的隔离机制

隔离,说白了就是让Bootloader和App互相不干扰。怎么做?我总结了三招:

2.3.1 向量表重映射

App启动时,必须把向量表从0x08000000改到App的起始地址。比如App在0x08008000,就要设置SCB->VTOR = 0x08008000。

// 在App的启动代码中
#define APP_BASE_ADDR 0x08008000

void SystemInit(void)
{
    // 重映射向量表到App区
    SCB->VTOR = APP_BASE_ADDR;
    
    // 其他初始化...
}

如果不做这一步,中断来了CPU还是去Bootloader的向量表找入口。那App里的中断服务函数永远跑不起来。我记得有个同事忘了这茬,调试串口中断时死活不进回调函数,查了一下午才发现是向量表没改。

2.3.2 内存隔离

Bootloader和App要约定好各自的内存范围。我习惯用链接脚本(.ld文件)来强制约束:

/* Bootloader的链接脚本 */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
    RAM (rw)   : ORIGIN = 0x20000000, LENGTH = 16K
}

/* App的链接脚本 */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 256K
    RAM (rw)   : ORIGIN = 0x20004000, LENGTH = 48K
}

注意看,Bootloader和App的RAM地址是分开的。这样App就算跑飞了,也写不到Bootloader的变量。嗯,这里要注意,中断向量表也要放在对应的Flash区。

2.3.3 跳转协议

Bootloader跳转到App前,要做三件事:

  1. 关闭所有外设中断
  2. 重置SysTick定时器
  3. 清理未处理的中断标志
void jump_to_app(uint32_t app_addr)
{
    uint32_t stack_ptr = *(volatile uint32_t*)app_addr;
    uint32_t reset_vec = *(volatile uint32_t*)(app_addr + 4);
    
    // 关闭全局中断
    __disable_irq();
    
    // 重置所有外设
    DeInitPeripherals();
    
    // 设置主栈指针
    __set_MSP(stack_ptr);
    
    // 跳转到App复位向量
    void (*app_main)(void) = (void (*)(void))reset_vec;
    app_main();
}

避坑指南:我曾经在跳转前忘了关SysTick中断,结果App启动后一直进SysTick_Handler,但向量表还没重映射,程序直接HardFault。所以跳转前一定要把所有中断都关了,一个不留。

2.4 实战中的分区策略

不同的医疗设备,分区策略也不一样。我按经验分了三种场景:

  • 小型设备(如血糖仪):Flash 128KB,Bootloader 16KB,App 96KB,参数区16KB。没有备份区,升级失败就回滚到出厂固件。
  • 中型设备(如输液泵):Flash 512KB,Bootloader 32KB,App 256KB,备份区192KB,参数区32KB。支持OTA双备份升级。
  • 大型设备(如监护仪):Flash 2MB,Bootloader 64KB,App 1MB,备份区896KB,参数区64KB。支持多版本回滚。

你想想看,医疗设备升级失败可不是闹着玩的。我见过一个案例,某输液泵OTA升级到一半断电,结果Bootloader和App都坏了。后来我们强制要求Bootloader必须支持从备份区恢复,这才彻底解决问题。

我的建议:分区大小最好留20%余量。别把Flash塞得满满当当,否则后面加功能时你就知道痛苦了。我吃过这个亏,现在设计分区时都会多留点空间。

2.5 总结

好了,今天的内容就到这里。总结一下:

  • MCU启动流程:复位→启动代码→main(),向量表地址是关键
  • Flash分区:Bootloader、App、备份区、参数区,各司其职
  • 隔离机制:向量表重映射、内存隔离、跳转协议,缺一不可

下一章我会讲Bootloader的具体实现,包括固件校验和升级流程。到时候咱们再细聊。