自动回滚,简单说就是系统在检测到新版本出问题后,自动退回到之前那个能正常工作的旧版本,不需要人工干预。
拆解问题分成三个步骤,判断出现问题、找到旧版本、执行回滚。
什么情况说明新固件有问题呢1、系统卡死了进hard fault了了,2、系统还在跑但是死锁了。人知道固件有问题的表现了,程序如何知道呢?这里启用了IWDG独立看门狗,独立看门狗的资料可以参考stm32f4的数据手册。独立看门狗的时钟由独立的 RC振荡器 LSI提供,即使主时钟发生故障它仍然有效,非常独立。
它的特征是:1、独立看门狗是一个递减计数器产生的复位。2、时钟信号是有独立RC振荡器产生。3、可以在待机和停止模式下运行。4、当看门狗被激活后,当递减为0是产生复位。因此独立看门狗常用于监控单片机系统的工作状态,当设备工作异常时,看门狗会产生复位信号,将单片机系统复位。
判断出现问题:
对应固件有问题的两个表现,可以想到,1、我们需要检查系统是否正常运行,2、我们需要检查关键任务是否正常执行,二者是并列关系,其中一个为否就需要复位系统。因此我们设置一个feeddog_Task,在这个任务中进行喂狗。为了确保每个任务的正常执行,我们设置了一个变量g_hb_mask来表示各个任务的完成情况,四个任务需要通过hb_set函数将g_hb_mask中的对应位置置1表示正常运行。四个任务都完成后,g_hb_mask由0x0000->0x000F。0x000F也就是HB_ALL对应的值,通过这个判断来说明每个任务都正确执行了。
/* 各关键任务的心跳位图,由 feeddog 任务按窗口收集 */ static volatile uint32_t g_hb_mask = 0; #define HB_WIFI (1U << 0) #define HB_FLASH (1U << 1) #define HB_UPGRADE (1U << 2) #define HB_APP (1U << 3) #define HB_ALL (HB_WIFI | HB_FLASH | HB_UPGRADE | HB_APP) static void hb_set(uint32_t bit) { taskENTER_CRITICAL(); g_hb_mask |= bit; taskEXIT_CRITICAL(); }feeddog 任务每 100ms 收一次,按 1 秒窗口累积,四个位都出现才HAL_IWDG_Refresh();缺一个就不喂,在指定时间后 IWDG 复位。需要注意的是IWDG的时钟只有32KHz会有一定的漂移,喂狗时间不能卡的太死。
找到旧版本:
我们保存的元数据是唯一事实来源。
原来的ota_config_t只保存了活动槽的固件长度和crc,一旦回滚,另一槽的长度和 CRC 就丢了,Bootloader 只能降级成“只看向量表”。回滚目标如果是个半截镜像,照样会被启动。
typedef struct { uint32_t magic; /* 固定标识 0x4F544131 */ uint32_t seq; /* 写入序号,越大越新(用于判定有效副本) */ uint8_t active_slot; /* 本次启动的槽:0 = A,1 = B */ uint8_t trial_pending; /* 1 = 新固件待确认 */ uint8_t slot_state[2]; /* 各槽状态:EMPTY / VALID / INVALID */ uint8_t reserved; uint32_t version[2]; /* 各槽固件版本号 */ uint32_t firmware_size; /* 当前活动槽固件长度(0 表示未知,跳过 CRC) */ uint32_t fw_crc; /* 当前活动槽整包 CRC32 */ uint8_t rx_bitmap[OTA_BITMAP_WORD_COUNT]; /* 断点续传:已收包位图 */ uint32_t meta_crc; /* 本结构 CRC32,计算时该字段按 0xFFFFFFFF 处理 */ } ota_config_t;为了保留回滚目标的固件长度和crc值需要修改结构体
typedef struct { uint32_t magic; /* 固定标识 0x4F544132 */ uint32_t seq; /* 写入序号,越大越新(用于判定有效副本) */ uint8_t active_slot; /* 本次启动的槽:0 = A,1 = B */ uint8_t prev_slot; /* 试运行失败时回滚到哪个槽 */ uint8_t trial_pending; /* 1 = 新固件待确认 */ uint8_t trial_count; /* 试运行失败启动次数,达到上限则回滚 */ uint8_t slot_state[2]; /* 各槽状态:EMPTY / VALID / INVALID */ uint32_t slot_size[2]; /* 各槽固件长度(0 表示未知,跳过整包 CRC) */ uint32_t slot_crc[2]; /* 各槽整包 CRC32 */ uint32_t version[2]; /* 各槽固件版本号 */ uint8_t rx_bitmap[OTA_BITMAP_BYTES]; /* 断点续传:已收包位图 */ uint32_t meta_crc; /* 本结构 CRC32,计算时该字段按 0xFFFFFFFF 处理 */ } ota_config_t;trial_count / prev_slot / slot_size[2] / slot_crc[2]修改了一共四处有关回滚的内容,slot_size[2] / slot_crc[2]是加入 了回滚槽的检验内容,trial_count / prev_slot则是方便回滚动作。
在完成固件的整包crc校验后,执行如下代码,指定当前槽为回退槽,指定新固件待确认,trial_count试运行次数当前为0。接着执行NVIC_SystemReset()开始重启。
meta.prev_slot = current_slot; /* 万一新固件不行,退回这里 */ meta.active_slot = target_slot; /* 下次启动跳新槽 */ meta.trial_pending = 1; meta.trial_count = 0; meta.slot_size[target] = g_firmware_size; meta.slot_crc[target] = g_firmware_crc;重启后回到bootloader程序,他需要做的事如下,
1. active 槽的向量表 + 整包 CRC 合法吗? 不合法 → 直接回退到另一个 VALID 槽,不消耗 trial 次数 2. trial_pending == 1? 读 RCC->CSR 看是不是 IWDG 复位,然后清标志 是 IWDG → trial_count++ count >= 3 → 回滚:active=prev、失败槽标 INVALID、清 trial、写回 否则 → 写回计数,跳 active 3. trial_pending == 0 → 直接跳 active试运行的计数工作需要放在bootloader不放在 APP 。因为坏固件可能根本跑不到 APP 的业务逻辑,而 Bootloader 是每次启动的必经之路。另一个关键点是只对 IWDG 复位计数。如果每次启动都计数,那么用户在 5 秒确认窗口内拔三次电,或者碰到电源不稳的情况就能把好固件误判成坏固件。用RCC_CSR_IWDGRSTF 过滤后,上电复位、软件复位都不算,只有“狗咬”才算。
跳转运行到新固件后,由ota_app_boot_init 来做三件事:建元数据(如果为空)、修正 active_slot、记录版本。trial_pending不在这里清除,放在IWDG中,需要系统连续 5 秒健康,才调 ota_app_confirm_healthy 清掉trial_pending/trial_count。
正常:active=B, trial=0 ──────────────────────> 跳 B 升级:active=B, prev=A, trial=1, count=0 ──复位──> 跳 B B 健康 5s ──> trial=0 ────────────────────────> 长期运行 B 挂死 ──IWDG──> count=1 ──> 跳 B ──IWDG──> count=2 ──> 跳 B ──IWDG──> count=3 ──> 回滚: active=A, B=INVALID ──> 跳 A这套程序的能力边界:
- 确认之后再崩, 不会回滚 。`trial` 只覆盖“首次启动是否健康”,不负责长期稳定性——这是有意为之,否则一次偶发 HardFault 就把正常固件退掉。
- 回滚不删除失败镜像,只标`INVALID` ,方便事后读出来分析。
- 没做防降级,`version` 目前只记录不比较。
- 回滚目标自身也坏了,Bootloader 停在`can't jump` ,不会乱跳。