3. 升级流程设计:升级包下载与校验、升级包写入非活跃分区、升级完成后的slot切换
好,咱们接着聊升级流程。这一节是A_B无缝升级的核心实操环节。说白了,就是三个步骤:把升级包拿下来、校验它是不是好的、写到另一个分区里,最后切过去。听起来简单?嗯,每个环节都有坑。
3.1 升级包下载与校验:别让坏包毁了设备
我见过不少团队,下载流程做得稀里糊涂。升级包下载,不只是「从服务器拉个文件」那么简单。
下载策略,我个人习惯用断点续传。为什么?因为设备网络不稳定,下载到一半断了,从头再来?用户会骂娘的。我建议这样设计:
- 先请求服务器获取升级包元信息(大小、版本、校验值)
- 分块下载,每块大小建议256KB~1MB
- 每下载一块,本地记录偏移量
- 断网后重连,从上次断点继续
校验这块,我踩过坑。曾经有个项目,只校验了升级包的MD5。结果呢?包是完整的,但解压后文件损坏了。后来我改成双层校验:
推荐校验流程:
- 下载完成后,校验整个升级包的SHA256
- 解压后,对每个文件单独校验CRC32
- 关键文件(如bootloader)额外做签名验证
你想想看,如果只校验外层包,内层文件被篡改了怎么办?双层校验就是给安全上了双保险。
// 伪代码示例:升级包校验
bool verify_update_package(const char* package_path) {
// 第一层:整体校验
uint8_t expected_hash[32];
get_package_sha256(package_path, expected_hash);
uint8_t actual_hash[32];
sha256_file(package_path, actual_hash);
if (memcmp(expected_hash, actual_hash, 32) != 0) {
log_error("Package SHA256 mismatch");
return false;
}
// 第二层:解压后逐文件校验
extract_package(package_path, "/tmp/update/");
FileList* files = get_file_list("/tmp/update/");
for (int i = 0; i < files->count; i++) {
if (!verify_file_crc32(files->entries[i])) {
log_error("File CRC32 mismatch: %s", files->entries[i]->name);
return false;
}
}
return true;
}
注意:校验失败后,不要直接删除升级包。保留现场,方便后续排查。我曾经因为直接删包,导致问题复现不了,查了三天才找到原因。
3.2 升级包写入非活跃分区:写对地方是关键
A_B升级的核心思想,就是「你升级你的,我跑我的」。写入非活跃分区时,有几个要点必须注意。
分区定位,说白了就是搞清楚当前在哪个槽位。我建议用boot_control HAL来获取:
// 获取当前活跃槽位
Slot current_slot = get_current_slot(); // slot_a 或 slot_b
Slot target_slot = (current_slot == SLOT_A) ? SLOT_B : SLOT_A;
// 挂载目标分区
if (mount(target_slot, "/mnt/update") != 0) {
log_error("Failed to mount target slot");
return;
}
写入策略,我推荐流式写入。什么意思?就是边下载边写,不要等全部下载完再写。好处很明显:
- 节省临时存储空间(设备存储通常吃紧)
- 缩短整体升级时间
- 如果写入失败,能尽早发现
但流式写入有个坑:写入顺序。我记得有个项目,分区是顺序写入的,但升级包里的文件顺序和分区布局不匹配。结果呢?写到最后发现空间不够。后来我改成先解析升级包的文件布局,再按分区偏移量写入。
小技巧:写入过程中,每写一个块就做一次sync()。别等到最后才sync,万一掉电,前面写的全白费。我习惯每写10MB强制sync一次。
还有一个容易被忽略的点:坏块处理。尤其是NAND Flash,写入时遇到坏块是常事。我建议:
- 写入前先擦除目标分区
- 写入时检查每个块的写入状态
- 遇到坏块,跳过并记录日志
- 写入完成后,回读校验关键区域
3.3 升级完成后的slot切换:最后一哆嗦
升级包写完了,校验通过了。接下来就是切换slot。这一步看似简单,但做不好,设备可能变砖。
切换时机,我建议在下次重启时生效。为什么?因为有些驱动在运行时不能热切换。你想想看,正在用的文件系统突然被切走了,系统不崩溃才怪。
切换的核心操作,就是修改bootloader的配置:
// 设置目标槽位为下次启动槽位
int set_active_slot(Slot slot) {
// 写入bootloader控制块
BootloaderControl bc;
if (read_bootloader_control(&bc) != 0) {
log_error("Failed to read bootloader control");
return -1;
}
bc.active_slot = slot;
bc.retry_count = 0; // 重置重试计数
bc.successful_boot = 0; // 标记尚未验证启动成功
if (write_bootloader_control(&bc) != 0) {
log_error("Failed to write bootloader control");
return -1;
}
return 0;
}
这里有个关键设计:回滚机制。我曾经吃过亏,切换后新系统起不来,又没有回滚方案,只能返厂。后来我设计了「三次启动验证」策略:
三次启动验证流程:
| 启动次数 | 状态 | 操作 |
|---|---|---|
| 第1次 | 新系统启动 | 标记为「试运行」,启动成功则继续 |
| 第2次 | 新系统运行 | 标记为「验证中」,启动成功则继续 |
| 第3次 | 新系统稳定 | 标记为「已验证」,正式切换 |
如果三次中有任何一次启动失败,bootloader自动回滚到旧槽位。用户甚至感觉不到升级失败过。
警告:切换后,旧系统的数据不要立即删除。保留至少一次完整升级周期的数据。我见过有人切换后马上删旧系统,结果新系统有bug,想回退都回不了。
最后说一句,切换完成后,记得通知用户。不是发个通知就完事,而是告诉用户「升级成功,下次重启生效」或者「已重启,新系统运行正常」。用户体验,往往就体现在这些细节里。
嗯,升级流程设计就聊到这儿。下一节咱们讲讲升级过程中的异常处理,那才是真正考验系统健壮性的地方。