24、IoT设备OTA方案:资源受限设备升级、差分算法优化、断点续传机制

做IoT设备的OTA升级,跟手机升级完全是两码事。

手机有8GB内存、256GB存储,升级包几百兆都无所谓。但IoT设备呢?我见过最夸张的——Flash只有512KB,RAM只有64KB,连一个完整的固件镜像都放不下。这种设备怎么做OTA?

说白了,资源受限设备的OTA,核心就三个字:省、快、稳。省空间、省带宽、省电量;快下载、快校验、快切换;稳传输、稳恢复、稳回滚。

这一章,我就把IoT设备OTA的实战经验掰开揉碎讲给你听。

核心挑战:IoT设备OTA不是简单的「下载→校验→刷写」,而是在资源极度受限的条件下,完成可靠、安全、低成本的固件升级。

24.1 资源受限设备的升级策略

先说说硬件限制到底有多苛刻。

我做过一个智能电表项目,主控是Cortex-M4,Flash 1MB,RAM 128KB。听起来还行?但系统本身占掉400KB,应用占300KB,剩下300KB要存OTA包、做解压、做校验。你想想看,一个完整的固件镜像就800KB,怎么塞得下?

常见的升级策略有三种:

策略 原理 Flash需求 适用场景
双区备份(A/B分区) 两个完整固件分区,一个运行,一个升级 2×固件大小 对可靠性要求极高
单区+外部存储 固件下载到外部Flash/SPI Flash,校验后刷入内部Flash 1×固件 + 外部存储 内部Flash不足,但有外部存储
差分升级(增量升级) 只传输新旧固件的差异部分 1×固件 + 差分包 带宽和存储都受限

我个人习惯,优先考虑差分升级。为什么?因为IoT设备很多是电池供电的,每次传输1MB数据和传输100KB数据,功耗差了一个数量级。而且差分包小,下载快,失败概率也低。

实战建议:如果Flash实在不够,可以考虑「运行时解压」方案。差分包压缩后传输,边下载边解压边刷写,但这对CPU算力有要求。我在一个低功耗蓝牙门锁上试过,效果不错,但要注意解压速度不能拖慢刷写流程。

24.2 差分算法优化:从bsdiff到自定义差分

差分算法,说白了就是找出新旧两个固件之间的「差异」,然后只传输差异部分。

最经典的差分算法是bsdiff,它基于后缀排序,能生成非常小的差分包。但bsdiff有个致命问题——内存占用极高。一个1MB的固件,bsdiff需要几十MB的RAM来跑差分计算。这在服务器端没问题,但在IoT设备端做差分还原时,内存根本扛不住。

我遇到过的情况:用bsdiff给一个智能插座做差分,差分包确实小,只有80KB。但设备端跑还原时,内存直接爆了。后来我换成了HDiffPatch,内存占用降到了几百KB,虽然差分包大了点(120KB),但设备能跑起来了。

差分算法的优化方向:

  • 内存优化:采用分块差分,把固件切成小块(比如4KB一块),逐块计算差异。这样内存占用只跟块大小有关,跟固件总大小无关。
  • 算法选择:bsdiff适合大固件、高压缩率;xdelta3适合中等固件、平衡性能;HDiffPatch适合资源受限设备。
  • 指令集优化:如果MCU支持SIMD指令,可以加速差分还原中的内存拷贝和比较操作。

注意:差分升级有一个隐藏坑——固件版本管理。如果设备当前版本跟服务器预期的版本不一致,差分包就无法应用。我曾经因为版本号管理混乱,导致一批设备升级失败,最后只能全量升级。所以,差分升级必须配合严格的版本控制策略。

下面是一个简单的差分还原流程(伪代码):

// 差分还原:将旧固件 + 差分包 → 新固件
int apply_diff(const uint8_t* old_fw, uint32_t old_size,
               const uint8_t* diff_patch, uint32_t patch_size,
               uint8_t* new_fw, uint32_t new_size) {
    uint32_t offset = 0;
    while (offset < patch_size) {
        uint8_t cmd = diff_patch[offset++];
        if (cmd == COPY_CMD) {
            // 从旧固件拷贝一段数据
            uint32_t src_off = read_uint32(diff_patch, &offset);
            uint32_t len = read_uint32(diff_patch, &offset);
            memcpy(new_fw + write_pos, old_fw + src_off, len);
            write_pos += len;
        } else if (cmd == ADD_CMD) {
            // 直接写入新数据
            uint32_t len = read_uint32(diff_patch, &offset);
            memcpy(new_fw + write_pos, diff_patch + offset, len);
            offset += len;
            write_pos += len;
        }
    }
    return 0;
}

这段代码看起来简单,但实际工程中要考虑对齐、校验、中断保护等问题。嗯,这里要注意:差分还原过程中不能断电,否则固件就废了。所以一定要配合CRC校验和回滚机制。

24.3 断点续传机制:从传输层到应用层

IoT设备的网络环境有多差?我做过一个农业物联网项目,设备在田里,信号时有时无。一个200KB的升级包,下载了5次才成功。如果没有断点续传,每次失败都要从头下载,那用户体验简直灾难。

断点续传,说白了就是「从哪里断的,从哪里接着传」。但实现起来有几个关键点:

  • 传输层支持:HTTP Range头、MQTT的会话续传、CoAP的块传输(Block-wise Transfer)。
  • 应用层状态记录:设备端要记录已下载的块序号、块大小、块校验值。这些信息要写入非易失存储(比如Flash的一个专用扇区)。
  • 服务端无状态设计:服务端最好不保存设备状态,所有状态由设备端维护。这样服务端可以水平扩展,设备端随时可以续传。

我常用的断点续传流程:

  1. 设备发起升级请求,服务端返回升级包的总大小和分块信息(比如每块4KB)。
  2. 设备检查本地是否有未完成的下载记录。如果有,从断点处请求剩余块。
  3. 每下载一块,立即写入Flash,并更新下载状态记录。
  4. 所有块下载完成后,进行整体校验。校验通过后,标记「下载完成」,然后进入刷写阶段。

关键设计:下载状态记录要写入Flash的「安全区域」,不能跟固件分区冲突。我一般会在Flash末尾预留4个扇区(每个扇区4KB),循环写入状态记录,防止频繁擦写导致Flash损坏。

下面是一个断点续传的状态机:

// 断点续传状态机
enum ota_state {
    OTA_IDLE,           // 空闲
    OTA_DOWNLOADING,    // 下载中
    OTA_DOWNLOAD_PAUSE, // 下载暂停(断点)
    OTA_DOWNLOAD_DONE,  // 下载完成
    OTA_FLASHING,       // 刷写中
    OTA_FLASH_DONE,     // 刷写完成
    OTA_ROLLBACK        // 回滚
};

// 状态记录结构体
struct ota_checkpoint {
    uint32_t magic;          // 魔数,用于校验
    uint32_t total_blocks;   // 总块数
    uint32_t downloaded;     // 已下载块数
    uint32_t crc32;          // 已下载数据的CRC
    uint8_t  reserved[16];   // 保留
};

为什么会这样设计?因为设备可能在任何时刻断电。如果只记录「已下载字节数」,断电后无法确认哪些数据是完整的。按块记录,每块都有独立的CRC,续传时只需要重新下载校验失败的块即可。

避坑指南:我曾经遇到一个问题——设备下载了99%的块,断电后重启,发现状态记录丢失了。原因是Flash写入时正好断电,数据只写了一半。后来我加了「双缓冲」机制:先写备份区,再写主区,写入完成后标记有效。这样即使写入过程中断电,也能从备份区恢复。

24.4 整体架构与流程

说了这么多,我们来画一张图,把IoT设备OTA的整体流程串起来。

IoT设备OTA升级整体流程 云端服务 差分生成 / 版本管理 传输层 HTTP / MQTT / CoAP 设备端 下载 / 校验 / 刷写 断点续传状态机 差分还原引擎 Flash刷写管理 回滚与恢复 关键机制 • 差分算法:bsdiff / HDiffPatch / xdelta3 • 分块传输:每块4KB~16KB,独立CRC校验 • 断点续传:状态记录写入Flash安全区域 • 双缓冲写入:防止断电导致状态丢失 • 回滚机制:升级失败自动切回旧版本 核心目标:在资源受限条件下,实现可靠、安全、低功耗的固件升级 省空间 · 省带宽 · 省电量 · 快下载 · 快校验 · 快切换 · 稳传输 · 稳恢复 · 稳回滚

这张图把整个流程分成了三个阶段:云端生成差分包、传输层负责可靠传输、设备端完成下载、校验、刷写和回滚。每个阶段都有对应的优化策略。

24.5 实战经验总结

最后,我把自己做IoT设备OTA的一些经验教训列出来,希望能帮你少走弯路:

  • 差分包一定要做压缩:差分包本身已经很小了,但再经过LZMA或gzip压缩,还能再小30%~50%。设备端解压时要注意内存占用,可以用流式解压。
  • 升级包签名不能省:IoT设备一旦被植入恶意固件,后果不堪设想。我见过一个案例,某厂商的智能摄像头因为没做签名校验,被黑客远程刷入了挖矿程序。所以,所有升级包必须签名,设备端必须验签
  • 升级失败要有「最后一道防线」:如果刷写过程中断电,设备变砖怎么办?我的做法是:在Bootloader中保留一个「最小恢复系统」,只有几百KB,专门用来接收紧急升级包。这样即使主固件坏了,也能通过Bootloader恢复。
  • 测试要覆盖极端场景:弱网环境、频繁断电、Flash寿命耗尽……这些场景在实验室里很难复现,但实际部署后一定会遇到。我建议在测试阶段,用「故障注入」的方式模拟各种异常情况。

一句话总结:IoT设备OTA,不是简单的「下载+刷写」,而是一个涉及差分算法、断点续传、安全校验、回滚机制的系统工程。每一个环节都要针对资源受限的特点做优化,才能做出真正可靠的升级方案。

好了,这一章的内容就到这里。差分算法怎么选、断点续传怎么实现、Flash怎么管理——这些实战经验我都毫无保留地分享给你了。下次做IoT设备OTA时,记得把这些坑都避开。

公众号:蓝海资料掘金营,微信deep3321