18、OTA升级与回滚:boot_control、slot切换、回滚保护

OTA升级,说白了就是让手机“空中换血”。但换血有风险,万一换到一半断电了、文件损坏了,手机变砖怎么办?这就是我们今天要聊的核心——boot_control、slot切换和回滚保护

我个人习惯把这套机制叫做“双保险”。Android从7.0开始引入A/B分区(也叫无缝升级),就是为了解决升级失败的问题。你想想看,系统有两套分区,一套在跑,一套在升级,互不干扰。就算升级失败,大不了切回旧分区,手机照样能用。

18.1 boot_control HAL:谁来管slot切换?

boot_control是一个硬件抽象层(HAL),它负责管理slot的切换。说白了,它就是系统里那个“管钥匙的人”。

我在项目中遇到过一个问题:升级完成后,系统重启了,但死活不切到新分区。查了半天,发现是boot_control HAL的实现里,setActiveBootSlot() 没有正确写入misc分区。嗯,这里要注意,misc分区是存储slot信息的关键区域,写错了就全白搭。

来看一个典型的boot_control HAL接口定义:

// hardware/interfaces/boot/1.0/IBootControl.hal
interface IBootControl {
    // 获取当前活跃的slot
    getActiveBootSlot() generates (uint32_t slot);
    
    // 设置当前活跃的slot
    setActiveBootSlot(uint32_t slot) generates (bool success);
    
    // 标记当前slot为成功启动
    markBootSuccessful() generates (bool success);
    
    // 获取slot数量(通常是2)
    getNumSlots() generates (uint32_t numSlots);
    
    // 判断当前slot是否可以启动
    isSlotBootable(uint32_t slot) generates (bool bootable);
};

每个方法都很关键。比如markBootSuccessful(),它决定了系统是否“确认”当前slot是好的。如果升级后第一次启动没调用这个,下次重启可能又会切回旧slot。

18.2 Slot切换:升级的“换手”逻辑

OTA升级时,系统会把新固件写入非活跃slot(比如当前是slot A,就写slot B)。写完后,设置slot B为活跃,然后重启。

重启后,bootloader会检查misc分区里的slot信息,决定从哪个slot启动。如果一切正常,系统启动后,init进程会调用markBootSuccessful(),确认新slot可用。

我画了一张流程图,帮你理清这个逻辑:

OTA升级与Slot切换流程 当前系统运行(Slot A) 下载OTA包到缓存分区 将新固件写入非活跃slot(Slot B) setActiveBootSlot(B) + 重启 bootloader从Slot B启动 升级失败/校验错误 回滚 回滚到Slot A 正常流程(蓝色) vs 回滚流程(红色)

你看,整个流程其实不复杂。但有一个关键点:回滚保护。如果升级后系统连续启动失败,bootloader会自动切回旧slot。这个机制是怎么实现的?

18.3 回滚保护:为什么能“救砖”?

回滚保护的核心是启动计数。每次从新slot启动,bootloader会递增一个计数器。如果系统正常启动并调用了markBootSuccessful(),计数器清零。如果启动失败,计数器一直累加,直到超过阈值(通常是7次),bootloader就认为这个slot“坏了”,自动切回旧slot。

我曾经踩过一个坑:某款设备升级后,每次启动都卡在开机动画,但系统并没有真正崩溃。结果计数器一直没清零,7次后自动回滚了。用户反馈说“手机自己好了”,其实就是回滚保护在起作用。

回滚保护的关键参数:

  • 最大启动尝试次数:通常为7次,由bootloader定义
  • 成功启动标记:由init进程在系统完全启动后调用
  • misc分区:存储slot状态和计数信息

来看一下misc分区里存储的数据结构:

// bootloader_message.h
struct bootloader_message {
    char command[32];          // 启动命令
    char status[32];           // 状态信息
    char recovery[768];        // recovery模式参数
    
    // A/B slot 信息
    struct slot_metadata {
        uint8_t priority;      // 优先级(0-15)
        uint8_t tries_remaining; // 剩余尝试次数
        uint8_t successful_boot; // 是否成功启动
        uint8_t verity_corrupted; // 是否校验损坏
    } slot_suffix[2];          // 两个slot的元数据
};

每次启动,bootloader会读取这个结构。如果tries_remaining大于0,就递减并尝试启动。如果启动成功,successful_boot被置为1。如果所有尝试都用完了,就切到另一个slot。

18.4 实战中的避坑指南

我在多个项目中调试过OTA回滚问题,总结了几条经验:

避坑1:markBootSuccessful() 的调用时机

我曾经遇到一个案例:系统在启动早期就调用了markBootSuccessful(),结果后续初始化失败,但slot已经被标记为“成功”。下次升级时,旧slot被覆盖,再也回不去了。

建议:在系统完全就绪后再调用,比如所有核心服务启动完毕、用户界面显示之后。

避坑2:misc分区的写入可靠性

misc分区通常很小(几MB),但写入频率不高。如果写入时掉电,可能导致数据损坏。我建议在写入时做CRC校验,或者使用双备份机制。

注意:有些厂商把misc分区放在eMMC的特定区域,写入前要确保分区没有被挂载。

还有一个容易被忽略的点:slot切换后的内核兼容性。如果新slot的内核模块和旧slot的硬件驱动不兼容,系统可能根本起不来。这时候回滚保护也救不了你——因为bootloader根本看不到内核启动。所以,升级前一定要做兼容性测试。

18.5 总结一下

OTA升级与回滚,核心就是三件事:

  1. boot_control HAL:管理slot的激活、标记、查询
  2. slot切换:写入非活跃slot,设置活跃位,重启
  3. 回滚保护:通过启动计数和misc分区,自动切回旧slot

这套机制在Android 10之后又加入了虚拟A/B分区(Virtual A/B),进一步减少了存储空间占用。但核心逻辑没变——还是那套“双保险”思路。

嗯,最后说一句:如果你在调试OTA回滚问题,先检查misc分区的内容。用hexdump看一眼,很多问题就一目了然了。


公众号:蓝海资料掘金营,微信deep3321