1. 项目概述:为什么 WiFi OTA 需要断点续传
前一阵在做一款智能家居网关的固件升级功能,设备本身没有 USB 口、没有调试串口外接,升级只能走无线。最初版本很简单,WiFi 连上服务器,把整个固件包拉下来,写到 Flash,重启。听起来挺顺,实际一跑全是问题——屋里 WiFi 信号满格,但路由器承载了十几台设备,传输过程动不动就卡住,一次 1MB 的固件包能失败三四回,而且每次失败都要从头再下。后来我花了一周时间把整条链路重构成“断点续传”,升级成功率从 60% 左右直接拉到 99.7%,这里把这套方案的完整思路和技术细节整理出来。
WiFi OTA 断点续传,简单说就是通过 Wi-Fi 网络完成设备固件的空中升级,并且当传输中断时,已下载的部分不会白费,设备会记住下载到了哪里,下次连接时直接从断点继续传输。这个功能解决的核心问题有三个:一是 WiFi 信号在家庭、工厂等真实环境里并不稳定,干扰和丢包很常见;二是物联网设备资源受限,重启一次下载进度就清零,用户体验极差;三是设备往往部署在难以物理接触的位置,升级失败轻则返厂,重则直接变砖。适合正在做智能家居、传感器节点、无人机、边缘网关等项目,需要提升 OTA 可靠性的开发者参考。
这套方案的难点不在于“下载文件”本身,而在于如何设计一套在掉电、断网、服务器切换、Flash 磨损等极端情况下仍然可靠的状态机。全文我会按照“整体设计思路 → 关键技术点 → 核心代码 → 问题排查”四个维度展开,所有内容都来自真实调试经历,不是贴文档式翻译。
2. 整体架构与核心技术选型
2.1 断点续传的整体工作流程
断点续传本质上就是“记录 + 校验 + 续传”三个动作不断循环。设备端发起升级请求时,先从服务器读取一个状态接口,拿到当前固件的版本号、文件大小、分块总数、每块的校验值,然后在本地 Flash 中找上次下载的记录。如果存在有效的下载记录,并且记录里的固件版本和服务器一致,就从最后一个完成写入的块开始继续;如果记录不存在或版本不一致,则清空旧数据,从第 0 块开始。
这里有个容易被忽略的点:记录本身也必须存放在掉电不丢失的介质上,同时要保证“记录”和“数据块”的一致性。我见过不少方案把进度存到 RAM 里,一旦掉电就丢失,这等于白做。还有的方案把进度写在数据区附近,结果进度写坏了,数据区也被连带污染,实际效果比不做断点续传还差。我的做法是:画出一个独立的 OTA 信息区,专门存放下载状态和元数据,和数据区分开,擦写次数也错开。
在此基础上,传输层我建议优先考虑 HTTP,而不是 MQTT 或私有 TCP 协议。原因很直观:物联网后端基础设施里 HTTP 的文件服务最成熟,Nginx、MinIO、阿里云 OSS 原生支持 Range 请求,固件包上传分发链路直接用现成方案,不需要自己造协议。MQTT 适合小报文遥测,固件包动辄几百 KB,连续传输大文件时要分帧、重组且没有标准断点续传语义,工程复杂度反而更高。
2.2 为什么选择 HTTP Range 而不是自定义协议
断点续传的底层能力,HTTP/1.1 标准里早就写好了,就是 Range 请求头。客户端发起GET /firmware.bin时带上Range: bytes=1024-,服务器就会从第 1024 字节开始返回后续内容,并附带206 Partial Content状态码。有了这个机制,设备端不需要知道文件总大小,只需要记录“我已经拿到多少字节”,然后让服务器接着给就行。
在实际项目里,我更喜欢两级粒度同时做:HTTP 层面用 Range 控制“从哪里继续”,业务层面把固件包切成固定大小的块,比如 4KB 或 8KB,每一块有独立的序号和 CRC 校验。这样做的好处有三点。第一,如果某一块在传输中损坏,不需要重新下载整块数据,只需要记录这一块的状态,重连后单独补这一块。第二,Flash 擦写是按扇区进行的,块大小对齐扇区后可以大幅减少擦写次数,延长 Flash 寿命。第三,元数据可以做得非常紧凑,每块只占 2 个字节记载状态,几百 KB 的固件也只需几十个字节的状态位图,放在独立 OTA 信息区绰绰有余。
服务端是否支持 Range 请求,其实很容易验证。用 curl 发个测试请求就行:
curl -I -H "Range: bytes=100-" http://your-server/firmware.bin如果响应头里看到HTTP/1.1 206 Partial Content和Content-Range: bytes 100-xxx/xxx,说明服务器支持断点续传。如果返回的是200 OK和完整的Content-Length,说明服务器忽略了 Range 头,这时候要么换个服务端组件,要么在业务层自行实现分块请求,不能依赖 HTTP 层了。
2.3 服务端存储和分发的选型参考
做这套方案时,服务端我只放了两个接口:一个是元数据查询接口,返回固件版本、文件大小、分块大小、各块 MD5;另一个是静态文件下载接口。元数据用 JSON 返回,格式大致是这样:
{ "version": "1.2.3", "file_size": 524288, "block_size": 4096, "block_count": 128, "blocks_md5": [ "a1b2c3d4e5f6...", "9f8e7d6c5b4a..." ], "force_erase": false }固件包本身放在对象存储里,MinIO、OSS 或者简单的 Nginx static 目录都行。MinIO 默认支持 Range 请求,这一点在官方文档里也有说明,所以“MinIO 支持断点续传吗”这个问题的答案其实是分成两层的:MinIO 作为服务端存储天然支持 HTTP Range,苹果系统浏览器或下载器里看到的是“服务端断点续传能力”;而如果你说的是客户端 SDK 层面的“断点续传”,那属于业务层逻辑,MinIO 本身不管,需要你的设备端自己实现。
有一点我特别想提醒:开发环境里常常用本地文件目录模拟对象存储,但应尽量避免直接用 Spring Boot 内置的静态资源处理大文件,因为默认实现会在请求过程中加载过多的上下文信息,嵌入式 WiFi 设备环境下的异常中断会让连接回收变得不可控,实际测试中失败率会明显上升。生产环境请务必在 Nginx 层把大文件下载单独配置,或者直接走对象存储。
3. 设备端断点续传的详细实现
3.1 下载任务状态机设计
整个下载过程我把它抽象成一个状态机,状态切换的清晰程度直接决定了后续出问题的排查难度。核心状态有四个:空闲、下载中、续传中、已完成。每个状态对应一组允许的事件和动作,我用一个枚举来定义:
typedef enum { OTA_IDLE = 0, OTA_DOWNLOADING, OTA_RESUMING, OTA_COMPLETED } ota_state_t;“空闲”状态下,设备收到升级指令后,会先做版本比对和目标固件大小检查,通过后进入“下载中”。需要特别注意的是,从“下载中”不能直接进入“已完成”,必须先经过“续传中”或者重传缺失块。这是因为 WiFi 环境下传输中断太常见了,如果直接认为“所有块都写成功”就切到完成态,很可能漏掉还没校验的尾巴。
每次收到数据块并成功写入 Flash 后,程序都会更新 OTA 信息区中的状态位图。位图中的每个 bit 对应一个块,置 1 表示该块已在 Flash 中校验通过,置 0 表示未下载或校验失败。位图本身就充当了断点续传的“记忆”,下次启动时只需扫描位图,找到第一个不为 1 的 bit,就知道该从哪个块开始继续。
状态机里特别容易出问题的是超时处理。WiFi 连接缓慢、TCP 半开连接、服务器响应延迟超过设备看门狗超时时间,这些情况都会导致状态机误判。我的经验是:网络操作的超时设置一定比看门狗短,并且任何“重连后回退”的动作都要有一个退避策略,比如第 1 次失败后等 5 秒重试,第 2 次等 10 秒,最多等 60 秒,避免设备反复重连导致网络瘫痪。
3.2 版本号与下载记录的一致性保障
很多断点续传方案做到中途就出问题,根源在于版本号管理混乱。设备端不仅要记录“当前正在下载哪个版本”,还要记录“上次启动时运行的是哪个版本”。这两个版本必须分开存,因为设备从 OTA 模式回落到旧版本时,如果固件版本号覆盖了旧版本号,整个回滚判定就不成立了。
我在 OTA 信息区设计了这样一组结构体:
typedef struct { uint32_t magic; uint32_t version_downloading; uint32_t version_running; uint32_t block_size; uint32_t total_size; uint32_t downloaded_size; uint8_t bitmap[32]; uint32_t crc32; } ota_info_t;magic字段用来判断这组记录是否有效,建议用固定的魔数比如0xAF55AA01。每次更新记录后,程序会重新计算整个结构体的 CRC32 并写到尾部,读取时先校验 CRC,CRC 不对就认为记录无效,清空重建。这个设计主要是防止写入过程中掉电导致记录处于半个状态。
版本一致性检查的逻辑是:启动 OTA 时,如果magic无效、或version_downloading不等于服务器下发的新版本号、或block_size与服务器配置不一致,一律清空信息区,重新开始全量下载。只有当所有字段都匹配,才读取位图进入续传逻辑。这个判断原则宁可保守也不能激进,因为一旦错误续传了不同版本的固件块拼凑文件,校验时一定失败,而且很难排查。
3.3 Flash 分区规划与双备份设计
Flash 空间的规划决定了一个 OTA 方案的上限。我的建议是至少划分四个区域:Bootloader、App A(当前运行)、App B(备用)、OTA 信息区。App A 和 App B 是双备份关系,每次升级写入到不活跃的那个区域,升级完成后通过 Bootloader 切换启动入口,这样即使新固件起不来,Bootloader 也能回滚到旧版本。
具体分区表举例,假设 Flash 总共 2MB:
- Bootloader:64KB,地址 0x000000 - 0x00FFFF
- App A:896KB,地址 0x010000 - 0x0FFFFF
- App B:896KB,地址 0x100000 - 0x1FFFFF
- OTA 信息区:32KB,地址 0x1F8000 - 0x1FFFFF
OTA 信息区虽然标了 32KB,但每次写入的数据其实只有几十字节,因为 Flash 擦写寿命有限,所以这个区域我会分成两个扇区交替使用。第一次写扇区 0,第二次写扇区 1,第三次擦掉扇区 0 再写,这样可以把擦写次数分散到两个扇区,延长寿命。对于频繁升级的设备来说,这个细节很关键。
双备份方案在实现上有一个注意点:App A 和 App B 的烧录地址必须与链接脚本保持一致,否则固件内跳转地址全都会错。比如工程里默认链接脚本在 ROM 的 0x010000 处,如果你想把固件烧到 App B 的 0x100000,就必须提供另一个链接脚本并重新编译一份固件。这是嵌入式开发里地址不对导致的诡异 bug 的最常见来源。
4. 核心代码实现:从 HTTP 拉取到 Flash 写入
4.1 HTTP 客户端与 Range 请求拼接
设备端如果是 ESP32,我直接用的 ESP-IDF 里的 esp_http_client 组件,它自带 Range 请求支持,传 header 时加上Range字段即可。核心代码结构如下:
esp_http_client_config_t config = { .url = "http://192.168.1.100:8080/firmware.bin", .method = HTTP_METHOD_GET, .timeout_ms = 10000, .buffer_size = 4096, .event_handler = http_event_handler, }; esp_http_client_handle_t client = esp_http_client_init(&config); char range_header[64]; snprintf(range_header, sizeof(range_header), "bytes=%d-", download_offset); esp_http_client_set_header(client, "Range", range_header); esp_http_client_open(client, 0);download_offset是上一次成功写入的字节数,直接从 OTA 信息区读出来。用esp_http_client_open之后,就可以通过esp_http_client_fetch_headers判断服务器返回的是不是206 Partial Content,并读取Content-Length计算本次实际接收的数据总量。
这里有一个特别容易踩的坑:服务器返回的Content-Length可能不是整个固件的长度,而是从断点位置开始到文件结尾的长度。比如固件总长 512KB,断点在第 100KB,那么Content-Length应该是 412KB。如果设备端忽略了这个细节,继续按照“总长度 - 已下载长度”去计算剩余量,就会得到错误结果。所以每次续传时都要同时读取并记录两个量:请求的 Range 起始位置,以及响应中的Content-Length。
如果你用的不是 ESP-IDF,而是 AT 指令或标准 socket,那也完全可行,只是需要自己处理 HTTP 报文解析。我建议不要手写解析完整 HTTP 头,而是用一个轻量级的 HTTP 解析库,比如 http-parser,否则分块传输、chunked 编码、keep-alive 这些细节会让你调试到崩溃。
4.2 固件块的校验与写入
数据流从网络收到后,不能直接盲写 Flash。我采用的做法是:在内存中攒满一个块(4KB 或 8KB),先累加 CRC 或计算 MD5,与元数据接口里的blocks_md5比对,一致才执行擦写。校验失败则丢弃这一块,让 TCP 层自动重传,也就是让 HTTP 的请求继续拉后续数据,但不对失败块做任何写入。
这是一个看似简单但实际效果非常好的优化。因为 WiFi 链路的误码率远高于有线网络,TCP 校验失败会重传,但应用层的 HTTP 解析并不会感知比特翻转,只有靠应用层的块校验才能兜底。实测下来,加了块校验后,OTA 升级后首次启动的失败率降了一个数量级。
写入 Flash 时最核心的要求是“擦写分离”,不要每收到一个小数据块就擦一次扇区。我的写入逻辑是这样的:
if (block_index == block_first_in_sector || (block_index > 0 && (block_index * block_size) % sector_size == 0)) { esp_partition_erase_range(partition, sector_start, sector_size); } esp_partition_write(partition, offset, block_buffer, block_size);每次block_index跨入一个新的扇区时,先擦除整个扇区,再将块数据连续写入。这样每个扇区只擦一次,而不是每块擦一次。这个优化对 Flash 寿命影响巨大,我的测试板上 Flash 擦写次数原本能到 10 万次,如果每 4KB 就擦一次 4KB 扇区,升级 1MB 固件要擦 256 次,长期高频升级很快会磨损完;而优化后同样的固件只需擦 128 次,整整省了一半寿命损耗。
4.3 断点记录更新时机与掉电安全
什么时候更新断点记录?我见过两种做法。一种是在每收到一块并成功写入后立即更新位图,另一种是每收到 N 块才更新一次。前者的优点是掉电损失最小,缺点是频繁写 Flash 会加速磨损。后者的优点是对 Flash 友好,缺点是一旦掉电会重复下载最近 N 块数据。
我最终采用的是“每成功写入一块就更新一次位图,但记录结构体只在每 16 块或扇区切换时才写一次”。为什么可以这样?因为位图存在 OTA 信息区,结构体里存的是已下载总字节数,这两个数据可以互相推导。实际掉电恢复时,即使位图显示的块数和downloaded_size有偏差,程序也会优先信任更保守的值,多下几块数据再校验,多传的数据量有限,但安全性更高。
掉电安全的另一个关键是写记录时的顺序。我必须强调:先写数据块,后写记录。如果顺序反了,记录先更新了但数据还没落盘,掉电后设备会认为那一块已经下载完成,而实际数据是旧的,最后校验失败。数据块先落盘、记录后更新,即使掉电导致记录丢失,也只会多做一次全量或增量下载,不会出现“虚假完成”的状态。
4.4 版本回滚与 Bootloader 切换策略
下载完成后,设备会写入一个“固件就绪”标记,然后重启进入 Bootloader。Bootloader 的职责有两个:校验新固件的完整性,以及在启动超时或崩溃时回滚到上一个版本。
我实现的方式是:App 启动后立即上报当前版本到中心端,正常运行时定期更新看门狗。Bootloader 维护一个启动计数器,每次启动时先读计数器值,如果 App 没有在 30 秒内“汇报成功”,计数器加 1;当计数器连续 3 次失败时,Bootloader 切换到另一分区启动,同时把计数器清零。这个机制简单但够用,避免了设备升级后反复重启的尴尬。
另外千万不要把“版本回滚”做成“重新拷贝”操作。双备份分区模式下,回滚只需要修改启动指针,指向旧版本即可,千万不要做批量拷贝。拷贝大文件的时间足够看门狗咬合,还可能出现拷贝到一半停电导致双分区全部损坏,那就只能返厂了。
5. 实操过程中的性能优化与参数调优
5.1 传输块大小与并发窗口的权衡
传输块大小的选择直接影响着下载速度和 Flash 磨损,但很多人不重视。我测试了几组数值:块大小 512 字节时,校验频繁,每块都要算一次 CRC,CPU 占用率高,速度只有 10KB/s;块大小 16KB 时,整块校验的数据缓冲较大,如果解码到一半出错就要重新请求,WiFi 恢复后重新传输的损失也高。最终在 4KB 到 8KB 这个区间取得了收益平衡,既能提升传输效率,又不会因为块太大导致单次校验失败需要重新拉取的数据量过大。
如果你用 HTTP,还可以开启 Keep-Alive 来减少 TLS/HTTP 握手带来的延迟开销。每次断点续传重新发起请求时,如果 TCP 连接还保活着,就可以省掉一次完整握手时间。我在 ESP32 上测试,开启 Keep-Alive 后,续传时整体耗时大约降低 20%,效果相当可观。
另一个容易被忽略的参数是 TCP 接收窗口。ESP32 的 lwIP 默认接收缓冲区可能只有 4KB,如果服务器下发速率超过这个值,数据会被内核丢包重传,吞吐量上不去。我把CONFIG_LWIP_TCP_WND调到 32KB,CONFIG_LWIP_TCP_RECVMBOX_SIZE调到 16,实测下载速度从 60KB/s 提升到了 180KB/s,提升非常明显。
5.2 失败重试策略与退避算法
WiFi 环境下,一次完整的固件下载过程中出现多次中断是常态。所以重试策略必须设计得足够有弹性,不能一失败就无脑重连、无脑重新请求。我整理出一套分级重试策略,简单说明如下:
- 第 1 次中断:立即重试,但是从头重发当前块的 HTTP 请求,不重新建立 TCP。
- 第 2~4 次中断:每次等待 2 秒、4 秒、8 秒递增退避,重新建立 TCP 和 HTTP 请求,但 Range 继续从断点开始。
- 第 5 次以上中断:等待 30 秒,主动断开 WiFi,重新扫描并连接网络后再继续。
这套策略在实际部署中效果很好。当 WiFi 信号不佳时,前 4 次重试能解决大部分瞬时干扰;信号长期不稳定时,断网重连往往能找到一个更好的 AP 或者信道,成功率明显提升。
退避算法的实现有个细节:重连后不要急于立刻发送 HTTP Range 请求,先做一次网络连通性探测,比如 ping 一下服务器,或读取一个极小的状态接口。因为 TCP 重连成功后,网络栈可能还在做 DHCP 或 ARP 解析,立刻发大请求很容易再次超时。等待 500ms 再发请求,成功率会高很多。
5.3 下载完成后的固件整体校验
逐块校验通过,并不代表整体固件没有问题。因为固件内部也可能有分段校验,甚至在链接时加入了某种填充。OTA 完成后,设备端还需要对整个固件文件再计算一次 SHA-256,与服务器下发的全局哈希比对,一致后才会真正写入启动标志。这样即使某块搬运时有错位、调整、填充等小概率事件发生,最终也能被检查出来。
双重校验的代价是会多花一点时间。1MB 固件计算 SHA-256,在 ESP32 上大约耗时 1~2 秒,这个成本可以接受。但如果你的设备主频很低,比如只有几十 MHz 的 MCU,可以考虑用 CRC32 做全局校验,虽然碰撞概率比 SHA-256 高一些,但对于消费类产品已经够用。
全局校验完成后,把固件就绪标志写到信息区,执行系统复位。注意:复位前要调用 WiFi 断连和 flash 缓存刷出的 API,否则复位瞬间 WiFi 模块可能还有未处理的 DMA 数据,写入 Flash 的最后一部分可能没落盘。
6. 常见问题与排查技巧实录
6.1 续传后写入错位导致校验失败
现象:断点续传后,下载进度显示正常,但最终整体校验失败,而且失败位置总是在断点处附近。
排查思路:先看download_offset是不是正确写入到了 OTA 信息区,再看服务器返回的Content-Range起始位置是否等于download_offset。我遇到过一种情况:客户端用Range: bytes=4096-请求,但服务器实现错误,返回了从 0 开始的数据,导致新下载的数据覆盖了旧数据,破坏了固件完整性。这种情况在 Nginx 上很少见,但自建 HTTP 服务时经常出现,尤其当服务端把 Range 头解析成了start=0。
解决办法有两个层面。第一层是协议层,客户端必须检查 HTTP 响应状态码是否为 206,以及Content-Range头里的起始字节是否等于请求的 Range 起始值,不相等直接终止并报错。第二层是业务层,每一块下载后做 MD5 比对,如果收到的是从错误位置开始的数据,块校验会直接失败,不会误写入 Flash。
6.2 WiFi 休眠导致下载中断
现象:下载进行到一半时,连接断开,而且重连后继续下载也没有恢复速度,反而反复超时。
排查原因:很多 WiFi 模组默认开启了省电模式,在低功耗策略下,WiFi 模块会在无数据活动时进入 sleep,但这个休眠周期和 HTTP 下载的长时间空闲期重叠后,部分模组状态未能正确唤醒,导致 TCP 连接被重置。
解决方法是下载期间主动关闭省电:
esp_wifi_set_ps(WIFI_PS_NONE);下载完成后恢复:
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);这不是唯一的坑。还有一个相关问题是 DHCP 租约到期。如果下载耗时很久且租期内没有续租成功,网络会断掉。解决方式是在 OTA 期间主动禁用租约刷新,或者确保网关的租约时间足够长。我一般把网关租约设为 12 小时,完全覆盖一次升级窗口。
6.3 Flash 写入失败或擦除失败
现象:下载过程中 Flash 写入返回错误,查看日志发现esp_partition_write返回ESP_ERR_FLASH_OP_FAIL,而且复现概率不稳定。
原因通常有两个。一个是软件原因:在写入 Flash 期间,系统触发了中断,可能是在 flash 操作期间访问了 flash 里的常量数据,这在 MMU 缓存一致性较弱的芯片上非常致命。另一个是硬件原因:Flash 供电电压偏低或温度过高。
这方面我的建议是:下载固件时尽量关掉不必要的系统日志输出,尤其是带时间戳的日志机制,它在写 Flash 时会打断大块擦写操作。另外,不要在 Flash 写过程中去读取调试输出缓冲,优先把日志发送到串口之外的通道,避免在 Flash 写期间触发对 flash 内容的读取。
6.4 断点续传记录丢失导致重新全量下载
现象:升级已经开始,中途断电,重新上电后发现进度归零,设备又从头开始下载。
我最开始也遇到这个现象,排查后发现问题在“记录擦除时机”。有些代码在写入固件前会把 OTA 信息区整个擦除,或者在每次启动 OTA 时不清除旧记录而是直接读取,但如果上次下载过程中信息区写的记录因为断电损坏,读取时 CRC 校验失败,程序就默认全量重来。
这里可以通过一个“双记录区 + 序列号”方案解决。我在 OTA 信息区存两个结构体,每次写入时带上递增序列号,读取时优先选择序列号大且 CRC 有效的那份。如果只有一份有效,就用有效的那份;如果两份都无效,才做全量下载。这样单次掉电把两份记录都写坏的概率极低,整体可靠性大幅提升。
6.5 常见问题速查表
我把自己踩过以及帮朋友排查的典型问题整理成一张表,可以快速对照定位:
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 续传后固件整体校验失败 | 断点记录与服务器 Range 不一致 | 检查 206 和 Content-Range 响应头 | 校验 Range 起始字节等于 offset |
| 下载中途 TCP 断开 | WiFi 省电模式进入休眠 | 确认 esp_wifi_get_ps 返回值 | 下载期间设置 WIFI_PS_NONE |
| 进度一直在 0% | 信息区记录 CRC 校验失败 | 查看日志中 OTA info 的错误码 | 增加双记录区 + 序列号机制 |
| 下载速度慢到不可用 | TCP 接收窗口过小 | 抓包看窗口字段 | 调大 LWIP TCP_WND 和 mailbox |
| 下载完启动死机 | 全局校验未做或跳过 | 看 Bootloader 启动日志 | 增加 SHA-256 整体校验 |
| 写 Flash 报 ESP_ERR_FLASH_OP_FAIL | 写期间日志输出触发 flash 读 | 确认日志输出通道 | 关闭日志或改到 DMA 通道 |
| 多次升级后 Flash 损坏 | 擦写次数太多 | 统计擦除次数 | 扇区切换 + 连续块写入策略 |
这张表基本覆盖了我在实际项目中遇到的 90% 问题。剩下 10% 大多和具体芯片、具体服务器配置有关,处理思路也差不多:先做分层定位,网络层看 TCP 连接和重传,应用层看 HTTP 状态码和块校验,存储层看 Flash API 返回值,没有一次快速定位不到的。
7. 最后分享一个实用技巧
说实话,WiFi OTA 断点续传这个功能最大的价值,不是省了那几次重新下载的时间,而是让远程升级这件事变得“敢做”。过去设备部署之后,我每次远程升级都提心吊胆,生怕升级失败又联系不上现场;有了断点续传、双备份、回滚机制之后,我可以放心地在半夜自动推送固件,第二天早上起来看统计报表就行。
如果上面任意一个问题你之前踩过坑,应该能明白这套方案里每个设计都不是多余的。尤其是“块校验 + 断点记录分离 + 双备份”这一套组合,几乎是目前资源受限设备上最稳妥的 OTA 底座。后续如果你打算做多设备批量升级,还可以在服务器端加一个升级任务管理系统,把这里的设备端状态上报和任务调度对接起来,顺理成章。