升级流程拆解:七个关键环节

固件升级看着简单,不就是把新文件写进去吗?

我早年也这么想,直到有一次升级把设备搞成砖头,才老老实实把每个环节拆开研究。说白了,升级流程就是一条流水线,任何一个工位出问题,整条线都得停。

我个人习惯把升级拆成七个步骤:下载、校验、比对、备份、写入、切换、回滚。下面我一个一个讲。

核心观点:升级流程的每个环节都要有独立的稳定性测试点。不要等整条流程跑完再查问题,那时候定位成本太高了。

固件升级七步流程 下载 完整性校验 版本比对 备份分区 写入新固件 切换启动 回滚机制 回滚机制贯穿整个流程:下载失败回滚、校验失败回滚、写入失败回滚、启动失败回滚

2.1 固件包下载

下载是升级的第一道门。我见过太多团队在这上面翻车——网络断了、服务器挂了、下载到一半设备重启了,各种情况。

测试时要关注几个点:

  • 断点续传:下载中断后能不能接着传?我遇到过一款设备,断点续传没做好,每次重传都从头开始,2MB的固件在弱网环境下载了半小时。
  • 超时处理:下载超时后是重试还是放弃?重试几次?间隔多久?
  • 并发下载:多个设备同时下载时,服务器能不能扛住?

我的经验:下载阶段一定要加一个「下载进度上报」的日志点。这样测试时能清楚看到每个包的到达时间,定位网络问题会快很多。

2.2 完整性校验

固件包下载完了,怎么知道它没被篡改或者损坏?

常用的校验方式有三种:

校验方式 原理 安全性 性能开销
CRC32 循环冗余校验 低(易碰撞) 极低
MD5 消息摘要算法 中(已知碰撞) 低
SHA256 安全哈希算法 高(无已知碰撞) 中

我个人建议:消费级产品用MD5就够了,但工业设备和车规级产品必须上SHA256。为什么?

有一次我在做车载ECU升级测试,发现攻击者可以通过篡改固件包注入恶意代码。MD5的碰撞攻击已经公开了,SHA256目前还扛得住。

注意:校验不通过时,不要直接丢弃固件包就完事。一定要记录校验失败的次数和原因,连续失败超过阈值要触发告警。我曾经排查过一个案例,设备反复校验失败,最后发现是Flash存储器的某个坏块导致的。

2.3 版本比对

校验通过后,设备要检查新固件的版本号。

这里有个坑:版本号格式不统一。有的用"V1.2.3",有的用"20240301",还有的用"build_1234"。你想想看,如果比对逻辑写死了字符串比较,遇到格式不一致就麻烦了。

我建议的做法:

  • 版本号统一用三段式:主版本.次版本.修订号
  • 比对时先比较主版本,再比较次版本,最后比较修订号
  • 禁止降级升级(除非有特殊开关)
// 版本比对示例
typedef struct {
    uint8_t major;
    uint8_t minor;
    uint16_t revision;
} firmware_version_t;

bool is_upgrade_allowed(firmware_version_t current, firmware_version_t target) {
    if (target.major < current.major) return false;  // 禁止降级
    if (target.major == current.major && target.minor < current.minor) return false;
    if (target.major == current.major && target.minor == current.minor 
        && target.revision <= current.revision) return false;
    return true;
}

2.4 备份分区

这是很多人容易忽略的一步。写入新固件之前,先把当前固件备份到另一个分区。

为什么要备份?说白了,万一新固件写坏了,你还能切回老固件。没有备份,设备就是一块砖头。

备份分区测试要点:

  • 备份空间是否足够?我遇到过备份分区只有256KB,但固件有300KB的情况
  • 备份过程中断电怎么办?要保证备份操作是原子性的
  • 备份完成后要做校验,确保备份数据完整

关键设计:备份分区和运行分区要物理隔离。有些低成本方案把备份和运行放在同一个Flash芯片的不同区域,一旦芯片坏了,两个分区都完蛋。我建议至少用两个独立的存储芯片。

2.5 写入新固件

写入阶段是风险最高的环节。为什么?因为写入过程中一旦出问题,设备可能直接变砖。

写入测试要覆盖的场景:

  1. 写入中断:写入到一半断电、复位、通信断开
  2. 写入速度:Flash写入速度是否匹配?写入太快可能触发写保护
  3. 坏块处理:遇到坏块怎么办?要跳过还是重试?
  4. 写入校验:写入完成后要读回来校验,确保数据正确

我记得有一次测试,设备在写入到90%时断电,重启后系统直接挂了。查了半天发现是写入过程中没有记录进度,断电后不知道写到哪了,只能全盘擦除重来。从那以后,我要求所有写入操作都必须有进度记录。

2.6 切换启动

新固件写完了,怎么让设备跑起来?

切换启动有两种常见方式:

方式 原理 优点 缺点
标志位切换 在Flash中写一个标志位,Bootloader根据标志位选择启动分区 实现简单 标志位可能被误写
向量表切换 修改中断向量表地址,指向新固件 切换速度快 需要芯片支持

我个人更推荐标志位切换,因为它更可控。但要注意:标志位要写在独立的存储区域,并且要有冗余备份。我曾经见过标志位被意外擦除,导致设备在两个分区之间反复重启。

警告:切换启动后,一定要给新固件一个「试用期」。比如启动后先运行5分钟,如果5分钟内没有异常,才把标志位永久锁定。这样即使新固件有问题,设备还能自动回滚。

2.7 回滚机制

回滚是整个升级流程的最后一道防线。说白了,就是「出事了怎么办」。

回滚触发条件:

  • 新固件启动失败(看门狗超时)
  • 新固件运行异常(系统崩溃、死锁)
  • 用户手动触发回滚
  • 升级过程中任何一步失败

回滚流程:

  1. 检测到异常
  2. 清除新固件的启动标志
  3. 将备份分区的固件复制回运行分区
  4. 重启设备
  5. 验证老固件能否正常启动

避坑指南:我曾经遇到一个案例,回滚时备份分区也被损坏了。原因是备份分区和运行分区共用同一个电源域,写入新固件时电压波动影响了备份分区。从那以后,我要求备份分区必须有独立的ECC校验,并且定期做完整性检查。