4. 备份机制设计:双备份区(A/B分区)原理、备份区切换逻辑

说到固件升级,我最怕什么?不是升级失败,而是升级失败后设备变砖。你想想看,一个智能电表或者工业控制器,OTA 到一半断电了,重启后系统起不来——这种事故要是发生在现场,那可真叫一个头疼。

双备份区,说白了就是给系统准备了两套完整的固件镜像。一套跑着,一套等着。我习惯叫它 A/B 分区方案,也叫无缝升级。这玩意儿在手机行业早就普及了,但在嵌入式领域,很多人还是舍不得那点 Flash 空间。我个人觉得,这钱不能省。

4.1 双备份区的基本原理

先看一张图,把整体架构搞清楚。

双备份区(A/B分区)架构图 Bootloader 选择 启动分区 分区 A(当前运行) 版本 v2.1.0 分区 B(待升级) 版本 v2.2.0 升级管理器 说明:Bootloader 根据标志位选择启动分区,升级管理器将新固件写入非活动分区

这张图里,Bootloader 是决策者。它不看心情,只看一个叫「启动标志位」的东西。标志位指向 A,就从 A 启动;指向 B,就从 B 启动。升级管理器负责把新固件写到当前不用的那个分区里。

核心要点:任何时候,至少有一个分区是完整可用的。这就是双备份区最大的价值——永不掉线。

4.2 分区布局与元数据

分区怎么划分?我一般这样安排 Flash 布局:

区域 起始地址 大小 内容
Bootloader 0x08000000 64 KB 启动加载程序 + 分区表
分区 A 0x08010000 512 KB 固件镜像 A + 元数据
分区 B 0x08090000 512 KB 固件镜像 B + 元数据
共享存储区 0x08110000 64 KB 配置参数、日志、标志位

每个分区头部都有一小块元数据区域,记录着:

  • 固件版本号——用来判断哪个分区更新
  • CRC32 校验值——启动时验证固件完整性
  • 升级状态标志——标记该分区是否完成升级
  • 启动计数——记录该分区已成功启动的次数

嗯,这里要注意。元数据一定要放在分区的最前面,并且用两个备份。我在项目中遇到过,Flash 写入到一半断电,元数据损坏了,结果 Bootloader 读到一个乱码,直接不知道该启动谁。后来我改成双备份元数据——写两份,启动时对比,不一致就用备份的。

4.3 备份区切换逻辑

切换逻辑其实不复杂,但细节决定成败。我把它拆成几个步骤:

  1. 升级触发:系统收到新固件包,校验签名和完整性。
  2. 确定目标分区:查看当前运行的是 A 还是 B,目标就是另一个。
  3. 写入固件:按块写入目标分区,每写完一块做一次 CRC 校验。
  4. 标记待验证:写入完成后,将目标分区的状态设为「待验证」。
  5. 切换标志位:修改启动标志位,指向新分区。
  6. 复位重启:系统复位,Bootloader 读取标志位,从新分区启动。
  7. 健康检查:新分区启动后,运行自检程序。如果通过,将状态改为「已验证」。
  8. 回滚(如果失败):如果自检失败或超时,Bootloader 自动切回旧分区。

我的习惯:在健康检查阶段,我会让应用层上报一个「启动成功」的心跳。如果 30 秒内没收到心跳,Bootloader 就认为新固件有问题,自动回滚。这个机制救过我两次。

4.4 回滚机制与防砖保护

回滚是双备份区的灵魂。没有回滚,双备份就只是个双倍浪费。回滚逻辑通常放在 Bootloader 里:

/* 伪代码:Bootloader 启动决策逻辑 */
uint8_t select_boot_partition(void) {
    uint8_t active = read_boot_flag();  // 读取启动标志位

    // 检查目标分区的元数据
    if (is_partition_valid(active)) {
        // 检查该分区是否标记为"待验证"
        if (get_partition_status(active) == STATUS_PENDING) {
            // 首次启动,启动计数 +1
            increment_boot_count(active);
            return active;
        }
        return active;
    }

    // 目标分区无效,尝试另一个
    uint8_t backup = (active == PARTITION_A) ? PARTITION_B : PARTITION_A;
    if (is_partition_valid(backup)) {
        // 记录回滚事件
        log_rollback_event(active, backup);
        set_boot_flag(backup);
        return backup;
    }

    // 两个分区都坏了... 进入恢复模式
    enter_recovery_mode();
    return PARTITION_NONE;
}

这段代码看着简单,但有个坑。我曾经遇到过,分区 A 的元数据被写坏了,但固件本身是好的。Bootloader 判断分区 A 无效,切到了 B。结果 B 是个旧版本,功能不全。用户反馈说设备异常,我排查了半天才发现是元数据损坏导致的误判。

避坑指南:我曾经在元数据校验上吃过亏。不要只校验元数据,还要对固件主体做快速 CRC 扫描。如果元数据坏了但固件完好,应该允许修复元数据后继续使用该分区。

4.5 升级过程中的异常处理

升级过程中可能出现的异常,我列了个清单:

异常场景 影响 处理策略
写入中途断电 目标分区固件不完整 重启后 Bootloader 发现 CRC 不匹配,自动切回旧分区
写入时 Flash 坏块 部分数据无法写入 跳过坏块,记录坏块表;如果坏块过多,放弃升级
新固件启动崩溃 系统反复重启 Bootloader 检测到启动计数异常,回滚到旧分区
标志位写入失败 无法切换分区 保持当前分区,下次升级时重试
两个分区都损坏 设备变砖 进入 DFU 模式,等待外部恢复

你想想看,如果没有双备份,上面任何一种异常都可能导致设备变砖。有了双备份,最坏的情况也就是回滚到旧版本,设备还能用。

4.6 实际项目中的经验总结

做了这么多年固件,我总结了几条关于双备份区的铁律:

  • 永远不要覆盖正在运行的分区。这是底线。升级管理器必须确认当前运行的是 A,才能写 B;反之亦然。
  • Bootloader 要足够简单。Bootloader 越复杂,出 bug 的概率越大。我只让它做三件事:读标志位、校验分区、跳转。别的什么都不干。
  • 标志位要有冗余。我习惯在 Flash 的三个不同位置各存一份标志位,启动时三取二投票。Flash 的位翻转不是闹着玩的。
  • 升级过程中不要依赖网络。如果升级到一半网络断了,设备应该能正常回滚,而不是卡在半死不活的状态。

一句话总结:双备份区不是用来「升级」的,是用来「兜底」的。它让你在升级失败时,还有一条退路。