第二章 固件架构设计:MCU选型考量、Flash分区规划、Bootloader与App分离设计、中断向量表重映射
2.1 MCU选型考量——别让芯片卡住你的OTA
做Type-C接口的固件升级,第一步就是选MCU。我见过不少团队,方案画得漂亮,结果芯片选小了,Flash装不下两套固件,最后只能砍功能。说白了,选型就是给OTA铺路。
我个人习惯从三个维度来卡MCU:
- Flash容量:至少是App固件大小的3倍。为什么?Bootloader占一份,当前App占一份,OTA下载的临时固件还得占一份。我做过一个项目,App固件128KB,选了256KB的芯片,结果每次升级都得先擦除旧固件,万一断电就变砖了。后来换了512KB的,稳得很。
- RAM大小:OTA升级时,你得有个缓冲区吧?至少能放下一个Flash扇区的大小。有些MCU的RAM只有8KB,你想想看,一个扇区4KB,剩下4KB给协议栈和业务逻辑,够呛。
- USB外设:既然走Type-C,MCU最好自带USB Device控制器。用软件模拟USB虽然也能跑,但稳定性差一截。我踩过这个坑,模拟USB在高速传输时经常丢包,后来换了带硬件USB的型号,问题全消。
我的建议清单:
- Flash ≥ 512KB(保守点1MB)
- RAM ≥ 64KB
- 支持USB 2.0 FS/HS
- 有硬件CRC或加密加速器(OTA校验用得上)
2.2 Flash分区规划——给固件们分好房间
Flash就像一套房子,你得给Bootloader、App、配置参数、OTA临时区各分一个房间。分区规划不好,升级时就会互相踩踏。
我常用的分区方案是这样的:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动引导、OTA下载管理 |
| App Slot A | 0x08010000 | 256KB | 当前运行的固件 |
| App Slot B | 0x08050000 | 256KB | 备用固件(OTA下载目标) |
| Config | 0x08090000 | 16KB | 设备参数、升级标志 |
| OTA Temp | 0x08094000 | 48KB | 下载缓冲区(分包暂存) |
这里有个关键点:Bootloader分区必须放在Flash起始地址。因为MCU上电后默认从0x08000000取中断向量表。我曾经见过有人把Bootloader放在后面,结果芯片死活跑不起来——嗯,这就是不尊重硬件规矩的下场。
避坑指南:Config分区里一定要存一个「升级标志」。Bootloader启动时先读这个标志,如果为1,就执行OTA升级流程;如果为0,直接跳转到App。这样即使升级中途断电,下次上电Bootloader还能接着干。
2.3 Bootloader与App分离设计——各司其职
Bootloader和App为什么要分离?说白了,就是让升级这件事和业务逻辑解耦。Bootloader只管「怎么升级」,App只管「做什么功能」。
我设计Bootloader时,遵循三个原则:
- 最小化原则:Bootloader只包含USB驱动、Flash擦写、CRC校验、跳转逻辑。别把蓝牙协议栈、GUI库塞进去,那会撑爆分区。
- 容错设计:Bootloader必须能处理升级失败的情况。比如下载到一半断电,下次上电Bootloader检测到固件不完整,就擦掉重来。
- 版本兼容:Bootloader和App之间约定一个接口结构体,包含版本号、固件大小、校验值。App升级时先更新这个结构体,Bootloader再根据它做校验。
举个例子,Bootloader跳转到App的代码大概长这样:
/* 定义App起始地址 */
#define APP_ADDRESS 0x08010000
/* 定义App中断向量表指针类型 */
typedef void (*pFunction)(void);
void jump_to_app(void)
{
uint32_t app_stack;
pFunction app_entry;
/* 1. 关闭全局中断 */
__disable_irq();
/* 2. 从App起始地址读取栈顶指针 */
app_stack = *(uint32_t*)APP_ADDRESS;
/* 3. 从App起始地址+4读取复位向量 */
app_entry = (pFunction)(*(uint32_t*)(APP_ADDRESS + 4));
/* 4. 设置主栈指针 */
__set_MSP(app_stack);
/* 5. 跳转到App */
app_entry();
/* 不会执行到这里 */
while(1);
}
注意看,跳转前必须关中断。为什么?因为Bootloader的中断向量表和App的不一样,如果不关,跳转瞬间一个中断进来,MCU会跑到Bootloader的向量表里找处理函数,那就乱套了。我刚开始做时忘了这步,结果每次跳转都死机,查了两天才找到原因。
2.4 中断向量表重映射——让中断找到新家
App固件不在Flash起始地址运行,中断向量表自然也不在0x08000000。你得告诉MCU:「嘿,我的中断向量表搬家了,搬到0x08010000去了。」
不同MCU的做法不一样。以STM32为例,有个寄存器叫SCB->VTOR,专门干这个事:
/* 在App的main函数最开头执行 */
void main(void)
{
/* 重映射中断向量表到App分区起始地址 */
SCB->VTOR = 0x08010000;
/* 然后才能初始化外设、开中断 */
HAL_Init();
SystemClock_Config();
...
}
这里有个细节:重映射必须在任何中断使能之前完成。如果你先开了串口中断,再改VTOR,那中断来了还是去老地方找向量表,结果就是跑飞。我有个同事就犯过这个错,调试时发现串口收数据就死机,后来发现是VTOR设置顺序不对。
特别注意:有些MCU(比如某些国产Cortex-M0芯片)不支持VTOR寄存器。这时候你得用「固定偏移」的方式:编译App时,在链接脚本里把中断向量表的地址硬编码到0x08010000。这样MCU上电后虽然从0x08000000取向量表,但Bootloader跳转前已经把App的向量表复制到了0x08000000。嗯,这招有点绕,但确实管用。
另外,如果你用了RTOS,还得注意系统滴答定时器的中断。RTOS通常用SysTick做时间片调度,跳转后SysTick的向量地址变了,你得确保RTOS的初始化代码在VTOR设置之后执行。否则,滴答中断一来,系统就懵了。
总结一下:中断向量表重映射,说白了就是给MCU指个路。你告诉它「中断处理函数的新地址在这儿」,它就能找到正确的入口。顺序很重要——先改VTOR,再开中断,最后初始化RTOS。这个顺序我踩过三次坑,现在闭着眼睛都能背出来。