2. 固件基础与存储架构:MCU启动流程、Flash分区设计、Bootloader与App的隔离机制
大家好,我是你们的嵌入式讲师。今天咱们聊点硬核的——MCU的启动流程、Flash怎么分区,还有Bootloader和App怎么做到“井水不犯河水”。
说实话,很多工程师做了好几年固件开发,对启动流程还是一知半解。我见过有人把Bootloader和App放在同一个分区,结果OTA升级失败后设备直接变砖。嗯,这种坑我踩过,所以今天必须给你们讲透。
2.1 MCU启动流程:从复位到main()
MCU上电后,到底发生了什么?我习惯把它分成三步:
- 硬件复位:CPU从0x00000000取向量表,拿到栈指针和复位向量
- 启动代码:汇编代码初始化C运行时环境(清零BSS、拷贝数据段)
- 跳转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前,要做三件事:
- 关闭所有外设中断
- 重置SysTick定时器
- 清理未处理的中断标志
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的具体实现,包括固件校验和升级流程。到时候咱们再细聊。