4. 存储器架构:Flash分区策略与磨损均衡

做OTA Bootloader,说白了就是跟Flash打交道。

我刚开始接触这块时,觉得不就是读写Flash嘛,有啥难的?结果第一次做产品升级,分区没规划好,差点把整个产线搞崩。嗯,今天咱们就把Flash分区这件事彻底聊透。

4.1 Flash分区策略

一个典型的MCU OTA系统,Flash至少要分成四个区:Boot区、App区、备份区、参数区。我习惯这么划分:

分区名称 起始地址 大小 作用
Boot区 0x08000000 64KB 存放Bootloader代码,上电先执行
参数区 0x08010000 4KB 存储升级标志、版本号等配置
App区 0x08011000 384KB 主应用程序运行区
备份区 0x08071000 384KB 接收新固件,校验后覆盖App区

为什么参数区要单独拎出来?

你想想看,如果参数和App混在一起,每次升级App,参数就被擦掉了。我之前有个项目,设备校准数据存在App区末尾,升级一次就得重新校准,客户差点骂娘。

核心原则:Boot区只负责启动和升级,App区只跑业务逻辑,参数区存持久化数据,备份区做升级缓冲。各司其职,互不干扰。

4.2 Flash读写特性

Flash这东西,写之前必须先擦除。而且擦除是按扇区来的,不是按字节。

我记得第一次用STM32时,想改一个字节,直接调了写函数,结果啥也没发生。查了半天手册才发现——擦除是前提。

几个关键点:

  • 写操作:只能将1写成0,不能把0写成1。想恢复1?必须擦除整个扇区。
  • 擦除粒度:不同MCU不一样。STM32F1是2KB一个扇区,F4是128KB。选MCU时一定要看手册。
  • 写入粒度:通常是16位或32位。别想着按字节写,很多MCU不支持。
// 典型Flash写入流程(伪代码)
void flash_write(uint32_t addr, uint32_t *data, uint32_t len) {
    // 1. 解锁Flash
    flash_unlock();
    
    // 2. 擦除目标扇区
    flash_erase_sector(addr);
    
    // 3. 按32位写入
    for (int i = 0; i < len; i++) {
        flash_program_word(addr + i*4, data[i]);
    }
    
    // 4. 锁定Flash
    flash_lock();
}

注意:擦除操作很耗时。一个128KB的扇区,擦除可能要几百毫秒。升级过程中如果断电,数据就丢了。所以备份区的作用就在这里——先收完整固件,再擦除App区。

4.3 擦除寿命与磨损均衡

Flash有擦写寿命,一般是1万到10万次。听起来很多?

我做过一个数据采集设备,每5秒存一次参数。按1万次寿命算,不到14小时就挂了。这就是典型的「擦写寿命陷阱」。

怎么解决?磨损均衡。

说白了,就是不要把数据总写在一个地方。比如参数区有4KB,但每次只写一个扇区,写满了再换下一个。这样寿命就翻了N倍。

我常用的策略:

  1. 环形缓冲区:参数区划分成多个槽位,写满一个槽就换下一个。
  2. 版本号标记:每个槽位头部存一个递增的版本号,启动时读版本号最高的那个。
  3. 垃圾回收:当所有槽位都用完,擦除最旧的那个,重新开始。
// 磨损均衡示例:参数存储
#define SLOT_COUNT  8
#define SLOT_SIZE   512

typedef struct {
    uint32_t version;  // 递增版本号
    uint8_t  data[SLOT_SIZE - 4];
} param_slot_t;

uint32_t find_latest_slot(void) {
    uint32_t max_ver = 0;
    uint32_t best_slot = 0;
    
    for (int i = 0; i < SLOT_COUNT; i++) {
        param_slot_t *slot = (param_slot_t *)(PARAM_BASE + i * SLOT_SIZE);
        if (slot->version > max_ver) {
            max_ver = slot->version;
            best_slot = i;
        }
    }
    return best_slot;
}

避坑指南:我曾经在磨损均衡里忘了处理「所有槽位版本号溢出」的情况。版本号从0xFFFFFFFF回滚到0,结果系统认为新数据是旧数据,直接用了垃圾数据。解决方案:版本号用64位,或者检测到溢出时强制擦除所有槽位。

4.4 实际项目中的分区考量

分区大小不是拍脑袋定的。我一般这么算:

  • Boot区:够放Bootloader代码就行,通常64KB~128KB。别贪大,浪费Flash。
  • App区:根据实际固件大小,留20%余量。比如固件300KB,App区给384KB。
  • 备份区:和App区一样大。因为要存完整的新固件。
  • 参数区:4KB~16KB。配合磨损均衡,够用很久。

嗯,分区这事,前期多花点时间,后期少掉很多头发。我见过太多项目,分区不合理,最后只能改硬件、换MCU,成本翻倍。

记住一句话:Flash分区是OTA的骨架,骨架歪了,肉长再好也没用。


公众号:蓝海资料掘金营,微信deep3321