3. 固件存储与分区设计:Flash布局、Bootloader与App分区、冗余设计
各位同学,今天我们来聊聊固件升级里最基础、也最要命的一环——Flash怎么布局。
说白了,就是你的代码往哪放,Bootloader占多大地方,App又该放哪。我见过不少项目,前期没规划好Flash分区,后期升级时各种踩坑。嗯,咱们今天就把这事彻底讲透。
3.1 Flash布局的基本原则
Flash不像RAM,它不能随便改。你得提前画好一张“地图”。我个人习惯,拿到芯片第一件事,就是打开数据手册,看Flash总大小和扇区大小。
举个例子,假设你用的是256KB Flash,扇区大小4KB。那我会这样分:
0x00000000 - 0x00003FFF (16KB) Bootloader
0x00004000 - 0x00007FFF (16KB) 参数存储区
0x00008000 - 0x00027FFF (128KB) App主分区
0x00028000 - 0x0003FFFF (96KB) App备份分区
为什么要留备份分区?你想想看,升级过程中突然断电了怎么办?没有备份,设备就变砖了。我在项目中遇到过,客户现场升级到一半停电,整个产线停了三天。从那以后,我再也不敢省备份分区。
3.2 Bootloader分区设计
Bootloader是固件升级的“守门员”。它负责启动、校验、跳转。我建议Bootloader分区至少包含以下功能:
- 启动向量表:中断向量表必须放在开头,这是硬性要求
- 升级协议栈:比如YModem、HTTP、或者自定义协议
- Flash擦写驱动:注意,Bootloader里的驱动要独立于App
- 校验算法:CRC32或SHA256,至少选一个
3.3 App分区设计
App分区就是你的业务代码。但这里有个细节——中断向量表重映射。
大多数ARM芯片,默认中断向量表在0x00000000。但你的App在0x00008000,怎么办?你需要修改VTOR寄存器(Vector Table Offset Register)。
// 以STM32为例
SCB->VTOR = 0x00008000; // 重映射中断向量表到App起始地址
嗯,这行代码必须在App启动的最早阶段执行。我建议放在复位处理函数的第一条语句。否则,一旦发生中断,CPU会跑到Bootloader的向量表里去,那可就乱套了。
3.4 冗余设计:A/B分区与回滚机制
冗余设计,说白了就是“双保险”。我常用的方案有两种:
| 方案 | 描述 | 适用场景 |
|---|---|---|
| A/B分区 | 两个完全相同的App分区,轮流运行 | 对可靠性要求极高的工业设备 |
| 备份+回滚 | 一个主分区,一个备份分区,升级失败自动回滚 | 大多数嵌入式设备 |
我个人更推荐A/B分区。为什么?因为升级过程中如果发生意外,系统可以直接切到另一个分区,用户完全无感。我曾经给一个电力终端做设计,要求升级失败后必须在100ms内恢复通信。A/B分区是唯一的选择。
核心要点:冗余设计不是浪费空间,而是买保险。你省下的那几十KB Flash,可能换来的是整个产线的停摆。
3.5 实战:Flash分区表代码示例
下面是我在一个项目中实际用过的分区表定义。你可以直接拿来改:
// flash_layout.h
#ifndef FLASH_LAYOUT_H
#define FLASH_LAYOUT_H
#define FLASH_TOTAL_SIZE (256 * 1024) // 256KB
#define FLASH_SECTOR_SIZE (4 * 1024) // 4KB
// 分区起始地址
#define BOOTLOADER_START 0x00000000
#define BOOTLOADER_SIZE (16 * 1024) // 16KB
#define PARAM_START 0x00004000
#define PARAM_SIZE (16 * 1024) // 16KB
#define APP_PRIMARY_START 0x00008000
#define APP_PRIMARY_SIZE (128 * 1024) // 128KB
#define APP_BACKUP_START 0x00028000
#define APP_BACKUP_SIZE (96 * 1024) // 96KB
// 校验用的魔数
#define APP_MAGIC 0xA5A5A5A5
typedef struct {
uint32_t magic;
uint32_t version;
uint32_t crc32;
uint32_t size;
} app_header_t;
#endif
注意看那个app_header_t结构体。每个App分区开头,我都会放一个这样的头部。Bootloader启动时,先读这个头部,校验魔数和CRC。魔数不对?说明分区是空的或者损坏了,直接切到备份分区。
小技巧:魔数不要用0x00000000或0xFFFFFFFF,因为Flash擦除后是全0或全1。用0xA5A5A5A5这种“花哨”的值,一眼就能看出分区是否被初始化过。
3.6 避坑指南:我踩过的那些坑
做固件升级这么多年,有些坑我是一路踩过来的。今天分享几个典型的:
- 扇区对齐问题:Flash擦除是按扇区来的。如果你的分区起始地址不是扇区对齐的,擦除时会误伤邻居。我曾经因为这个,把参数存储区擦了个干净,设备重启后所有校准数据都没了。
- 中断向量表对齐:有些芯片要求向量表必须对齐到512字节或1024字节。不对齐?直接硬件异常。嗯,这个坑我帮你们踩过了。
- Bootloader自更新:除非你有两个Bootloader分区,否则别想着在线更新Bootloader。一旦失败,芯片就是一块砖。我见过有人试图这么做,结果JTAG都救不回来。
3.7 总结
Flash布局这件事,说难不难,说简单也不简单。核心就三点:
- 提前规划:画好分区图,留足余量
- 冗余设计:永远给自己留一条后路
- 严格校验:魔数、CRC、版本号,一个都不能少
你想想看,如果连Flash布局都没想清楚,后面的升级流程、断点续传、安全校验,全都是空中楼阁。所以,花时间把这一步做扎实了,后面会省心很多。
好,这一章就到这里。下一章我们聊聊升级协议的选择——YModem、HTTP还是自定义?到时候见。