空调OTA升级可靠性设计:AB分区、掉电保护与自动回滚工程实践
2026/9/7 16:14:05 网站建设 项目流程

空调OTA升级,听起来是一件“把新固件包发下去、设备重启一下”就能搞定的事情。但真正做过家电/物联网设备OTA的工程师都清楚,这个功能上线之前最让人睡不着的,不是功能开发,而是那个无法预知的“升级到一半断电了”的场景。空调和手机不一样:手机升级失败最多卡Logo,用户返修或线刷就行;空调升级失败,轻则机器直接变砖无法启动,重则用户报修、售后上门、批量召回。

这篇文章不是去讲“如何调用某个OTA平台接口”,而是解决空调这类对可靠性要求极高的设备,在做OTA升级时真正绕不开的三个工程问题:

  1. 为什么必须用AB分区,而不是传统的单分区“升级完成后再切换”?
  2. 升级到任意阶段断电,如何保证设备一定能重新启动?
  3. 系统启动失败时,如何做到自动回滚,而不是让机器变砖?

围绕这三个问题,我会把AB分区的原理、掉电保护的阶段设计、自动回滚的完整工程链路拆开讲清楚,并给出可以直接落地到STM32、ESP32或类似资源受限MCU上的参考实现。这套方案不只有空调适用,凡是涉及变频设备、白电、小家电、车载控制器的OTA升级,思路基本都是相通的。

1. 这篇文章真正要解决的问题

先聊一个真实场景。某款空调的室内机主控板使用一颗带1MB Flash的MCU,原厂固件只有400KB,剩余空间看起来不少。团队决定做OTA升级,早期方案很“经典”:Flash里划分一个APP区,升级时把新固件整体写入一个临时缓存区,校验通过后再覆盖APP区,最后跳转运行。

这个方案在实验室测试了两个月,一切都好。但到了小批量试产阶段,问题出现了。

有用户家里半夜升级空调固件时遇到停电,第二天空调开机无任何反应。售后拆机排查后发现:Bootloader在跳转APP时发现APP区内容是乱码。原因是升级程序在“擦除APP区”这一步骤执行到一半时断电,旧固件已经被擦掉,新固件又没有写完,设备永久变砖。

这就是单分区方案最核心的缺陷:在“擦除旧固件”到“写入新固件完成”这段窗口内,Flash里没有任何一份完整的可用固件,任何一次断电都会导致设备变砖。

对于手机、路由器这类设备,变砖还有救砖通道;但空调的MCU一般没有外置烧录接口暴露给用户,售后维修成本和用户满意度损失都非常大。

所以这篇文章要解决的核心问题,正是这个“升级可靠性”问题。不管是做空调控制器、热水器主控板还是智能网关,只要产品支持OTA,就必须面对这个窗口期。你需要在设计阶段就回答清楚:

  • 设备里同时保留几份固件?
  • 升级过程中写入的顺序是什么?
  • 如果任意时刻断电,重启后系统走哪条恢复路径?
  • 如何判定“新固件真的能正常工作”,而不是仅仅“写进去了”?

读完这篇文章,你应该能自己设计出一套“在任何时刻掉电都不会变砖、启动失败能自动回滚”的OTA工程方案。注意,这里说的是工程方案,不是demo。它包含分区设计、Bootloader策略、启动计数机制、掉电保护时序和一套完整的验证方法。

2. AB分区的核心概念与工程价值

2.1 什么是AB分区

AB分区,也叫A/B Slot双槽分区方案,核心思想是:在Flash中划分两个完全独立的固件分区,当前运行的分区称为Slot A,备用分区称为Slot B。系统始终确认两个分区中至少有一个是完整可用的。

升级时,新固件写入“非当前运行”的那个分区。比如当前运行的是Slot A,新固件就写入Slot B;全部写入并校验通过后,再通过一个“启动标志”(boot flag)告知Bootloader下次启动使用Slot B。如果校验失败或启动异常,Bootloader回退到Slot A继续运行。

这个方案在Android系统上已经成为标准(即A/B Seamless Updates),汽车嵌入式软件也在大量采用。空调这类家电设备虽然成本敏感,但如今带Wi-Fi/蓝牙模组的空调主控MCU,Flash普遍在1MB以上,采用AB分区已经具备硬件条件。

2.2 AB分区解决了什么,代价是什么

AB分区解决了单分区方案最致命的问题:

对比维度单分区升级AB分区升级
升级前旧固件在APP区Slot A运行旧固件
写入时直接覆写APP区新固件写入Slot B
写入中断电APP区半新半旧,无法启动Slot A完整无损,正常启动
启动失败无恢复手段Bootloader自动切回Slot A
Flash成本1份APP空间2份APP空间
实现复杂度较低较高

代价很直接:Flash空间需要多一倍的APP区开销。如果MCU Flash只有512KB,而APP固件已经有350KB,那AB分区基本不可行。这也是为什么很多低成本设备仍然停留在“升级失败就返厂”的状态。

2.3 升级的整体生命周期

在AB分区架构下,一次完整升级包含以下阶段:

  1. 升级器从服务器下载新固件,先存入分区B的临时存储区域(或直接写入Slot B的备份区)。
  2. 全量写入完成后,对Slot B分区做CRC32/MD5/SHA256校验。
  3. 校验通过后,设置“下一次启动使用Slot B”的启动标志,并执行复位。
  4. Bootloader读取启动标志,跳转到Slot B运行。
  5. Slot B运行后,向上位机/云端上报“升级成功”,确认后才允许进入下一次升级。

任何一个阶段异常,都走回滚路径:回到Slot A继续运行,等待下次重试。

2.4 容易误解的地方

很多人第一次接触AB分区,会误以为“分区A坏了还有分区B,双保险所以很稳”。这个理解需要纠正。AB分区真正保证的是“升级过程中任何时候都有一个完整固件可运行”,它并不保证A和B两个分区都不损坏。如果Flash本身因为老化、坏块、非法写入等原因损坏,或者运行中的固件自身逻辑崩溃,AB分区并不能解决所有问题。因此,完整的方案还需要配套校验机制(CRC/签名)、看门狗和启动计数回滚,这几部分我们后面都会讲到。

3. 环境准备与前置条件

在动手写代码之前,先说清楚做这样一套方案需要什么样的软硬件基础。不同项目的芯片型号和编译器会有差异,本文的示例以通用嵌入式工程思维来写,具体型号相关参数请以你自己项目的数据手册为准。

3.1 硬件层面

做AB分区+掉电保护+自动回滚,至少需要满足以下条件:

  • MCU Flash容量至少能装下两份APP固件,外加Bootloader空间和分区表/标志位存储空间。
  • 如果是资源相对紧张的MCU,可以考虑把升级产物做差分压缩(增量升级),降低Slot B的空间占用,但那会引入补丁算法的复杂度,本文不展开。
  • 必须有可用的掉电检测机制。常见做法是MCU的供电引脚经过一个电压检测电路(检测AC输入侧的掉电信号),或者使用MCU内部的Brown-Out Detector(BOD,掉电复位检测)。空调行业还有一种做法:检测310V直流母线电压下降沿。它的意义在于:主控在真正掉电前的几毫秒到几十毫秒内,又能获得一次执行关键Flash写保护动作的机会。
  • 需要外接或内置看门狗(IWDG/ WWDG)。

3.2 软件/工具链层面

  • 交叉编译工具链(arm-none-eabi-gcc、IAR、Keil等,按你的MCU选择)
  • 支持Flash分区地址链接脚本(Linker Script),一般通过分散加载文件或ld链接脚本定义各分区起始地址和长度。
  • 烧录工具(用于工厂初始烧录Bootloader与出厂固件)。
  • 一个串口或日志输出通道,升级过程中打印关键节点日志,方便定位问题。

3.3 典型分区表设计

假设MCU Flash为1MB,Bootloader占用64KB,Slot A和Slot B各400KB,剩余用于参数存储和升级标志。分区表大致如下:

分区起始地址(举例)大小说明
Bootloader0x0800000064KB引导程序,不随OTA更新
Slot A0x08010000400KB当前出厂版本/上一稳定版本
Slot B0x08074000400KB升级目标分区
OTA标志区0x080E80004KB启动标志、升级状态、启动计数
参数区0x080E9000剩余设备参数、Wi-Fi配置等

注意:不同MCU的Flash扇区/页大小不一样,分区起始地址必须对齐到扇区边界,否则擦除时可能误擦其他区域。这一条在实战中非常关键,后面排错部分还会提到。

4. 空调OTA升级的关键设计:掉电保护分阶段详解

掉电保护不是“做一个函数”,而是贯穿升级过程的设计准则。它的核心问题只有一个:在Flash操作的任意时刻发生掉电,设备重启后都必须能走到一个确定的、可恢复的状态。

要达成这个目标,需要把升级流程细分为几个阶段,并明确每个阶段掉电后的后果和恢复动作。

4.1 阶段一:升级头/标志写入阶段

在正式开始升级之前,升级器先把“升级意图”写入OTA标志区的魔数(magic number)字段,例如写入0xA5A5A5A5表示“升级进行中”。

这个阶段掉电的后果:可能会出现升级标志残留。因此重启后Bootloader不能仅仅因为看到“升级中”标志就直接认为升级失败,而是应该先检查两个分区的完整性,再决定继续升级还是回滚。

工程上更稳妥的做法是:把升级状态机设计为“升级未开始 -> 固件下载中 -> 校验通过 -> 准备切换 -> 切换完成 -> 运行新版本 -> 升级确认”。状态切换必须按照严格顺序写入Flash,并且每次写入前先擦除对应的Flash页。擦除和写入期间禁用中断和看门狗喂狗操作,避免执行到一半被打断导致状态错乱。

4.2 阶段二:固件数据写入阶段

新固件按“块”写入Slot B。每一块写入前可先做一次擦除,写入后立即读回校验,确认写入正确后再写下一块。

这个阶段掉电的后果:Slot B可能处于“半成品”状态,但它不会影响Slot A。重启后Bootloader通过检查Slot B的固件头、CRC、签名等方式就能识别这是一个无效分区,自动忽略它,继续从Slot A启动。

这里有一个隐藏工程点:不要在写完所有固件之后才做整包校验,而应该每写一块就做一遍“写入后读回比对”,至少保证单块写入正确。整包校验放在最后再做一次,是为防止分包传输过程中出现丢包、篡改等问题。

4.3 阶段三:分区切换标志写入阶段

当Slot B固件全量写入并校验通过后,需要把“下次启动使用Slot B”这个标志写入OTA标志区。

这是整个升级过程中最接近“胜负手”的一步。如果标志写入不完整,Bootloader不知道应该启动新分区;如果标志误写,可能出现闪退循环。因此该标志写入建议采用“双备份 + 反码校验”方式:

  • 在Flash的两个不同页里,各写一份boot_target标志副本。
  • 每一份都同时存储原码和反码。
  • 读取时先校验反码,若两份都有效则以第一份为准;若一份损坏,使用另一份;若两份都无效,则默认启动Slot A。

这样做能最大程度规避“写到了一半掉电”导致标志位处于不确定状态的问题。

4.4 阶段四:新分区启动确认阶段

Bootloader按标志跳转到Slot B后,APP代码正式运行。但这里还不能认定“升级成功”。必须由APP运行一段时间(例如运行10秒、完成关键外设初始化、通讯模块正常注册入网等自定义业务条件)后,APP主动向OTA标志区写入“升级确认完成”标志。

如果APP在启动后的确认窗口期内崩溃、死机、反复复位,Bootloader的启动计数会不断递增,达到阈值后自动切回Slot A并清除升级标志。

这个“确认窗口期”与“自动回滚”是配套出现的,只写新固件而不做启动确认,等于没有回滚功能。

4.5 掉电检测的硬件联动

在空调这种强电设备上,掉电不是像嵌入式开发板那样“USB拔掉”这么简单。空调主控供电一般来自开关电源,当电网掉电时,开关电源的电容还能维持几十毫秒的电压输出。这个时间足够MCU感知到“母线电压跌落”,然后进入紧急处理流程。

建议的联动动作是:

  1. 掉电中断触发后,立即停止升级写入流程。
  2. 关闭不必要的外设,减小电流消耗,为Flash写入争取更多时间。
  3. 把当前“升级进度”和“已完成的块数”写入OTA标志区(前提是Flash擦写时间足够)。
  4. 若剩余时间不足以完成一次完整Flash写操作,则不做任何写动作,保持当前状态,依靠重启后Bootloader的逻辑来恢复。

更保守的做法是:只要检测到掉电,就立即放弃本轮升级,清除“升级中”状态,保证设备下一次上电走正常启动路径。这样虽然可能浪费了这一次升级机会,但最大程度保证了设备可用性。

5. 自动回滚机制:启动计数与看门狗配合

自动回滚是整个OTA方案“不让人上门维修”的兜底手段。它的设计思路是:系统启动本身是一个“先假设成功,再逐步确认”的过程,如果确认失败,就回退到上一个可用版本。

5.1 启动计数器

在OTA标志区中定义一个计数器,每次Bootloader准备启动新版本分区(Slot B)时,先递增该计数器,再执行跳转。如果APP启动成功并写入确认标志,计数器清零。

如果APP启动失败、卡死、反复复位,计数器就会不断递增。Bootloader每次启动时检查计数器是否超过阈值(例如3次)。超过阈值后,Bootloader判断Slot B“不可用”,把启动目标切回Slot A,同时清除Slot B的完整标志,要求重新下载升级包。

5.2 看门狗的作用

看门狗在这里的核心价值,是防止“APP看起来活着,但实际上卡死在某个初始化循环里”这种情况。MCU的IWDG,通常几十毫秒到几秒不喂狗就会复位。APP上电后第一时间启动看门狗,并在主循环中喂狗。如果APP在启动早期因为访问非法外设、配置错误等原因卡死,看门狗会强制复位MCU,让Bootloader有机会重新判断“该不该回滚”。

看门狗与启动计数的关系是:看门狗负责“发现问题”,启动计数负责“记录问题和触发决策”,Bootloader负责“执行回滚”。

5.3 Bootloader回滚决策伪代码

下面是Bootloader的核心判断逻辑伪代码,可以直接对应到工程实现:

// bootloader_ota_rollback.c uint32_t boot_count = read_ota_flag(OTA_FLAG_BOOT_COUNT); uint32_t boot_target = read_ota_flag(OTA_FLAG_BOOT_TARGET); if (boot_target == TARGET_SLOT_B) { boot_count++; write_ota_flag(OTA_FLAG_BOOT_COUNT, boot_count); if (boot_count >= MAX_BOOT_ATTEMPTS) { // 新版本连续启动失败,回滚到 Slot A write_ota_flag(OTA_FLAG_BOOT_TARGET, TARGET_SLOT_A); clear_ota_flag(OTA_FLAG_NEW_FW_VALID); boot_count = 0; write_ota_flag(OTA_FLAG_BOOT_COUNT, boot_count); jump_to_slot_a(); } else { // 尝试启动 Slot B if (check_fw_integrity(SLOT_B_BASE) == OK) { jump_to_slot_b(); } else { // Slot B 校验失败,立即回滚 rollback_to_slot_a(); } } } else { // 默认启动 Slot A jump_to_slot_a(); }

这里要注意,Bootloader本身也在Flash中运行,而Flash擦写操作期间CPU取指可能会有风险,所以工程上一般建议把“写入OTA标志”的关键代码放到RAM中执行,或者关闭中断并确保代码在Cache/Flash执行不冲突。这个问题在一些带Flash缓存(ART加速器)的MCU上尤其需要验证。

5.4 APP确认升级成功的代码结构

APP端需要在确认“我已经正常工作了”之后,主动写入确认标志。下面是一个典型的确认逻辑:

// app/ota_confirm.c #include "ota_flag.h" #define APP_RUNNING_OK_MAGIC 0x5A5A5A5A #define CONFIRM_DELAY_MS (10 * 1000) int main(void) { system_init(); watchdog_init(1000); // 1秒不喂狗则复位 ota_flag_init(); // 如果本次是从 Slot B 启动,先不立即确认 if (get_current_boot_slot() == SLOT_B) { // 等待关键业务初始化完成、通讯模块注册成功等条件 delay_ms(CONFIRM_DELAY_MS); if (all_self_check_pass()) { write_ota_flag(OTA_FLAG_FW_CONFIRMED, APP_RUNNING_OK_MAGIC); } } // 进入正常业务主循环 while (1) { watchdog_feed(); run_air_controller(); } }

这个设计的一个细节:即使APP没有在10秒内确认成功,Bootloader也不会立即回滚,而是等启动计数超过阈值后才回滚。这给固件提供了一定的容错空间,比如某些情况下APP需要更长时间完成自检。

6. 完整示例:分区表、OTA标志区与升级流程代码实现

下面给出一个简化但仍然完整的三段式实现思路,包含Bootloader跳转、升级器写入和标志区读写。示例以C语言为主,读者可以在自己的工程中按需裁剪。

6.1 OTA标志区结构定义与读写

OTA标志区是整个方案的“状态存储中枢”,它的结构设计需要考虑到掉电安全。建议用结构体加校验字段,并且使用双页备份。

// ota_flag.h #ifndef __OTA_FLAG_H__ #define __OTA_FLAG_H__ #include <stdint.h> #define OTA_FLAG_PAGE_A_ADDR 0x080E8000u #define OTA_FLAG_PAGE_B_ADDR 0x080E8400u #define FLAG_TARGET_SLOT_A 0x00000000u #define FLAG_TARGET_SLOT_B 0x00000001u #define MAGIC_UPGRADING 0xA5A5A5A5u #define MAGIC_UPGRADE_DONE 0x5A5A5A5Au typedef struct { uint32_t magic; // 升级状态魔数 uint32_t boot_target; // 启动目标:Slot A / Slot B uint32_t boot_count; // 启动失败计数 uint32_t fw_confirmed; // 新固件确认标志 uint32_t reserved[4]; uint32_t crc32; // 以上字段的CRC32校验值 } ota_flag_t; int ota_flag_read(ota_flag_t *flag); int ota_flag_write(const ota_flag_t *flag); void ota_flag_clear_upgrading(void); #endif // __OTA_FLAG_H__
// ota_flag.c #include "ota_flag.h" #include "flash_if.h" #include "crc32.h" #include <string.h> // 简化:实际工程需处理Flash解锁/加锁、擦除对齐、双页一致性 static void write_page(uint32_t addr, const ota_flag_t *flag) { flash_erase_sector(addr); flash_program(addr, (const uint8_t *)flag, sizeof(ota_flag_t)); } static int read_page(uint32_t addr, ota_flag_t *flag) { memcpy(flag, (const void *)addr, sizeof(ota_flag_t)); uint32_t cal_crc = crc32((uint8_t *)flag, sizeof(ota_flag_t) - 4); return (cal_crc == flag->crc32) ? 0 : -1; } int ota_flag_read(ota_flag_t *flag) { ota_flag_t tmp_a, tmp_b; int ret_a = read_page(OTA_FLAG_PAGE_A_ADDR, &tmp_a); int ret_b = read_page(OTA_FLAG_PAGE_B_ADDR, &tmp_b); if (ret_a == 0) { *flag = tmp_a; return 0; } if (ret_b == 0) { *flag = tmp_b; // 如果A页损坏,用B页恢复A页 write_page(OTA_FLAG_PAGE_A_ADDR, &tmp_b); return 0; } return -1; } int ota_flag_write(const ota_flag_t *flag) { ota_flag_t tmp = *flag; tmp.crc32 = crc32((uint8_t *)&tmp, sizeof(ota_flag_t) - 4); write_page(OTA_FLAG_PAGE_A_ADDR, &tmp); write_page(OTA_FLAG_PAGE_B_ADDR, &tmp); return 0; }

这个设计的好处是:两个页互相备份,读任一个成功就能恢复出完整状态;写时先写A页再写B页,即使第二页写入失败,第一页仍然有效。CRC32保证结构体即使读到损坏数据也能被识别出来。

6.2 升级器侧流程:分包写入与逐块校验

升级器可以是MCU内部的一段程序,也可以是一个独立的Bootloader升级模块。核心流程是:接收数据块 -> 擦除目标地址 -> 写入 -> 读回比对 -> 更新进度。

// ota_writer.c #include "ota_flag.h" #include "flash_if.h" #include "crc32.h" #define SLOT_B_BASE 0x08074000u #define SLOT_B_SIZE (400 * 1024) #define BLOCK_SIZE 4096 static uint32_t total_crc = 0; int ota_download_and_flash(uint8_t *block_buf, uint32_t block_index) { if (block_index >= SLOT_B_SIZE / BLOCK_SIZE) { return -1; } uint32_t dst_addr = SLOT_B_BASE + block_index * BLOCK_SIZE; // 1. 擦除 flash_erase_sector(dst_addr); // 2. 写入 flash_program(dst_addr, block_buf, BLOCK_SIZE); // 3. 读回比对 uint8_t read_buf[BLOCK_SIZE]; flash_read(dst_addr, read_buf, BLOCK_SIZE); if (memcmp(read_buf, block_buf, BLOCK_SIZE) != 0) { return -2; // 写入不一致,标记该块失败 } // 4. 累计整包CRC(也可以选用全包校验) total_crc = crc32_update(total_crc, block_buf, BLOCK_SIZE); return 0; } int ota_finish(uint32_t expected_crc) { if (total_crc != expected_crc) { return -1; } ota_flag_t flag; memset(&flag, 0, sizeof(flag)); flag.magic = MAGIC_UPGRADING; flag.boot_target = FLAG_TARGET_SLOT_B; flag.boot_count = 0; flag.fw_confirmed = 0; ota_flag_write(&flag); system_reset(); return 0; }

在实际工程里,还有几个点需要额外小心:

  • 块大小必须按MCU Flash扇区大小对齐,否则擦除会失败或误擦其他区域。
  • 写入前建议关闭全局中断并暂时停止喂狗,但关闭中断时间不能过长,否则MCU的中断实时性会受影响。一般Flash单页写入在几毫秒到几十毫秒,可以接受。
  • 有些MCU在做Flash写操作时,不允许同时读取Flash上的代码,这种情况下需要用RAM中的Flash驱动函数来执行写操作。

6.3 Bootloader跳转代码

Bootloader在完成标志判断和完整性校验后,执行跳转。注意,跳转前需要把中断向量表切换到目标分区,并设置好栈指针。

// bootloader/main.c typedef void (*app_entry_t)(void); void jump_to_app(uint32_t app_base) { uint32_t app_stack = *(volatile uint32_t *)app_base; uint32_t app_vector = *(volatile uint32_t *)(app_base + 4); app_entry_t app_entry = (app_entry_t)app_vector; // 关闭全局中断,避免跳转过程被打断 __disable_irq(); // 设置主栈指针 __set_MSP(app_stack); // 设置中断向量表偏移(Cortex-M系列) SCB->VTOR = app_base; // 执行APP入口 app_entry(); }

需要提醒的是,APP工程的链接脚本也需要把中断向量表放在与app_base对应的地址,且在APP初始化代码中,如果用到RTOS或外设中断,要重新配置向量表。否则跳转后会出现中断无法响应、系统异常复位等现象。

7. 运行结果与效果验证

这一段是很多人容易忽略的部分。OTA不是“能下载、能写Flash”就算完成,而是要反复验证异常场景。下面是推荐的最小验证矩阵。

7.1 正常升级验证

设备当前运行Slot A,向云端请求升级,将新固件写入Slot B,确认CRC后重启。

预期结果:

  • Bootloader打印日志显示boot_target = SLOT_B。
  • 设备从Slot B启动,10秒后OTA标志区写入fw_confirmed。
  • 再次重启,设备仍从Slot B启动,boot_count保持为0。

7.2 固件校验失败验证

在下载完成后、切换之前,人为制造CRC错误(例如通过网络中断传输,或测试工具篡改固件包)。

预期结果:

  • Slot B的完整性校验失败。
  • 设备继续从Slot A启动。
  • 升级标志被清除,系统不进入异常状态。

7.3 掉电注入测试

这是整套方案最关键的一步,也是空调OTA最容易在实验室以外翻车的场景。测试方法是:给设备供电,在升级过程中随机时间点断开电源,然后重新上电,观察设备行为。

建议测试覆盖这些时间点:

  • 擦除Slot B首扇区后断电。
  • Slot B写入50%时断电。
  • Slot B写完后、切换标志写入前断电。
  • 切换标志写入一半时断电。
  • 新版本启动后第2秒断电(APP尚未确认成功)。

每个时间点重复多次,记录结果。

预期结果如下表:

断电时间点重启后行为是否符合预期
写入升级标志前从Slot A正常启动
写入Slot B过程中从Slot A正常启动
写入切换标志前从Slot A正常启动
切换标志写入后尝试启动Slot B,若启动异常则回滚
APP启动确认前启动计数增加,连续失败后回滚

如果任何一组不符合预期,都要回到代码中检查对应阶段的写入顺序和标志策略,而不是简单“多测几次碰运气”。

7.4 看门狗回滚验证

在Slot B的APP中故意加入一个死循环(或延迟喂狗),模拟APP卡死。

预期结果:

  • 设备启动Slot B后,卡死,看门狗复位。
  • 再次启动,boot_count递增。
  • 连续三次后,Bootloader自动切换回Slot A并清除升级标志。

8. 空调OTA升级常见问题与排查方法

问题现象可能原因排查方式解决方案
升级后设备反复重启APP启动确认超时,启动计数递增导致回滚查看Bootloader日志,确认APP是否进入主循环延长确认窗口期,检查APP启动初期是否卡死
设备升级后黑屏/无响应中断向量表未正确设置用调试器查看复位后PC指针位置检查SCB->VTOR设置和APP链接脚本地址
写入切换标志后回滚到旧版本Slot B完整性校验失败打印CRC计算值与期望值检查固件包是否完整,传输过程是否丢包
升级过程掉电后,下次升级无法开始OTA标志区状态残留,状态机未清理读取标志区原始数据在升级前增加“升级重置”流程,非法状态一律视作无升级任务
写Flash时系统卡死擦除/写入期间访问了相同Flash区域,或中断未关闭检查Flash操作是否在RAM中执行将Flash驱动复制到RAM执行,关闭中断
自动回滚不触发启动计数逻辑有误,或看门狗未初始化检查Bootloader是否在跳转前启动看门狗在Bootloader启动早期就初始化看门狗
升级成功率低,但日志无异常电源在升级状态下负载过大,Flash写入电压不足测量升级时MCU供电电压优化升级时序,降低同时运行的负载

还有一个在空调场景中特别容易踩的坑:空调主控板上通常同时存在多个MCU,室内机主控、室外机主控、显示面板、Wi-Fi模组各有一颗芯片。OTA可能只升级主控,但升级期间如果Wi-Fi模组仍在通信、显示面板仍在刷新显示,可能造成电源波动。所以实际产品中,升级期间往往要先关闭强干扰外设,或者把OTA动作安排在整机低功耗运行状态下执行。

9. 生产环境最佳实践与工程建议

9.1 工厂首次烧录必须写“出厂即为稳定版本”

为了防止出厂设备第一次OTA就遇到回滚问题,工厂烧录时应该确保Slot A和Slot B初始状态完全一致,并且两个分区都做完整校验。有些工厂只烧录Slot A,Slot B空着,OTA时才首次写入。这种安排在逻辑上可行,但一旦工厂烧录的Slot A本身有问题,后续升级就缺少回滚依靠。更稳妥的做法是:出厂时两个分区都烧录同一份经过验证的固件。

9.2 升级包必须加签名验签

AB分区解决的是可靠性问题,但OTA还有一个维度的风险:固件被篡改或伪造。如果设备升级包没有签名机制,攻击者可以伪造一个包含恶意代码的“新固件”,诱导设备升级,后果远比升级失败严重。

推荐的做法是:

  • 在编译阶段,对固件包使用非对称签名算法(如ECDSA或RSA)签名。
  • 设备端保存公钥,Bootloader在升级前验证固件签名。
  • 校验不通过则拒绝升级。

值得借鉴的是汽车嵌入式软件OTA的做法,整个链路一般会做“加签验签 + 安全传输 + 安全存储”三层设计。白电虽然威胁等级不如汽车高,但面对安全要求日益严格的物联网环境,签名验签应当成为标配,而不是可选项。

9.3 日志与观测:让每次升级都有据可查

生产环境的OTA最怕“用户说升失败了,但设备已经重启,现场日志全没了”。因此在设计时就要规划好升级日志的输出位置和保留策略:

  • Bootloader阶段日志输出到串口UART,同时尝试写入Flash日志区。
  • APP运行时可以把升级状态上报到云端。
  • 设备发生回滚时,必须保留“当前是回滚后状态”的标记,方便售后人员判断设备是否处于异常状态。

如果没有独立的Flash日志区,建议至少保留几个关键状态变量在参数区,并允许售后通过Wi-Fi/蓝牙读取。

9.4 灰度发布与分批发包

空调设备的升级和手机不太一样。手机用户基数大、出现问题可以快速推补丁;白电设备分散在千家万户,一旦某一批型号批量翻车,召回代价极高。

所以强烈建议在OTA平台侧做灰度发布:

  1. 先选择少量测试设备,升级后观察24小时以上。
  2. 观察指标不仅要看“升级成功率”,还要看“设备活跃度”“故障上报率”“返修率”。
  3. 确认稳定后,再逐步扩大升级范围。

遇到批量回滚时,云端应该能快速停止下发升级包,并允许设备停留在旧版本继续运行,而不是反复重试导致用户困扰。

9.5 升级期间禁止空调压缩机启动

这是一个空调产品特有的工程细节。升级过程中,如果压缩机突然启动,会引起比较大的母线电压波动,可能干扰MCU Flash写入,甚至在写入瞬间导致供电跌落。

实际产品设计中,升级任务启动前要先进入“禁止启动压缩机”状态,确认外机无功率输出后再开始Flash写入。每次写入关键分区前,还可以再检查一次母线电压是否稳定。这一条在空调项目里是硬性要求。

9.6 最小权限与操作安全

如果你是在产线或售后现场手动触发OTA,要注意以下安全边界:

  • 升级工具必须验证操作者身份,避免误操作将全批次设备推送到错误固件。
  • 所有Flash写操作只能在设备已断开强电负载的情况下进行测试验证。
  • 生产环境的固件签名私钥必须离线保存,不随开发工具链分发。

10. 总结:做好空调OTA的核心是“失败可控”,而不是“成功多快”

回到开头的问题。空调OTA升级真正考验的不是下载速度、不是固件压缩率,而是当一切意外发生时,系统能不能保证“设备仍然可用”。

AB分区解决了“升级中途断电永远有一个好分区可启动”的问题;掉电保护解决了“升级状态在意外情况下依然可解析、可恢复”的问题;自动回滚解决的是“新固件本身有问题时系统能自己退回去”的问题。这三者组合在一起,才是一套完整的空调OTA升级可靠性方案。

把这套方案落到你自己的项目上时,建议按照以下顺序推进:

  1. 先根据MCU Flash容量确定Slot A/B是否可行,不行就先压缩固件或换芯片。
  2. 先把OTA标志区的双页备份与Bootloader跳转逻辑实现并验证。
  3. 再做升级器写入流程,重点验证逐块读回和整包CRC。
  4. 再用掉电注入测试反复打击升级过程的每一个阶段。
  5. 最后补签名验签和灰度发布。

下一步值得深入的内容包括:差分增量升级(大幅减少升级包体积)、多副本冗余分区(三个分区互为备份)、以及基于安全芯片的OTA密钥管理。如果你的设备已经具备Wi-Fi联网条件,还可以研究下如何在空调这种带强干扰和电源波动的环境中优化Flash写入时序,这一块每颗MCU的实测数据都不一样,需要在自己的硬件上积累经验。

OTA功能做出来很容易,做到“任何一条升级路径走到头都安全”才见功夫。建议把这篇文章收藏起来,等真正开始做OTA设计时,对照着逐项排查。你在哪个环节掉过坑,或者有什么更好的方案,欢迎在评论区分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询