ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

空调OTA升级避坑:AB分区、掉电保护与自动回滚工程实践

空调OTA升级避坑:AB分区、掉电保护与自动回滚工程实践 空调OTA升级避坑实战AB分区、掉电保护与自动回滚工程方案很多做空调、热水器、智能家居网关的嵌入式开发同学都有过这样一段经历用户在App上点了“立即升级”几秒后设备离线然后就再也叫不醒了。售后报修工单里写着“设备无法开机需要上门刷机”维修师傅拆开面板发现bootloader已经被新固件覆盖了一部分半新半旧没有任何恢复入口。这不是段子而是智能家电OTA项目里最常见的生产事故。空调OTA升级和手机OTA升级有一个本质区别手机升级失败用户还能长按电源键重启、进Recovery、连电脑刷机但空调安装在墙壁上、天花板上一旦引导程序损坏维修人员要爬高拆机直接动主控板。这个成本不是一台手机能比的。所以空调设备做OTA必须比手机更保守、更严谨。这篇文章不讨论“OTA有什么用”而是直接进入最容易出生产事故的三个环节升级包怎么放——AB分区布局升级过程中断电怎么办——掉电保护升级失败后怎么恢复——自动回滚。我会把一个最小可落地的空调主控OTA工程方案拆开讲从分区设计到bootloader校验再到掉电恢复和回滚状态机全部给出代码级示例和验证方法。读完你可以理解设备收到升级包之后bootloader和APP固件之间到底如何协作才能做到“升级失败不变成砖”。1. 空调OTA升级为什么容易出事故1.1 OTA不是“把新固件写进去”那么简单很多人第一次做OTA时会觉得OTA就是通过网络下载一个bin文件然后调用flash写入接口把新固件写进芯片最后复位重启。这个流程听上去很简单但放到实际空调主控板上每一环都有翻车风险。以常见的MCU方案为例固件可能存储在外部SPI Flash里也可能存储在芯片内部Flash。写入外部Flash时如果写入过程中断电Flash中可能出现半个扇区的数据错乱写入内部Flash时如果擦除扇区和编程之间被中断轻则升级包不完整重则把当前正在运行的系统代码区破坏掉。更隐蔽的问题是分区重叠。有些起步阶段的方案只有一个固件分区新固件直接覆盖正在运行的固件区域。一旦升级包在下载或校验阶段出错设备重启后连旧系统都找不回来。这就是“OTA升级变砖”最常见的根因。1.2 空调整机OTA的特殊约束空调和普通的物联网传感器节点还不一样它有更强的实时控制需求、更复杂的硬件外设、更长的产品生命周期。具体到OTA上有三个约束第一空调没有“空闲时间”。空调在制冷、制热运行时主控板在实时采集温度、控制压缩机、调节风机。OTA升级通常需要设备处于待机或不运行状态。如果用户在半夜升级刚好压缩机正在运转触发升级后系统复位可能造成压缩机频繁启停或者设备状态丢失。第二空调的主控芯片型号差异大。从8位MCU到Cotex-M系列、再到带MPU的高端SoCFlash容量从几十KB到几MB不等。不是所有芯片都原生支持双Bank对称映射很多低端芯片做AB分区只能靠Flash地址手动划分这给工程实现增加了不少复杂度。第三售后成本极高。空调一旦变砖不是用户自己刷机就能解决的。所以空调OTA的设计目标从来不是“升级成功率高”而是“失败后一定能回到可运行状态”。换句话说回滚能力比升级成功率更重要。2. OTA升级的核心概念与AB分区原理2.1 从单分区到双分区为什么需要AB分区传统单分区方案里Flash里只有一个固件运行区OTA新固件写入后会覆盖旧固件。升级过程中的任何一次异常复位都可能导致设备无法启动。AB分区也叫双分区方案它将Flash划分为两个固件区域分别叫Slot A和Slot B。系统当前从哪个槽位启动哪个就是“活动槽位”。OTA时系统只往另一个非活动槽位写入新固件不影响当前运行的系统。写入完成后设置引导标志重启后bootloader优先从新槽位启动。AB分区的核心价值不是“存储两个固件”而是保证了任意时刻Flash里都至少有一个完整的可启动固件。就算新固件写入失败、校验失败、启动失败旧固件依然完整存在回滚在理论上永远可行。从工程角度看AB分区付出的代价是Flash容量翻倍。对于存储容量受限的低成本方案也有人用“压缩存储”“差分包”来缓解但这不改变AB分区作为OTA安全基座的地位。2.2 AB分区的两个关键副作用掉电保护和回滚很多人用AB分区只是为了“装两个固件”其实这两个槽位配合状态标志能同时解决OTA里最难的两个问题。第一个是掉电保护。OTA升级写入B槽位的过程中任意时刻断电B槽位都是不完整的但A槽位仍然完好。设备重启后bootloader检查到B槽位写入未完成或校验不通过直接放弃B槽位继续从A槽位运行升级失败但设备不损坏。第二个是自动回滚。B槽位写入完整校验通过bootloader尝试从B槽位启动。如果新固件在启动后某个时间段内没有“报活”Heartbeatbootloader判定新固件不可用再次重启后自动切回A槽位。这样即使新固件存在隐藏bug导致设备启动后死机也能通过回滚机制恢复到旧版本等待下一次升级修复。所以AB分区解决的不只是存储问题它同时是掉电保护策略和自动回滚策略的物理基础。2.3 常见误区有人觉得既然有AB双分区OTA时是不是就可以随便断电、随便失败不是。AB分区保证了“失败不损坏”但没有保证“升级能成功”。升级包下载不完整、签名校验失败、写入逻辑有bug、flash坏块这些仍然会造成升级终止。AB分区只是把最坏情况从“变砖”降级为“升级失败但不影响运行”。还有人把AB分区和“备份分区”混为一谈。备份分区通常是某个固定区域的只读映像而AB分区的两个槽位都是可被OTA写入的二者在更新策略上完全不同。AB分区更灵活但对分区管理和引导逻辑的要求也更高。3. AB分区布局与分区表设计3.1 典型的分区布局我们以一个常见的带外部SPI Flash的空调主控为例。假设Flash容量为8MB地址空间如下分配| Bootloader | Slot A (APP) | Slot B (APP) | Param / NVS | OTA Download Cache |Bootloader区存放引导程序一般也负责固件校验、回滚判定通常不允许OTA覆盖。Slot A当前默认运行的APP固件区。Slot BOTA新固件写入区同时也是另一个可启动槽位。Param/NVS存放运行参数、状态标志、SN、校准数据等。OTA Download Cache用于下载缓存升级包数据可按扇区分批搬运到非活动槽位。如果芯片Flash较小也可以把OTA Download Cache省略改为“下载一块、写入一块”。但这里强烈建议保留一个独立的下载缓存区因为从网络接收的数据包和Flash写入的速度不匹配没有缓存会导致写入压力过大断电时更容易留下半包数据。3.2 分区表数据结构分区表是OTA工程里最需要对齐的信息结构。它必须同时存在于bootloader和APP固件中且升级过程中不允许被随意修改。下面给出一个通用的分区表定义// 文件路径common/partition.h #ifndef __PARTITION_H__ #define __PARTITION_H__ #include stdint.h #define PARTITION_NAME_MAX_LEN 16 #define PARTITION_MAGIC 0xA5A5A5A5 typedef enum { PART_TYPE_BOOTLOADER 0, PART_TYPE_APP, PART_TYPE_PARAM, PART_TYPE_CACHE, PART_TYPE_UNKNOWN } partition_type_t; typedef struct { uint32_t magic; // 分区表魔数用于校验 char name[PARTITION_NAME_MAX_LEN]; partition_type_t type; // 分区类型 uint32_t offset; // 起始地址偏移单位字节 uint32_t size; // 分区大小单位字节 uint32_t crc32; // 分区项CRC校验值 } partition_entry_t; typedef struct { uint32_t magic; // 分区表整体魔数 uint32_t entry_count; // 分区项数量 uint32_t version; // 分区表版本号 partition_entry_t entries[8];// 分区项数组按需扩展 uint32_t crc32; // 分区表整体CRC校验值 } partition_table_t; /* 获取指定槽位分区信息 */ int partition_get_entry(partition_table_t *tbl, const char *name, partition_entry_t *out); #endif建议在量产前把分区布局固定下来任何一处地址偏移的修改都要同步更新bootloader和APP的分区表定义。分区表变更应该是“一次发布、配套升级”不能出现APP已经用了新地址bootloader还按旧地址引导的情况。4. 掉电保护升级过程随时可能断电4.1 掉电中断的检测空调所在的家庭用电环境并不稳定。用户可能拔插头、空气开关跳闸、雷击导致短暂停电。OTA升级过程通常持续几秒到几十秒正好落在“随时可能断电”的时间窗口。掉电保护的第一步是检测掉电。硬件上通常使用电源监控芯片或MCU自带的掉电比较器BOD/PVD。当供电电压跌落到阈值以下时MCU会触发一个高优先级中断。嵌入式程序在这个中断里需要立刻停止对Flash的写入记录当前升级进度并尽量保持系统时钟和外设进入安全状态。需要特别注意的是从掉电中断触发到系统的可用电能完全消失一般只有几毫秒到几十毫秒这段窗口期不足以完成一次完整的Flash扇区擦除。所以掉电检测能做的是“止损”而不是“抢救”。真正保证数据完整性的是AB分区结构配合升级进度和状态标志的管理。4.2 数据写入的原子性处理掉电保护的核心设计原则是每一步写入都必须可恢复。最简单实用的做法是先把目标分区标记为“写入中”再写数据写完校验通过后才把它标记为“可启动”。在代码实现上可以用三个状态值来描述某个槽位的状态INVALID槽位无固件或固件不完整不可启动。IN_PROGRESS槽位正在被写入尚未校验不可启动。VALID槽位固件完整且通过校验可以被bootloader选中启动。所有状态标志的写入必须使用“先擦后写”的安全方式并且建议放在独立的状态扇区中避免和固件数据混在一起。下面给出一个简化的状态管理接口// 文件路径common/ab_update.h #ifndef __AB_UPDATE_H__ #define __AB_UPDATE_H__ #include stdint.h #include partition.h typedef enum { SLOT_STATE_INVALID 0, SLOT_STATE_IN_PROGRESS, SLOT_STATE_VALID } slot_state_t; /* 读取指定槽位状态 */ int ab_get_slot_state(uint8_t slot, slot_state_t *state); /* 设置指定槽位状态建议在写状态前先备份旧值 */ int ab_set_slot_state(uint8_t slot, slot_state_t state); /* 获取当前活动槽位 */ int ab_get_active_slot(uint8_t *slot); /* 设置活动槽位 */ int ab_set_active_slot(uint8_t slot); /* 标记升级开始此时目标槽位进入 IN_PROGRESS */ int ab_start_update(uint8_t target_slot); /* 升级数据全部写入后调用触发完整校验 */ int ab_finish_update(uint8_t target_slot); #endif在任何状态标志写入之前要先把旧状态保存到另一个扇区的备份位置。因为状态扇区本身也可能在写入时掉电如果只写一份下次启动时读到的是一个擦除后的随机值无法判断当前到底处于什么状态。常见的做法是同一状态写两份一份为主一份为备份启动时读取并比对不一致则采用更保守的那一个。4.3 断点续传还是从零开始有些方案会做“断点续传”记录已经写入到哪个扇区断电恢复后从断点继续写。这个功能在产品上有价值但复杂度很高。断点续传要求对每个扇区的写入状态做精确记录而且要考虑Flash擦写特性。如果项目周期紧、存储空间够我更推荐“断电后重新下载整包重写”的策略。空调的运行特性决定了OTA升级次数不会非常频繁重新下载一次完整固件的成本是可控的。整包重写逻辑简单状态机少一半排错成本也低很多。对于大多数MCU级产品这是性价比最高的方案。5. 自动回滚从“失败”到“恢复”5.1 回滚判定条件自动回滚不是一个独立功能它依赖bootloader和APP的协作。回滚判定的本质是新固件启动后是否在指定时间内“证明自己可用”。常见判定条件有四个启动校验失败新固件分区CRC、签名校验不通过直接回滚。启动超时bootloader给APP设置一个启动超时时间比如30秒内APP没有完成启动流程则回滚。报活失败APP启动后需要周期性上报心跳如果在预定的“尝试窗口”内没有上报bootloader触发回滚。显式回滚APP运行过程中检测到严重错误如配置文件损坏、外设初始化失败主动设置回滚标志并复位。需要强调的是自动回滚不是“启动一次失败就立刻切回”。更稳妥的做法是如果新固件连续启动失败达到N次才真正触发回滚。因为偶尔一次启动失败可能来自外部环境干扰不能轻易判定新固件不可用。5.2 bootloader回滚流程回滚流程需要由一个不会被OTA覆盖的bootloader来执行。空调主控在复位后bootloader按以下顺序处理读取当前活动槽位和备用槽位的状态如果备用槽位是VALID且尝试次数超过阈值则回滚到备用槽位如果当前槽位是IN_PROGRESS或INVALID则直接回滚到备用槽位如果两个槽位都不可用进入串口或USB强制升级模式等待维修工具连接。下面给出一个bootloader回滚判定的伪代码流程reset_reason read_reset_reason(); slot_a_state read_slot_state(SLOT_A); slot_b_state read_slot_state(SLOT_B); active_slot read_active_slot(); if (slot_a_state SLOT_STATE_VALID slot_b_state SLOT_STATE_VALID) { /* 两个槽位都可用继续从 active_slot 启动 */ boot_from(active_slot); } else if (slot_a_state ! SLOT_STATE_VALID slot_b_state SLOT_STATE_VALID) { /* A损坏B可用 */ set_active_slot(SLOT_B); boot_from(SLOT_B); } else if (slot_a_state SLOT_STATE_VALID slot_b_state ! SLOT_STATE_VALID) { /* B损坏A可用这种情况最常见于升级写入中途掉电 */ set_active_slot(SLOT_A); boot_from(SLOT_A); notify_upgrade_failed(); } else { /* 两个槽位都不可用进入强制恢复模式 */ enter_recovery_mode(); }这个流程看起来不复杂真正容易出错的是状态标志的读写时序、复位类型判断以及两个槽位都VALID时如何选择。有一些方案还会在每次启动时递增“当前槽位尝试计数”只有校验通过并运行稳定后才清零。这是更工程化的做法。6. 完整示例一个最小可落地的OTA升级流程6.1 示例环境说明为了不让代码绑定特定芯片下面示例采用抽象接口的方式编写只依赖C语言和标准类型的定义。实际移植时你需要把flash_read、flash_erase、flash_write等函数替换为你所用SDK的API。无论芯片是ESP32、STM32还是其他平台这套流程都可以照搬思路。我们假设主控MCU的Flash布局如下分区偏移地址大小Bootloader0x0000000064KBSlot A0x000100001MBSlot B0x001100001MBNVS / Param0x00210000128KBDownload Cache0x00230000512KB实际项目中请以你的Flash大小和芯片手册为准不要照抄偏移地址。6.2 升级入口代码APP收到云端下发的新固件后需要完成下载、分包校验、往Slot B写入、更新状态标志。我们用一个简化函数展示主流程// 文件路径app/ota_service.c #include string.h #include ab_update.h #include partition.h #include flash_hal.h #include crc32.h #include ota_network.h #define OTA_BLOCK_SIZE 4096 #define DOWNLOAD_TIMEOUT_MS 30000 typedef struct { uint32_t total_len; uint32_t received_len; uint32_t crc32_from_header; uint16_t block_seq; } ota_context_t; static ota_context_t s_ota_ctx; /* 从网络接收一包数据 */ static int ota_recv_block(uint8_t *buf, uint32_t size, uint32_t timeout_ms); /* 检查当前是否允许OTA不在制冷/制热运行中 */ static int ota_safety_check(void); int ota_start(const char *download_url) { if (ota_safety_check() ! 0) { return -1; /* 当前正在运行压缩机拒绝升级 */ } /* 获取 Slot B 分区信息 */ partition_entry_t slot_b; if (partition_get_entry(g_part_table, slot_b, slot_b) ! 0) { return -2; } /* 标记升级开始 */ if (ab_start_update(SLOT_B) ! 0) { return -3; } uint8_t block_buf[OTA_BLOCK_SIZE]; uint32_t flash_addr slot_b.offset; uint32_t total_written 0; while (total_written s_ota_ctx.total_len) { int ret ota_recv_block(block_buf, sizeof(block_buf), DOWNLOAD_TIMEOUT_MS); if (ret ! 0) { /* 下载失败Slot B 仍保持 IN_PROGRESS / 可清理 */ ab_set_slot_state(SLOT_B, SLOT_STATE_INVALID); return -4; } /* 擦写前保证目的地址在Slot B范围内 */ if (flash_erase(flash_addr, OTA_BLOCK_SIZE) ! 0) { ab_set_slot_state(SLOT_B, SLOT_STATE_INVALID); return -5; } if (flash_write(flash_addr, block_buf, OTA_BLOCK_SIZE) ! 0) { ab_set_slot_state(SLOT_B, SLOT_STATE_INVALID); return -6; } /* 读回验证防止写入bit错误 */ uint8_t readback[OTA_BLOCK_SIZE]; flash_read(flash_addr, readback, OTA_BLOCK_SIZE); if (memcmp(readback, block_buf, OTA_BLOCK_SIZE) ! 0) { ab_set_slot_state(SLOT_B, SLOT_STATE_INVALID); return -7; } flash_addr OTA_BLOCK_SIZE; total_written OTA_BLOCK_SIZE; } /* 整个分区写入完成触发完整固件校验 */ if (ab_finish_update(SLOT_B) ! 0) { return -8; } /* 设置下一次启动从Slot B启动 */ ab_set_active_slot(SLOT_B); /* 通知系统复位进入新固件 */ system_reboot(); return 0; }这段代码里有几个容易被忽略的细节写入前先擦除整个块不能只擦除扇区的一部分每次写入后都要读回比对最好用CMA或CRC重新计算整包校验而不只是逐块校验下载或写入失败时立即把Slot B状态改为INVALID避免bootloader误以为存在一个可用但不完整的固件。6.3 bootloader校验与回滚代码bootloader的职责是“用最少的代码、最少的出错机会把设备引导到可运行的分区”。我们给出一个bootloader侧的启动选择逻辑// 文件路径bootloader/boot_main.c #include ab_update.h #include crc32.h #include flash_hal.h #include partition.h #define APP_HEADER_MAGIC 0xCAFEBABE #define ROLLBACK_THRESHOLD 3 typedef struct { uint32_t magic; uint32_t firmware_len; uint32_t crc32; uint32_t version; } app_header_t; static int app_header_check(uint32_t slot_offset) { app_header_t hdr; flash_read(slot_offset, (uint8_t *)hdr, sizeof(hdr)); if (hdr.magic ! APP_HEADER_MAGIC) { return -1; } if (hdr.firmware_len 0 || hdr.firmware_len (1 * 1024 * 1024)) { return -2; } uint32_t calc_crc crc32_calc(slot_offset sizeof(hdr), hdr.firmware_len - sizeof(hdr)); if (calc_crc ! hdr.crc32) { return -3; } return 0; } void boot_main(void) { uint8_t active_slot; slot_state_t state_a, state_b; ab_get_slot_state(SLOT_A, state_a); ab_get_slot_state(SLOT_B, state_b); ab_get_active_slot(active_slot); /* 情况1两个槽位都VALID按活动槽位启动 */ if (state_a SLOT_STATE_VALID state_b SLOT_STATE_VALID) { if (active_slot SLOT_B app_header_check(SLOT_B_OFFSET) 0) { jump_to_app(SLOT_B_OFFSET); } else if (app_header_check(SLOT_A_OFFSET) 0) { ab_set_active_slot(SLOT_A); jump_to_app(SLOT_A_OFFSET); } } /* 情况2B失效但A有效说明上次升级失败本应发生在A启动后 */ if (state_b ! SLOT_STATE_VALID state_a SLOT_STATE_VALID) { ab_set_active_slot(SLOT_A); jump_to_app(SLOT_A_OFFSET); } /* 情况3A失效但B有效尝试从B启动 */ if (state_a ! SLOT_STATE_VALID state_b SLOT_STATE_VALID) { ab_set_active_slot(SLOT_B); jump_to_app(SLOT_B_OFFSET); } /* 情况4两个都失效进入恢复模式 */ enter_recovery_mode(); }这里注意bootloader侧的代码越简单越好。不要在bootloader里做联网、解密、解压等复杂操作。bootloader只是“裁判”负责选择正确的A或B槽位启动并管理回滚计数。一旦bootloader自身逻辑复杂化它本身的故障风险也会上升。6.4 APP启动后的报活与回滚计数APP侧需要配合bootloader完成“可用性确认”。常见做法是APP启动后初始化外设运行自检如果一切正常清空回滚计数并把当前槽位标记为“已稳定”如果检测到严重异常主动增加回滚计数请求重启。// 文件路径app/system_monitor.c #include ab_update.h #define HEARTBEAT_WINDOW_S 30 void system_monitor_init(void) { /* 启动后开启一个定时器在HEARTBEAT_WINDOW_S内上报存活 */ start_one_shot_timer(HEARTBEAT_WINDOW_S, on_heartbeat_timeout); } void on_heartbeat_timeout(void) { /* * 若超时未完成自检说明新固件可能无法正常工作。 * 这里应主动触发回滚而不是继续卡死。 */ ab_set_slot_state(SLOT_B, SLOT_STATE_INVALID); system_reboot(); } void on_app_ready(void) { /* 自检通过通知bootloader当前系统稳定 */ ab_clear_rollback_counter(); ab_set_slot_state(SLOT_A, SLOT_STATE_VALID); }需要注意的是回滚计数应该存储在独立的NVS区域并且不能在每次启动时都被新固件清掉。只有当APP真正完成自检、外设就绪、与云端连接成功之后才允许清空计数。否则一个启动即崩溃的新固件会在“崩溃—重启—计数—崩溃”中让设备反复重启最终bootloader在达到阈值后自动切回旧固件。7. 运行结果与升级验证7.1 正常升级场景在开发板上模拟一次正常OTA升级预期流程如下设备当前从Slot A启动状态为VALID云端下发新固件APP将固件写入Slot BSlot B状态变为VALID活动槽位切到Slot B设备重启bootloader引导到Slot BAPP启动自检并上报心跳回滚计数器清零。升级成功后设备继续运行新固件旧固件仍然保留在Slot A里作为下一次回滚的底牌。7.2 模拟断电场景这是验证掉电保护最有效的方法。在开发板上反复执行OTA写入并在写入过程中用继电器或手动按钮随时切断电源。每次断电后重新上电检查设备能否从旧固件恢复启动。开发阶段可以写一个自动化脚本配合程序控制随机延时断电重复测试50次以上。判断标准是断电后重新上电设备能正常进入旧固件旧固件运行后Slot B的状态不是VALID设备不会进入无法引导的状态状态标志和数据区没有出现不一致。7.3 模拟坏包场景人为构造一个CRC错误或签名错误的升级包下发到设备。此时APP应该在写入前就拒绝升级或者在写入完成后被校验逻辑拦截。bootloader侧则应该在启动时发现校验失败自动回退到旧固件。如果设备支持远程日志上报排查时可以直接在云端看到“升级校验失败”和“回滚成功”的日志。否则需要配合串口打印和调试器观察状态。8. 常见问题与排查方法问题现象可能原因排查方式解决方案升级过程中断电重启后设备变砖分区表被覆盖或状态标志写入不完整用调试器查看Flash分区表内容和状态扇区值确保bootloader区不被OTA写入状态标志使用双备份存储新固件启动后反复重启最终无法开机回滚计数未被正确管理查看NVS中的回滚计数和复位原因APP启动自检通过后再清空回滚计数设置回滚阈值升级包下载完成但校验一直失败下载缓存区溢出或分包顺序错乱检查下载缓存区大小和各分包的seq值增加下载缓存区增加序列号校验两个槽位状态都变成VALID但系统启动到旧固件活动槽位标志未更新或被误写检查ab_set_active_slot的写入顺序和备份状态活动槽位标志应最后更新并做双备份OTA写入速度太慢升级窗口过长每次写块前擦除整个扇区且没有利用Flash页写入特性分析flash_erase和flash_write耗时按扇区批量擦除再连续写入多个页减少擦除次数新固件启动时间超过bootloader超时时间bootloader超时设置过短或APP启动流程过长查看bootloader的超时配置和APP启动时间将超时配置为预估值的2倍以上或在启动阶段增加进度上报云端下发失败设备反复重试下载网络不稳定或升级服务未做断线重试查看OTA下载模块的重试策略和错误码增加指数退避重试策略失败达到上限后停止并上报出现问题时第一件事不是改代码而是看状态标志和复位原因。只要状态标志设计得合理大多数现场问题都可以通过日志定位到具体环节。建议在设备端输出结构化日志例如[OTA][2025-06-01 12:00:01] start update to slot B [OTA][2025-06-01 12:00:10] block 10/100 written [OTA][2025-06-01 12:00:20] block 20/100 written [OTA][2025-06-01 12:00:35] slot B set VALID [OTA][2025-06-01 12:00:36] active slot switch to B [OTA][2025-06-01 12:00:40] reboot to slot B有了这种日志排查“升级后断电”“升级后无响应”的效率会大幅提升。9. 工程化最佳实践9.1 固件签名与校验OTA升级必须对升级包做签名验证不能只依赖CRC。CRC只能发现偶然性错误无法防止固件被篡改或替换。空调设备一般有较长的生命周期一旦攻击者通过升级包注入恶意固件影响的不只是设备本身还可能波及整个家庭网络。推荐做法使用非对称签名私钥保存在离线构建服务器公钥固化在设备bootloader中升级包在下载完成后先验签名再验CRCbootloader启动每个槽位前都要验证固件头部的签名签名验证不通过时该槽位视为无效转入回滚或恢复模式。这里要再次提醒签名密钥的保管比代码本身更重要。如果私钥泄露整个产品线都可能被刷入恶意固件。9.2 日志与遥测OTA升级失败后维修人员和研发人员往往看不到设备现场只能依赖日志。建议在设备端实现一套“升级事件日志”包括升级开始时间、升级包版本、目标槽位、当前槽位、每一个关键节点、失败错误码、复位原因。这类日志可以存储在NVS中即使设备崩溃下次启动后仍能读出。如果设备支持联网最好在升级前后给云端上报事件OTA_STARTOTA_BLOCK_WRITE_OKOTA_VERIFY_OKOTA_SWITCH_SLOTBOOT_FAILEDROLLBACK_OK有了这些事件即使售后问题已经发生研发也能快速判断是网络问题、固件问题还是硬件问题。9.3 灰度发布与熔断空调设备量一大OTA发布就不能盲目全量推送。建议分阶段灰度先推送给内部测试设备再推送5%到10%的设备观察24小时内崩溃率和回滚率确认无异常后再扩大到50%最后全量推送。同时在云端设置“升级失败熔断”机制如果某个版本设备的启动失败率超过预设阈值自动暂停该版本的推送避免问题扩大。9.4 器件选型与Flash磨损Flash有擦写寿命限制常见的SPI Flash P/E次数在1万到10万次之间。空调产品生命周期内不可能频繁OTA但如果升级逻辑写得不好反复失败重试会在短时间内消耗大量P/E周期。建议避免对同一个扇区反复擦写引入磨损均衡状态标志和回滚计数的写入频率要低不要每次启动都写在量产测试中做Flash坏块检测标记坏块区域对OTA下载缓存区使用独立的坏块管理策略。9.5 测试验收清单工程交付前建议按以下清单验收新固件整包校验失败设备不启动、不误导写入中途断电恢复后能回到旧固件新固件启动即崩溃连续N次后自动回滚两个槽位都可用时能按活动槽位正常启动升级包签名错误时设备拒绝升级升级完成后旧固件仍可作为回滚目标升级事件日志完整错误码清晰可读多次断电和坏包测试后Flash无异常坏块增长。10. 总结与后续学习方向空调OTA不是“下载新固件、写进Flash、重启”三句话能概括的。它更像一个在Flash资源有限、运行环境不稳定的情况下需要同时保证设备可用性、升级成功率、远程可维护性的系统工程。这篇文章里核心就三件事第一用AB分区保证Flash中永远有一个可启动的固件这是所有安全策略的物理基础第二用掉电保护和状态标志管理保证升级过程在任意时刻断电都不会损坏设备第三用bootloader配合APP心跳和回滚计数让升级失败后能自动回到旧版本而不是让设备变砖。后续值得继续深入的方向有三个。一是差分包升级空间受限时通过压缩差分包减少下载和写入数据量二是多槽位扩展一些高端设备开始设计三个槽位用于支持“当前版—上一版—灰度版”并存三是OTA与配置管理结合升级固件的同时还要管理配置参数、密钥、证书的原子更新。如果你正要给空调、热水器或其他家电设备做OTA建议先从本文的AB分区和掉电恢复逻辑入手在开发板上反复做断电实验把回滚流程打磨到“100次断电不坏板”的程度再考虑对接云端服务。OTA这件事设计越保守生产事故越少。把这篇文章的代码和思路跑通一遍你就有了一个能应对大多数OTA事故的底子。
返回列表