空调OTA升级,听起来是一件“把新固件包发下去、设备重启一下”就能搞定的事情。但真正做过家电/物联网设备OTA的工程师都清楚,这个功能上线之前最让人睡不着的,不是功能开发,而是那个无法预知的“升级到一半断电了”的场景。空调和手机不一样:手机升级失败最多卡Logo,用户返修或线刷就行;空调升级失败,轻则机器直接变砖无法启动,重则用户报修、售后上门、批量召回。
这篇文章不是去讲“如何调用某个OTA平台接口”,而是解决空调这类对可靠性要求极高的设备,在做OTA升级时真正绕不开的三个工程问题:
- 为什么必须用AB分区,而不是传统的单分区“升级完成后再切换”?
- 升级到任意阶段断电,如何保证设备一定能重新启动?
- 系统启动失败时,如何做到自动回滚,而不是让机器变砖?
围绕这三个问题,我会把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分区架构下,一次完整升级包含以下阶段:
- 升级器从服务器下载新固件,先存入分区B的临时存储区域(或直接写入Slot B的备份区)。
- 全量写入完成后,对Slot B分区做CRC32/MD5/SHA256校验。
- 校验通过后,设置“下一次启动使用Slot B”的启动标志,并执行复位。
- Bootloader读取启动标志,跳转到Slot B运行。
- 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,剩余用于参数存储和升级标志。分区表大致如下:
| 分区 | 起始地址(举例) | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 引导程序,不随OTA更新 |
| Slot A | 0x08010000 | 400KB | 当前出厂版本/上一稳定版本 |
| Slot B | 0x08074000 | 400KB | 升级目标分区 |
| OTA标志区 | 0x080E8000 | 4KB | 启动标志、升级状态、启动计数 |
| 参数区 | 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感知到“母线电压跌落”,然后进入紧急处理流程。
建议的联动动作是:
- 掉电中断触发后,立即停止升级写入流程。
- 关闭不必要的外设,减小电流消耗,为Flash写入争取更多时间。
- 把当前“升级进度”和“已完成的块数”写入OTA标志区(前提是Flash擦写时间足够)。
- 若剩余时间不足以完成一次完整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平台侧做灰度发布:
- 先选择少量测试设备,升级后观察24小时以上。
- 观察指标不仅要看“升级成功率”,还要看“设备活跃度”“故障上报率”“返修率”。
- 确认稳定后,再逐步扩大升级范围。
遇到批量回滚时,云端应该能快速停止下发升级包,并允许设备停留在旧版本继续运行,而不是反复重试导致用户困扰。
9.5 升级期间禁止空调压缩机启动
这是一个空调产品特有的工程细节。升级过程中,如果压缩机突然启动,会引起比较大的母线电压波动,可能干扰MCU Flash写入,甚至在写入瞬间导致供电跌落。
实际产品设计中,升级任务启动前要先进入“禁止启动压缩机”状态,确认外机无功率输出后再开始Flash写入。每次写入关键分区前,还可以再检查一次母线电压是否稳定。这一条在空调项目里是硬性要求。
9.6 最小权限与操作安全
如果你是在产线或售后现场手动触发OTA,要注意以下安全边界:
- 升级工具必须验证操作者身份,避免误操作将全批次设备推送到错误固件。
- 所有Flash写操作只能在设备已断开强电负载的情况下进行测试验证。
- 生产环境的固件签名私钥必须离线保存,不随开发工具链分发。
10. 总结:做好空调OTA的核心是“失败可控”,而不是“成功多快”
回到开头的问题。空调OTA升级真正考验的不是下载速度、不是固件压缩率,而是当一切意外发生时,系统能不能保证“设备仍然可用”。
AB分区解决了“升级中途断电永远有一个好分区可启动”的问题;掉电保护解决了“升级状态在意外情况下依然可解析、可恢复”的问题;自动回滚解决的是“新固件本身有问题时系统能自己退回去”的问题。这三者组合在一起,才是一套完整的空调OTA升级可靠性方案。
把这套方案落到你自己的项目上时,建议按照以下顺序推进:
- 先根据MCU Flash容量确定Slot A/B是否可行,不行就先压缩固件或换芯片。
- 先把OTA标志区的双页备份与Bootloader跳转逻辑实现并验证。
- 再做升级器写入流程,重点验证逐块读回和整包CRC。
- 再用掉电注入测试反复打击升级过程的每一个阶段。
- 最后补签名验签和灰度发布。
下一步值得深入的内容包括:差分增量升级(大幅减少升级包体积)、多副本冗余分区(三个分区互为备份)、以及基于安全芯片的OTA密钥管理。如果你的设备已经具备Wi-Fi联网条件,还可以研究下如何在空调这种带强干扰和电源波动的环境中优化Flash写入时序,这一块每颗MCU的实测数据都不一样,需要在自己的硬件上积累经验。
OTA功能做出来很容易,做到“任何一条升级路径走到头都安全”才见功夫。建议把这篇文章收藏起来,等真正开始做OTA设计时,对照着逐项排查。你在哪个环节掉过坑,或者有什么更好的方案,欢迎在评论区分享。