4、中断后的状态恢复策略:升级进度保存、断点续传原理、Flash分区表备份与恢复、环境变量保护
好,咱们接着聊中断恢复的事。上一节讲了怎么防中断,但说实话,再好的防护也挡不住所有意外。断电、硬件复位、看门狗超时……这些情况总归会遇到。那问题来了:中断之后,怎么把系统拉回来?
我个人习惯把这个问题拆成四个层面:进度保存、断点续传、分区表保护、环境变量兜底。咱们一个一个说。
4.1 升级进度保存——别让用户从头再来
你想想看,一个固件包50MB,用串口传得传半小时。结果传到80%的时候啪一下断电了,重启后又要从头传。用户不骂娘才怪。
所以,进度保存是中断恢复的第一道防线。说白了,就是在升级过程中,定期把“当前做到哪一步了”记下来。
我在项目中常用的做法是:
- 在Flash里划一个专门的状态扇区(比如最后一个扇区,4KB就够)
- 每次完成一个关键步骤,就往这个扇区写一个状态标记
- 重启后U-Boot先读这个标记,判断是否需要恢复
状态标记我一般用枚举值,比如:
enum upgrade_status {
UPGRADE_IDLE = 0x00, // 空闲,无升级任务
UPGRADE_STARTED = 0x01, // 已开始下载
UPGRADE_DOWNLOADING = 0x02, // 下载中
UPGRADE_FLASHING = 0x03, // 正在写入Flash
UPGRADE_COMPLETE = 0xFF // 升级完成
};
嗯,这里要注意:写状态扇区时一定要做CRC校验。我曾经遇到过Flash写入到一半断电,状态标记写成了0x03和0x04的混合体,重启后U-Boot直接懵了。后来我改成先写备份标记,再写主标记,配合CRC,才彻底解决。
4.2 断点续传原理——从哪断的从哪接
进度保存只是告诉你“做到哪了”,但具体怎么接着干?这就需要断点续传机制了。
断点续传的核心思想很简单:记录已经成功写入Flash的最后一个块地址。重启后,跳过已写入的部分,从断点处继续。
具体实现上,我一般这样做:
- 把固件包分成固定大小的块(比如256KB一块)
- 每写完一块,就把块号记录到状态扇区
- 重启后读取最后成功写入的块号,从下一块开始下载
这里有个坑——Flash的擦除特性。你想想看,如果上一轮已经擦除了某个扇区但没写完,下一轮直接写,会不会出问题?
会的。我遇到过这种情况:上一轮擦除了扇区A,写了一半断电。下一轮从断点续传,直接往扇区A写数据,结果因为没重新擦除,数据全乱了。
解决方案是:每次续传前,先检查目标扇区的擦除状态。如果发现扇区是脏的(已擦除但未写满),先重新擦除再写。
/* 断点续传核心逻辑 */
int resume_upgrade(struct upgrade_info *info) {
/* 读取上次保存的进度 */
int last_block = read_status_sector();
if (last_block == INVALID) {
/* 没有断点,从头开始 */
return start_new_upgrade(info);
}
/* 跳过已完成的块 */
info->current_block = last_block + 1;
/* 检查当前块所在扇区是否需要重新擦除 */
if (is_sector_dirty(info->current_block)) {
erase_sector(get_sector_addr(info->current_block));
}
return continue_download(info);
}
4.3 Flash分区表备份与恢复——系统的骨架不能乱
分区表是什么?说白了就是Flash的“地图”。它告诉U-Boot:哪个区域放U-Boot,哪个区域放内核,哪个区域放文件系统。
如果升级过程中分区表被写坏了,那整个系统就废了。U-Boot连自己该从哪启动都不知道。
所以,分区表必须备份。我个人的做法是:
- 在Flash的末尾保留一个备份分区表(通常和主分区表一模一样)
- 每次修改分区表前,先把旧的分区表备份到备份区
- 修改完成后,再同步更新备份区
这里有个细节:备份分区表的位置要固定。不能跟着主分区表一起移动。我习惯把它放在Flash的最后一个扇区,这样即使前面的分区全乱了,U-Boot也能找到备份。
恢复流程是这样的:
- U-Boot启动时,先读主分区表
- 如果主分区表CRC校验失败,自动切换到备份分区表
- 用备份分区表启动系统,同时标记“分区表异常”
- 系统启动后,用备份分区表恢复主分区表
4.4 环境变量保护——U-Boot的“记忆”不能丢
环境变量是U-Boot的“记忆”。它记录了bootargs、bootcmd、IP地址、MAC地址这些关键参数。如果环境变量丢了,U-Boot可能连网络都起不来。
升级过程中,环境变量最容易在两种情况下被破坏:
- 误操作:升级脚本不小心擦除了环境变量所在的扇区
- 写入中断:正在写环境变量时断电,导致数据损坏
我的防护策略是:
第一,环境变量用双备份。U-Boot默认就支持这个——主环境变量和冗余环境变量各占一个扇区。写的时候先写冗余区,再写主区。读的时候两个都读,取CRC正确的那一个。
第二,升级过程中锁定环境变量。在升级开始前,把环境变量区域设置为只读。升级完成后才解锁。这样可以防止升级脚本意外修改环境变量。
第三,关键参数硬编码。像MAC地址、序列号这种不能丢的参数,我建议直接写在U-Boot的源码里,或者放在一个独立的OTP区域。别只依赖环境变量。
/* 环境变量保护示例 */
int protect_env_during_upgrade(void) {
/* 锁定环境变量区域 */
if (flash_protect(ENV_SECTOR_START, ENV_SECTOR_END) != 0) {
printf("Failed to protect env sector!\n");
return -1;
}
/* 备份当前环境变量到内存 */
memcpy(env_backup, (void *)ENV_SECTOR_START, ENV_SECTOR_SIZE);
/* 开始升级... */
/* 升级完成后解锁 */
flash_unprotect(ENV_SECTOR_START, ENV_SECTOR_END);
return 0;
}
4.5 整体恢复流程——一张图说清楚
好了,上面讲了四个方面的策略。它们不是孤立的,得配合起来用。我画了一张流程图,帮你理清整体思路:
这张图的核心逻辑是:先检查、再恢复、最后续传。别一上来就急着续传,先把分区表和环境变量这些“基础设施”修好,不然传了也是白传。
好了,中断恢复这块就讲到这里。记住一句话:好的恢复策略,比好的防护策略更重要。因为中断总会发生,关键是中断之后系统还能不能站起来。
- 进度保存:用状态扇区记录升级步骤,配合CRC防写坏
- 断点续传:记录最后成功写入的块号,续传前检查扇区状态
- 分区表备份:在Flash末尾保留备份,主表坏了自动切换
- 环境变量保护:双备份+升级期间锁定+关键参数硬编码