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,至少选一个
警告:Bootloader千万不要依赖App的代码或数据。我见过有人偷懒,Bootloader里直接调用App的函数,结果App升级失败后,Bootloader也跟着挂了。这叫“耦合致死”,切记!

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布局这件事,说难不难,说简单也不简单。核心就三点:

  1. 提前规划:画好分区图,留足余量
  2. 冗余设计:永远给自己留一条后路
  3. 严格校验:魔数、CRC、版本号,一个都不能少

你想想看,如果连Flash布局都没想清楚,后面的升级流程、断点续传、安全校验,全都是空中楼阁。所以,花时间把这一步做扎实了,后面会省心很多。

好,这一章就到这里。下一章我们聊聊升级协议的选择——YModem、HTTP还是自定义?到时候见。