banner
NEWS LETTER

嵌入式系统 OTA 升级方案设计:A/B 分区与回滚机制

Scroll down

嵌入式设备一旦部署到现场,固件升级就变成了一项高频且高风险的操作。传统的单分区原地升级(in-place upgrade)在写入过程中遭遇断电或异常,设备可能变砖,恢复成本极高。A/B 分区方案通过保留两份完整的系统镜像,将升级风险从”写入失败”降级为”切换失败”,配合可靠的回滚机制,能显著提升 OTA 的成功率和设备可用性。本文将围绕 A/B 分区的设计思路、落地步骤和常见陷阱展开讨论。

一、问题背景

OTA(Over-The-Air)升级是嵌入式产品生命周期管理的核心能力。无论是 IoT 网关、工业控制器还是车载终端,固件更新都是修复漏洞、推送功能迭代的必经之路。单分区方案在写入过程中如果发生掉电、存储介质损坏或校验失败,设备将停留在不完整的镜像上,无法正常启动。这类故障在实验室里可控,但在大规模部署场景中几乎必然出现。A/B 分区方案的核心目标是:任何时刻设备都持有一份完整可启动的镜像,升级只是在”下一版本镜像”就绪后做一次原子切换。

二、核心思路

  • 双分区冗余:将存储空间划分为 A、B 两个等大的系统分区,各自包含完整的 rootfs 和内核。Bootloader 始终从标记为”活动”的分区启动。
  • 后台写入:新固件在当前活动分区运行期间,下载并写入非活动分区,不打断设备正常工作。
  • 原子切换:写入完成后,更新 Bootloader 中的活动标记,下次重启即切换到新分区。若新分区启动失败或校验不通过,自动回滚到上一个已知良好的分区。
  • 状态机管理:通过一个持久化的升级状态(通常存在 EEPROM 或专用分区)记录”下载中→写入中→待验证→已激活→已确认”等阶段,确保断电后状态可恢复。
  • 回滚触发条件:新分区连续 N 次启动失败(看门狗超时)、应用层校验不通过、或人工主动回滚。

三、落地步骤

  1. 规划分区表

    在存储介质(eMMC、NAND、SPI Flash)上划分 Bootloader、A 分区、B 分区、数据分区。以 eMMC + U-Boot 为例:

    1
    2
    3
    4
    5
    6
    # 使用 gdisk 或设备树中的分区表定义
    # 典型布局(以 4GB eMMC 为例):
    # Bootloader: 0x0000_0000 - 0x0007_FFFF (512KB)
    # A 分区: 0x0008_0000 - 0x0FFF_FFFF (~1.5GB)
    # B 分区: 0x1000_0000 - 0x1FFF_FFFF (~1.5GB)
    # 数据分区: 0x2000_0000 - 0x7FFF_FFFF (~2GB)
  2. 修改 Bootloader 支持 A/B 选择

    在 U-Boot 中添加启动选择逻辑,读取升级状态标志:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* U-Boot cmd/bmp.c 或自定义 cmd 中 */
    int ab_select_boot_slot(void) {
    uint8_t slot;
    /* 从 EEPROM 或环境变量读取当前活动槽位 */
    env_get_u8("ab_active_slot", &slot);
    if (slot != 0 && slot != 1)
    slot = 0; /* 默认 A 槽 */
    return slot;
    }
  3. 实现升级状态机

    定义状态枚举,写入非易失存储:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    typedef enum {
    OTA_STATE_IDLE = 0,
    OTA_STATE_DOWNLOADING,
    OTA_STATE_WRITING,
    OTA_STATE_VERIFYING,
    OTA_STATE_BOOT_PENDING,
    OTA_STATE_CONFIRMED,
    OTA_STATE_ROLLBACK
    } ota_state_t;

    void ota_set_state(ota_state_t state) {
    /* 写入 EEPROM,带 CRC 校验 */
    eeprom_write(OTA_STATE_ADDR, &state, sizeof(state));
    }
  4. 后台写入流程

    当前系统检测到新固件可用后,确定非活动分区并逐块写入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 伪代码:写入非活动分区
    def write_to_inactive(new_fw_path, inactive_partition):
    with open(new_fw_path, 'rb') as fw:
    block = fw.read(BLOCK_SIZE)
    offset = 0
    while block:
    write_block(inactive_partition, offset, block)
    offset += len(block)
    block = fw.read(BLOCK_SIZE)
    # 写入完成,更新状态
    ota_set_state(OTA_STATE_VERIFYING)
  5. 实现回滚逻辑

    在 Bootloader 或 init 脚本中检测启动成功条件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/bin/sh
    # /etc/init.d/ota-confirm.sh
    # 设备启动后由应用层调用,确认系统正常
    if [ "$1" = "confirm" ]; then
    ota_set_state CONFIRMED
    echo "OTA confirmed, slot is stable."
    elif [ "$1" = "rollback" ]; then
    # 切换活动标记到另一分区
    flip_active_slot
    reboot
    fi
  6. 添加看门狗保护

    内核启动后在用户空间喂狗,若应用层未在规定时间内确认,触发重启并切换分区。

四、常见坑

  • 分区大小不一致:A 和 B 分区容量必须严格一致,否则镜像打包和校验逻辑容易出错。部署前用工具校验分区对齐。
  • 数据分区共享陷阱:升级后新旧固件对数据分区的格式假设可能不同,务必在数据分区中保存版本兼容性标记,升级脚本需处理前向兼容。
  • 校验覆盖不全:只校验 rootfs 不校验内核或设备树,可能导致部分更新后启动异常。校验应覆盖所有被替换的组件。
  • Bootloader 自身不升级:Bootloader 分区通常不纳入 OTA 范围,但 Bootloader 的 bug 会导致整个升级流程失效。需要单独的 Bootloader 升级通道(如 UART DFU)作为兜底。
  • 写入性能瓶颈:NAND Flash 的写入速度有限,大镜像后台写入可能持续数十分钟,期间需处理存储磨损均衡和坏块管理。
  • 时钟漂移与证书过期:OTA 下载依赖 HTTPS,设备时钟不准会导致 TLS 握手失败。首次启动或长时间离线后需同步时间。

五、检查清单

  • 分区表已定义,A/B 分区大小一致且对齐
  • Bootloader 能正确读取并切换活动槽位
  • 升级状态机覆盖所有阶段,断电后状态可恢复
  • 校验逻辑覆盖 rootfs、内核、设备树等全部组件
  • 回滚条件明确(启动失败次数阈值、应用层校验)
  • 看门狗保护已配置,超时后触发重启与回滚
  • 数据分区版本兼容性方案已确认
  • Bootloader 自身升级通道已验证
  • 升级包签名与验证机制已集成
  • 已在目标硬件上完成断电、异常断网等异常场景测试

六、小结

A/B 分区方案的本质是用存储空间换取升级安全性。它不消除所有故障,但将故障的影响从”设备变砖”缩小为”自动回滚到旧版本”,这对大规模部署的嵌入式产品而言是值得的取舍。设计时需要关注状态机的完整性、校验覆盖的全面性、以及 Bootloader 兜底通道的可用性。实际落地建议先在少量设备上跑通完整生命周期,再逐步扩大 OTA 覆盖范围。

其他文章
目录导航 置顶
  1. 1. 一、问题背景
  2. 2. 二、核心思路
  3. 3. 三、落地步骤
  4. 4. 四、常见坑
  5. 5. 五、检查清单
  6. 6. 六、小结
请输入关键词进行搜索