4. 存储与校验优化:双备份机制、校验算法与Flash磨损均衡

固件升级最怕什么?升级到一半断电,设备变砖。我早年做消费类产品时,就吃过这个亏。从那以后,双备份机制就成了我设计升级方案时的底线。今天咱们就聊聊存储与校验这块的三个核心话题:A/B分区怎么玩、校验算法怎么选、Flash磨损怎么均衡。

4.1 双备份机制(A/B分区)

说白了,A/B分区就是准备两份固件。一份在跑,一份待命。升级时只写备用分区,写完了校验通过,再切换启动。这样就算升级中途断电,设备重启后还能从老分区正常启动。

核心原则:永远保证至少有一个可启动的固件副本。

4.1.1 分区布局

我习惯把Flash分成这么几块:

  • Bootloader:负责引导,也负责判断从哪个分区启动
  • 分区A(Slot A):当前运行固件
  • 分区B(Slot B):备用固件
  • 元数据区:记录当前激活分区、版本号、校验值等

举个例子,一个8MB Flash的典型布局:

区域 起始地址 大小 说明
Bootloader 0x000000 64KB 引导程序,不可变
元数据区 0x010000 4KB 分区状态、版本、校验值
分区A 0x011000 3.5MB 固件A
分区B 0x3A1000 3.5MB 固件B
用户数据区 0x732000 剩余 配置、日志等

4.1.2 升级流程

我建议的升级流程是这样的:

  1. Bootloader读取元数据,确定当前激活分区(比如A)
  2. 下载新固件到备用分区(B),边下载边校验
  3. 全部写入完成后,对B分区做一次完整校验
  4. 校验通过,更新元数据,将激活分区标记为B
  5. 系统重启,Bootloader发现B是激活分区,从B启动
  6. 如果B启动失败,Bootloader回退到A

我的习惯:在元数据区里多存一份备份。万一元数据区被写坏了,还能从备份恢复。我曾经遇到过Flash某块坏掉导致元数据丢失的情况,从那以后我就坚持双份元数据。

4.2 校验算法选择

校验算法这事儿,说白了就是平衡。你要在安全性和性能之间找个合适的点。我见过有人为了省时间用简单的累加和,结果固件被篡改了都不知道。也见过有人不管什么场景都上SHA256,结果升级慢得要命。

4.2.1 常见算法对比

算法 输出长度 计算速度 安全性 适用场景
CRC32 4字节 极快 低(防随机错误) 传输校验、Flash完整性检查
MD5 16字节 快 中(已不推荐安全场景) 非安全场景的固件校验
SHA256 32字节 较慢 高(抗碰撞) 安全升级、签名验证

4.2.2 我的选择策略

我个人习惯分层使用:

  • 传输层:用CRC32。每收到一个包就计算一次,发现错误立即重传。速度快,够用。
  • 存储层:用CRC32或MD5。写入Flash后做一次校验,确保数据没写错。
  • 安全层:用SHA256。配合签名,确保固件来源可信。

你想想看,如果每包都用SHA256,MCU算力有限,升级时间会翻好几倍。但如果你完全不校验,万一Flash有个坏块,设备就挂了。

注意:CRC32只能检测随机错误,不能防恶意篡改。如果有人故意修改固件,CRC32很容易被绕过。安全场景必须上SHA256+签名。

4.2.3 代码示例:边下载边校验

// 伪代码:边下载边计算CRC32
uint32_t crc = 0;
uint32_t total_bytes = 0;

while (receive_packet(&packet)) {
    // 写入Flash
    flash_write(packet.addr, packet.data, packet.len);
    
    // 更新CRC
    crc = crc32_update(crc, packet.data, packet.len);
    total_bytes += packet.len;
    
    // 进度反馈
    report_progress(total_bytes, firmware_size);
}

// 下载完成后,对比CRC
if (crc != expected_crc) {
    // 校验失败,回滚
    rollback();
    return ERROR_CRC_MISMATCH;
}

4.3 Flash磨损均衡

Flash的擦写次数是有限的。NOR Flash一般10万次,NAND Flash只有几千到几万次。如果你每次都往同一个地址写元数据,那个块很快就会报废。

4.3.1 磨损均衡策略

我常用的方法有几种:

  • 元数据轮转:在元数据区里划出多个槽位,轮流使用。每次更新元数据时写到下一个槽位。
  • 动态映射:用一张映射表,把逻辑地址映射到物理地址。每次写操作都选一个擦写次数最少的块。
  • 分区交换:A/B分区本身就有均衡效果。每次升级写的是备用分区,两个分区轮流被写。

4.3.2 元数据轮转实现

举个例子,元数据区有4个槽位,每个槽位4KB:

// 元数据槽位结构
typedef struct {
    uint32_t magic;          // 魔数,标识有效槽位
    uint32_t sequence;       // 序列号,越大越新
    uint32_t active_slot;    // 激活分区(0=A, 1=B)
    uint32_t firmware_crc;   // 固件CRC
    uint32_t reserved[60];   // 保留
} metadata_slot_t;

// 查找最新槽位
int find_latest_slot(void) {
    int latest_idx = -1;
    uint32_t max_seq = 0;
    
    for (int i = 0; i < NUM_SLOTS; i++) {
        metadata_slot_t slot;
        flash_read(SLOT_ADDR(i), &slot, sizeof(slot));
        
        if (slot.magic == MAGIC_NUMBER && slot.sequence > max_seq) {
            max_seq = slot.sequence;
            latest_idx = i;
        }
    }
    return latest_idx;
}

// 写入新元数据
void write_metadata(metadata_slot_t *new_data) {
    int next_slot = (find_latest_slot() + 1) % NUM_SLOTS;
    new_data->sequence = get_next_sequence();
    
    // 擦除槽位
    flash_erase(SLOT_ADDR(next_slot));
    // 写入新数据
    flash_write(SLOT_ADDR(next_slot), new_data, sizeof(*new_data));
}

避坑指南:我曾经遇到过一个问题——元数据槽位擦除后还没写完就断电了,结果所有槽位都无效。后来我加了个"写入中"标志,先写标志再写数据,写完再清除标志。这样就算断电,也能识别出哪个槽位是完整的。

4.4 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,双备份、校验、磨损均衡这三块是互相配合的。

存储与校验优化知识体系 存储与校验优化核心逻辑 双备份机制 校验算法 Flash磨损均衡 A/B分区布局 升级流程与回滚 元数据管理 CRC32 / MD5 / SHA256 分层校验策略 边下载边校验 元数据轮转 动态映射 分区交换 可靠固件升级 安全 · 快速 · 耐用 三者缺一不可:双备份保证可用性,校验保证完整性,磨损均衡保证寿命

4.5 总结

嗯,这一章的内容就这些。双备份机制是底线,校验算法要分层选,磨损均衡不能忽视。我做了这么多年固件,见过太多因为省事而翻车的案例。你想想看,一个产品卖出去几万台,如果因为升级失败导致批量变砖,那损失可不是一星半点。

所以我的建议是:别在这三个地方省钱。该做的备份要做,该算的校验要算,该均衡的磨损要均衡。这样你的固件升级方案才算真正靠谱。