3、双备份机制:A/B分区切换原理

双备份机制,圈里人更喜欢叫它 A/B 系统。说白了,就是准备两套完整的固件,一套在跑,一套待命。我最早接触这个方案是在做车机项目的时候,当时客户要求 OTA 失败后必须能回滚,不然车开半路黑屏了,那可不是闹着玩的。

A/B分区切换原理

先看分区布局。Flash 里划出两个完全独立的区域,A 区和 B 区。每个区都包含 bootloader、kernel、rootfs 和 app 分区。系统启动时,bootloader 会检查一个叫 slot 状态的标记,决定从哪个区启动。

// 分区布局示意
// 每个 slot 都是完整的系统
struct partition_table {
    slot_a: {
        bootloader: 0x00000000 - 0x0001FFFF,
        kernel:     0x00020000 - 0x0003FFFF,
        rootfs:     0x00040000 - 0x0013FFFF,
        app:        0x00140000 - 0x0023FFFF
    },
    slot_b: {
        bootloader: 0x00240000 - 0x0025FFFF,
        kernel:     0x00260000 - 0x0027FFFF,
        rootfs:     0x00280000 - 0x0037FFFF,
        app:        0x00380000 - 0x0047FFFF
    }
};

切换流程其实不复杂。假设当前系统在 A 区运行,要升级到新版本。OTA 下载的新固件会直接写入 B 区。写入完成后,bootloader 把启动标记从 A 切换到 B,然后重启。如果 B 区能正常启动并运行,就确认切换成功。如果启动失败,bootloader 会自动切回 A 区。

核心要点:切换的关键在于 bootloader 的决策逻辑。它不看版本号,只看 slot 状态。状态包括:正常、待更新、更新中、失败回滚。

双备份的优缺点

先说说优点,这也是为什么很多工业级产品都在用。

  • 零停机升级:系统在 A 区跑着,B 区随便写。用户完全感觉不到升级过程。
  • 秒级回滚:新版本出问题?重启一下,bootloader 自动切回旧版本。我见过最快的一次回滚只用了 800 毫秒。
  • 升级失败零风险:写入过程中断电、断网,最多就是 B 区废了,A 区纹丝不动。

缺点也很明显,说白了就是「空间换安全」。

  • Flash 容量翻倍:你想想看,本来 8MB 就能装下的固件,现在得 16MB。对于成本敏感的 IoT 设备,这可不是小数目。
  • 升级流程复杂:需要管理 slot 状态、处理边界情况。我在项目中遇到过一个问题:B 区写了一半,用户强制重启,结果两个区都标记为「待更新」,系统直接变砖了。
  • 测试工作量翻倍:每个版本都要在两个 slot 上验证,兼容性测试也得做两遍。
对比项 单分区方案 A/B双备份
Flash占用 1x 2x
升级安全性 低(断电即变砖) 高(总有退路)
回滚速度 需重新刷写 秒级重启切换
实现复杂度 简单 中等

如何实现无缝切换

无缝切换,说白了就是让用户完全感知不到升级过程。我总结了几条实战经验。

第一,状态机要严谨。每个 slot 的状态转换必须原子操作。我习惯用这样的状态定义:

enum slot_state {
    SLOT_ACTIVE,      // 当前运行
    SLOT_INACTIVE,    // 空闲待用
    SLOT_UPDATING,    // 正在写入
    SLOT_PENDING,     // 等待验证
    SLOT_FAILED       // 验证失败
};

第二,写入策略要保守。不要一次性写完整个分区。我建议分块写入,每块写完后做 CRC 校验。如果某块校验失败,只重写这一块,不用从头再来。

// 分块写入示例
#define BLOCK_SIZE 4096

bool write_firmware_block(uint32_t offset, uint8_t* data, uint32_t len) {
    // 先擦除块
    if (!flash_erase(offset, BLOCK_SIZE)) {
        return false;
    }
    // 写入数据
    if (!flash_write(offset, data, len)) {
        return false;
    }
    // 读回验证
    uint8_t verify[BLOCK_SIZE];
    flash_read(offset, verify, len);
    if (memcmp(data, verify, len) != 0) {
        return false;  // 校验失败,重试
    }
    return true;
}

第三,切换时机要选对。不要收到新固件就立刻切换。我建议等系统空闲时再触发切换。比如在智能家居设备上,我会等用户离家后或者深夜再执行切换。

实战技巧:在切换前,先把当前系统的关键状态保存到非易失存储区。这样即使新系统启动失败,回滚后也能恢复到升级前的状态。我曾经因为没做这一步,回滚后设备配置全丢了,被客户骂了一周。

第四,验证机制要可靠。新系统启动后,不要立刻标记为「正常」。我习惯的做法是:先标记为「待验证」,运行一段时间(比如 5 分钟)后,如果没收到异常报告,再正式标记为「正常」。如果期间收到看门狗复位或者崩溃日志,立刻回滚。

避坑指南:我曾经遇到过一个问题:新系统启动后,所有功能都正常,但三天后突然死机。原因是新版本的内存泄漏问题,需要长时间运行才能暴露。所以,验证期不能太短,至少覆盖一个完整的使用周期。

最后说一句,双备份机制不是银弹。如果你的 Flash 容量实在不够,可以考虑压缩固件或者只备份关键分区。但如果你做的是医疗设备、汽车电子这类对可靠性要求极高的产品,别犹豫,上 A/B 系统。安全第一,空间第二。