# 引言 OTA 升级在物联网设备中至关重要,但固件传输过程中的数据损坏或中断可能导致系统变砖。自定义 bootloader 通过 CRC 校验检测固件完整性,失败时回滚到上一个可用版本。然而,一个隐蔽的陷阱是:当回滚过程中发生硬件复位(例如看门狗超时、外部复位引脚干扰),系统可能无法正确恢复,反而触发无限重启。本文以 Arduino(基于 STM32)为例,揭示这一陷阱的根源,并给出工程级解决方案。 ## 1. 原理剖析:bootloader、OTA 与回滚机制 ### 1.1 典型 OTA 流程 - **Bootloader 阶段**:上电后执行,检查升级标志(如特定 Flash 地址的标记),若存在待升级固件,则进行 CRC 校验。 - **应用阶段**:校验通过后跳转至应用区执行;失败则回滚至备份区。 - **回滚机制**:通常将当前固件备份在另一 Flash 分区,失败时从备份区恢复。 ### 1.2 硬件复位陷阱的根源 - **看门狗(IWDG/WWDG)**:在 OTA 写入过程中,若耗时过长未喂狗,看门狗触发复位,此时 Flash 写入可能未完成,导致 CRC 校验再次失败。 - **外部复位**:用户按键或电源抖动可能产生复位信号,打断回滚过程。 - **关键问题**:复位后 bootloader 重新执行,但升级标志和回滚状态未持久化,导致系统反复尝试升级同一损坏固件。 ## 2. 硬件与软件环境 - 硬件:STM32F103C8T6(Arduino 兼容板),外部看门狗(如 TPL5010)或内部 IWDG。 - 软件:Arduino IDE 1.8.19,STM32CubeProgrammer 烧录 bootloader,自定义 OTA 协议(基于串口或 LoRa)。 - Flash 分区:Bootloader(0x08000000-0x08003FFF)、App1(0x08004000-0x08007FFF)、App2(备份,0x08008000-0x0800BFFF)、状态区(0x0800C000)。 ## 3. 配置步骤与代码实现 ### 3.1 状态区设计 使用 Flash 最后一个页(1KB)存储状态结构体,包含升级标志、CRC 结果、回滚计数等。 ```c // status.h typedef struct { uint32_t magic; // 0xA5A5A5A5 表示有效 uint8_t upgrade_pending; // 1=有待升级固件 uint8_t crc_ok; // 上次校验结果 uint8_t rollback_count; // 连续回滚次数 uint32_t app1_crc; // App1 的 CRC 值 uint32_t app2_crc; // App2 的 CRC 值 } Status; ``` ### 3.2 Bootloader 核心逻辑 ```c // bootloader.c #include #include // 内部看门狗库 #define STATUS_ADDR 0x0800C000 void load_status(Status* s) { memcpy(s, (void*)STATUS_ADDR, sizeof(Status)); } void save_status(Status* s) { // 先擦除页,再写入 flash_unlock(); flash_erase_page(STATUS_ADDR); flash_write(STATUS_ADDR, (uint8_t*)s, sizeof(Status)); flash_lock(); } void boot_app(uint32_t app_addr) { // 跳转前关闭中断,设置栈指针 __disable_irq(); void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr + 4)); __set_MSP(*(volatile uint32_t*)app_addr); app_reset(); } void setup() { IWatchdog.begin(2000000); // 2秒超时 Status st; load_status(&st); if (st.magic != 0xA5A5A5A5) { // 首次运行,初始化状态 st.magic = 0xA5A5A5A5; st.upgrade_pending = 0; st.crc_ok = 0; st.rollback_count = 0; save_status(&st); } if (st.upgrade_pending) { // 计算待升级固件 CRC(假设存储在 App1 区) uint32_t crc = compute_crc(APP1_ADDR, APP1_SIZE); if (crc == st.app1_crc) { st.crc_ok = 1; st.upgrade_pending = 0; st.rollback_count = 0; save_status(&st); boot_app(APP1_ADDR); } else { // CRC 失败,回滚到 App2 st.rollback_count++; if (st.rollback_count > 3) { // 连续失败,进入安全模式(例如 LED 闪烁) while(1) { digitalToggle(LED_BUILTIN); delay(100); } } st.upgrade_pending = 0; save_status(&st); boot_app(APP2_ADDR); } } else { // 正常启动,选择 CRC 正确的应用 if (st.crc_ok) boot_app(APP1_ADDR); else boot_app(APP2_ADDR); } } void loop() {} ``` ### 3.3 应用区 OTA 写入与复位处理 ```c // app.ino #include void perform_ota() { // 接收固件数据,写入 App1 区 // 写入完成后,更新状态区 Status st; load_status(&st); st.upgrade_pending = 1; st.app1_crc = compute_crc(APP1_ADDR, APP1_SIZE); save_status(&st); // 关键:在复位前喂狗,并延迟确保状态写入完成 IWatchdog.reload(); delay(100); // 等待 Flash 写入稳定 NVIC_SystemReset(); // 触发复位 } ``` ### 3.4 看门狗与复位陷阱的规避 - **在 bootloader 中定期喂狗**:在 CRC 计算和 Flash 操作期间,每循环一次喂狗,避免超时。 - **状态持久化**:在每次状态变更后立即写入 Flash,并等待写入完成。 - **复位原因检测**:通过 RCC->CSR 寄存器判断复位源,若为看门狗复位,则清除升级标志,强制回滚。 ```c // 复位源检测示例 if (RCC->CSR & RCC_CSR_IWDGRSTF) { // 看门狗复位,清除标志并回滚 Status st; load_status(&st); st.upgrade_pending = 0; st.crc_ok = 0; save_status(&st); RCC->CSR |= RCC_CSR_RMVF; // 清除复位标志 } ``` ## 4. 注意事项与调试技巧 - **Flash 写入时序**:STM32 的 Flash 写入需要时间,务必在写入后读取验证,并确保在复位前完成。 - **看门狗超时设置**:应大于 OTA 写入最坏情况时间,建议设置 5-10 秒,并在写入循环中喂狗。 - **回滚计数**:限制连续回滚次数,避免无限循环。可结合 LED 或串口输出错误码。 - **测试方法**:使用串口助手模拟损坏固件(修改 CRC),观察回滚行为;用示波器监测复位引脚,模拟外部干扰。 - **进阶优化**:使用双 bank 启动(如 STM32F7),硬件自动回滚,减少软件复杂度。 ## 5. 总结 硬件复位陷阱是 OTA 回滚机制中的隐形杀手,但通过状态持久化、看门狗协同和复位源检测,可以彻底规避。本文提供的代码框架可直接应用于 Arduino/STM32 项目,确保系统在恶劣环境下仍能可靠恢复。记住:稳健的 OTA 不只是校验和跳转,更是对异常情况的全面防御。