升级流程拆解:七个关键环节
固件升级看着简单,不就是把新文件写进去吗?
我早年也这么想,直到有一次升级把设备搞成砖头,才老老实实把每个环节拆开研究。说白了,升级流程就是一条流水线,任何一个工位出问题,整条线都得停。
我个人习惯把升级拆成七个步骤:下载、校验、比对、备份、写入、切换、回滚。下面我一个一个讲。
核心观点:升级流程的每个环节都要有独立的稳定性测试点。不要等整条流程跑完再查问题,那时候定位成本太高了。
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 写入新固件
写入阶段是风险最高的环节。为什么?因为写入过程中一旦出问题,设备可能直接变砖。
写入测试要覆盖的场景:
- 写入中断:写入到一半断电、复位、通信断开
- 写入速度:Flash写入速度是否匹配?写入太快可能触发写保护
- 坏块处理:遇到坏块怎么办?要跳过还是重试?
- 写入校验:写入完成后要读回来校验,确保数据正确
我记得有一次测试,设备在写入到90%时断电,重启后系统直接挂了。查了半天发现是写入过程中没有记录进度,断电后不知道写到哪了,只能全盘擦除重来。从那以后,我要求所有写入操作都必须有进度记录。
2.6 切换启动
新固件写完了,怎么让设备跑起来?
切换启动有两种常见方式:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 标志位切换 | 在Flash中写一个标志位,Bootloader根据标志位选择启动分区 | 实现简单 | 标志位可能被误写 |
| 向量表切换 | 修改中断向量表地址,指向新固件 | 切换速度快 | 需要芯片支持 |
我个人更推荐标志位切换,因为它更可控。但要注意:标志位要写在独立的存储区域,并且要有冗余备份。我曾经见过标志位被意外擦除,导致设备在两个分区之间反复重启。
警告:切换启动后,一定要给新固件一个「试用期」。比如启动后先运行5分钟,如果5分钟内没有异常,才把标志位永久锁定。这样即使新固件有问题,设备还能自动回滚。
2.7 回滚机制
回滚是整个升级流程的最后一道防线。说白了,就是「出事了怎么办」。
回滚触发条件:
- 新固件启动失败(看门狗超时)
- 新固件运行异常(系统崩溃、死锁)
- 用户手动触发回滚
- 升级过程中任何一步失败
回滚流程:
- 检测到异常
- 清除新固件的启动标志
- 将备份分区的固件复制回运行分区
- 重启设备
- 验证老固件能否正常启动
避坑指南:我曾经遇到一个案例,回滚时备份分区也被损坏了。原因是备份分区和运行分区共用同一个电源域,写入新固件时电压波动影响了备份分区。从那以后,我要求备份分区必须有独立的ECC校验,并且定期做完整性检查。