4、App分区管理:双备份(A/B)系统原理、切换策略、状态标记

双备份系统,圈里人常说的 A/B 系统,其实不是什么新概念。但真正把它用好在 OTA 升级里,坑不少。我最早接触这个,是在一个智能网关项目上。当时设备升级失败率高达 15%,用户投诉不断。后来上了 A/B 分区方案,失败率直接降到 0.5% 以下。嗯,今天咱们就聊聊这里面的门道。

4.1 双备份系统原理

说白了,A/B 系统就是给 App 准备了两套分区。一套跑着,一套等着。你想想看,升级的时候,设备还在用老系统正常工作。新固件下载到备用分区,校验通过后,下次重启才切过去。整个过程对用户来说,几乎无感。

我习惯把这两个分区叫做 slot_a 和 slot_b。它们的大小完全一样,各自包含完整的系统、App 和数据分区。结构上大概是这样的:

核心思想:任何时候,至少有一个分区是可启动的。升级失败不会导致设备变砖。

A/B 双分区结构图 Slot A(当前运行) Bootloader App 固件 v2.1 用户数据 Slot B(备用/升级目标) Bootloader App 固件 v2.2(新) 用户数据(待迁移) 升级时写入 Slot B,重启后 Bootloader 根据状态标记决定启动哪个 Slot

这里有个关键点:Bootloader 必须支持 A/B 切换逻辑。它得能读懂状态标记,知道该启动哪个分区。我在一个项目里见过,Bootloader 代码里硬编码了启动分区,结果 A/B 功能根本用不了。那真是欲哭无泪。

4.2 切换策略

切换策略,说白了就是「什么时候切,怎么切」。我总结下来,主流的有三种:

策略名称 触发时机 优点 缺点
立即切换 升级完成后立即重启 用户等待时间短 新版本有问题会立即暴露
延迟切换 下次设备重启时 给用户缓冲时间 可能忘记切换
条件切换 满足特定条件(如空闲、电量充足) 风险最低 实现复杂

我个人比较推荐 条件切换 + 健康检查 的组合。具体流程是这样的:

  1. 新固件下载到备用分区,校验通过。
  2. 设置「待切换」标记,但不立即重启。
  3. 等待设备空闲(比如没有用户操作、网络流量低)。
  4. 检查电量是否充足(我一般设 30% 以上)。
  5. 执行重启,Bootloader 读取标记,启动新分区。
  6. 新系统启动后,运行自检程序。如果自检通过,提交「成功」标记。
  7. 如果自检失败或超时,自动回滚到旧分区。

我的经验:健康检查这一步千万别省。我曾经在一个项目里,新系统启动后网络模块初始化失败,但因为没做健康检查,设备直接「失联」了。后来加了 30 秒超时检测,问题才解决。

4.3 状态标记

状态标记是 A/B 系统的「大脑」。Bootloader 全靠它来做决策。我习惯把标记存储在 Bootloader 专用的 misc 分区 里,这样即使系统崩溃,标记也不会丢。

常用的状态标记有这些:

标记名称 值 含义
active_slot 0 / 1 当前启动的是 Slot A 还是 Slot B
boot_attempts 0-3 当前分区已尝试启动的次数
update_pending true / false 是否有待完成的升级
update_result success / failed / unknown 上次升级的结果
rollback_count 0-N 回滚次数,防止无限回滚

代码实现上,我一般用一个结构体来管理:

// 状态标记结构体
typedef struct {
    uint8_t active_slot;        // 0: Slot A, 1: Slot B
    uint8_t boot_attempts;      // 启动尝试次数
    bool    update_pending;     // 升级待处理
    uint8_t update_result;      // 0: unknown, 1: success, 2: failed
    uint8_t rollback_count;     // 回滚计数
    uint32_t crc32;             // 结构体校验
} ab_status_t;

// 读取状态标记
ab_status_t* get_ab_status(void) {
    // 从 misc 分区读取
    return (ab_status_t*)MISC_PARTITION_ADDR;
}

// 写入状态标记
void set_ab_status(ab_status_t* status) {
    status->crc32 = calculate_crc32(status, sizeof(ab_status_t) - 4);
    write_to_misc_partition(status, sizeof(ab_status_t));
}

注意:CRC32 校验不能省。我遇到过 flash 位翻转导致标记错误,设备反复在两个分区之间来回切换。加上校验后,一旦数据损坏,Bootloader 会使用默认值,至少保证能启动。

4.4 回滚机制

回滚是 A/B 系统的最后一道防线。我见过不少方案,回滚逻辑写得过于简单,结果越滚越乱。

我的做法是:

  • 限制回滚次数:最多回滚 3 次。超过 3 次,设备进入恢复模式,等待人工干预。
  • 记录回滚原因:是启动超时?还是健康检查失败?记录下来,方便远程排查。
  • 保留旧分区:回滚后不立即擦除新分区。万一新版本只是偶发问题,还可以再试一次。

嗯,这里要特别提一下。回滚不是简单的「切回去就完事了」。你得考虑用户数据的一致性。比如新版本改了数据库结构,回滚后旧版本可能读不了新数据。我一般会在升级前备份用户数据,回滚时一并恢复。

4.5 实际部署建议

最后,给几个我在项目中踩过的坑,算是避坑指南吧:

  • 分区大小要留余量:固件总会越写越大。我建议每个分区至少留 20% 的余量。
  • Bootloader 要支持双分区:别像我一样,做到一半才发现 Bootloader 不支持。
  • 测试要覆盖回滚场景:不只是测升级成功,更要测升级失败后的回滚。
  • 日志要持久化:把升级日志写到独立的日志分区,方便事后分析。

一句话总结:A/B 系统不是银弹,但用好了,能让你的 OTA 升级成功率从 85% 提升到 99.5% 以上。关键是原理要懂,策略要合理,标记要可靠。