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的最后一个块地址。重启后,跳过已写入的部分,从断点处继续。

具体实现上,我一般这样做:

  1. 把固件包分成固定大小的块(比如256KB一块)
  2. 每写完一块,就把块号记录到状态扇区
  3. 重启后读取最后成功写入的块号,从下一块开始下载

这里有个坑——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);
}
警告:断点续传不能100%保证成功。如果Flash本身出现了坏块,或者分区表被破坏了,续传可能会失败。所以一定要配合后面的分区表备份和环境变量保护一起用。

4.3 Flash分区表备份与恢复——系统的骨架不能乱

分区表是什么?说白了就是Flash的“地图”。它告诉U-Boot:哪个区域放U-Boot,哪个区域放内核,哪个区域放文件系统。

如果升级过程中分区表被写坏了,那整个系统就废了。U-Boot连自己该从哪启动都不知道。

所以,分区表必须备份。我个人的做法是:

  • 在Flash的末尾保留一个备份分区表(通常和主分区表一模一样)
  • 每次修改分区表前,先把旧的分区表备份到备份区
  • 修改完成后,再同步更新备份区

这里有个细节:备份分区表的位置要固定。不能跟着主分区表一起移动。我习惯把它放在Flash的最后一个扇区,这样即使前面的分区全乱了,U-Boot也能找到备份。

恢复流程是这样的:

  1. U-Boot启动时,先读主分区表
  2. 如果主分区表CRC校验失败,自动切换到备份分区表
  3. 用备份分区表启动系统,同时标记“分区表异常”
  4. 系统启动后,用备份分区表恢复主分区表
关键点:备份分区表本身也要做CRC保护。我曾经见过一个案例,备份分区表因为Flash老化出现了位翻转,结果恢复时把系统搞得更乱了。所以,读备份分区表时一定要校验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;
}
避坑指南:我曾经遇到过一个问题——升级完成后环境变量莫名其妙被重置了。查了半天才发现,是升级脚本里有个saveenv命令,把默认环境变量覆盖了正确的值。从那以后,我规定:升级过程中绝对不允许执行saveenv。如果非要改环境变量,升级完成后单独做。

4.5 整体恢复流程——一张图说清楚

好了,上面讲了四个方面的策略。它们不是孤立的,得配合起来用。我画了一张流程图,帮你理清整体思路:

中断后状态恢复整体流程 系统重启 有中断标记? 否 正常启动系统 是 读取状态扇区 分区表正常? 否 恢复分区表 环境变量正常? 否 恢复环境变量 执行断点续传

这张图的核心逻辑是:先检查、再恢复、最后续传。别一上来就急着续传,先把分区表和环境变量这些“基础设施”修好,不然传了也是白传。

好了,中断恢复这块就讲到这里。记住一句话:好的恢复策略,比好的防护策略更重要。因为中断总会发生,关键是中断之后系统还能不能站起来。

总结一下四个要点:
  • 进度保存:用状态扇区记录升级步骤,配合CRC防写坏
  • 断点续传:记录最后成功写入的块号,续传前检查扇区状态
  • 分区表备份:在Flash末尾保留备份,主表坏了自动切换
  • 环境变量保护:双备份+升级期间锁定+关键参数硬编码
可可精品资料库