19、蓝牙OTA固件升级:DFU(Device Firmware Update)流程、固件分包传输、校验机制、断点续传

OTA升级,说白了就是让设备能“空中换脑”。

我最早接触DFU是在一个智能穿戴项目上。当时设备已经量产了,结果发现蓝牙连接有个偶发断连的bug。你想想看,总不能把几千台设备全召回吧?OTA升级就成了唯一的救命稻草。从那以后,我对DFU的每个细节都格外上心。

19.1 DFU的核心流程

一个完整的DFU流程,我习惯把它拆成四个阶段:

  1. 准备阶段:手机端获取固件包,设备端进入DFU模式
  2. 传输阶段:将固件分包,通过蓝牙逐包发送
  3. 校验阶段:接收完成后,设备对固件做完整性校验
  4. 激活阶段:校验通过,设备重启并加载新固件

嗯,这里要注意——设备进入DFU模式的方式有很多种。有的设备是上电时检测按键,有的则是通过蓝牙指令触发。我个人建议在应用层预留一个“静默升级”的入口,这样用户体验会好很多。

关键点:DFU模式下的设备,通常会运行一个最小化的bootloader。这个bootloader只负责接收固件和写入flash,不跑应用逻辑。这样做的好处是——即使应用固件写坏了,bootloader还在,设备就不会变砖。

19.2 固件分包传输

蓝牙一次能传的数据量有限。BLE的MTU(最大传输单元)默认只有23字节,就算协商到最大,也就512字节左右。所以固件必须分包。

我在项目中遇到过一个问题:分包太小,传输效率低;分包太大,又容易丢包重传。后来我总结了一套经验:

  • 包大小:建议用MTU-3字节(去掉ATT头)。比如MTU=247,那每包传244字节数据
  • 包序号:每个包带一个2字节的序号,从0开始递增
  • 总包数:在第一个包中告知设备,方便设备预分配内存

分包格式大概长这样:

// 固件包结构
typedef struct {
    uint16_t seq;       // 包序号
    uint16_t total;     // 总包数
    uint8_t  flags;     // 标志位(首包、尾包等)
    uint8_t  data[];    // 固件数据
} __attribute__((packed)) dfu_packet_t;

小技巧:首包可以额外携带固件的版本号、CRC校验值、固件大小等信息。设备收到首包后,可以先做版本比对——如果版本相同或更低,直接拒绝升级,省得浪费流量。

19.3 校验机制

固件传错了怎么办?校验机制就是用来回答这个问题的。

我常用的校验方式有两种:

校验方式 特点 适用场景
CRC32 速度快,碰撞概率低 每包校验,实时性要求高
SHA256 安全性高,不可逆 整包校验,安全性要求高

我的做法是“双重校验”:

  • 每包校验:每个数据包尾部带4字节CRC32。设备收到后立即计算比对,不对就要求重传
  • 整包校验:所有包传完后,设备对完整的固件做一次SHA256,跟手机端发过来的摘要比对

你可能会问:为什么不用SHA256做每包校验?因为SHA256计算量太大,在低功耗蓝牙芯片上跑,一包算下来可能几十毫秒,严重影响传输速度。CRC32就快得多,硬件上甚至一个时钟周期就能算完。

注意:我曾经遇到过一个坑——设备端的CRC32算法跟手机端的不一致。原因是两边的多项式初始值不同。后来我统一用0xFFFFFFFF作为初始值,并且结果取反,这才对齐。所以,校验算法一定要在文档里写清楚参数,别想当然。

19.4 断点续传

蓝牙传输过程中,断连是家常便饭。用户走远了、手机没电了、信号被遮挡了……原因五花八门。如果没有断点续传,每次断连都要从头传,那用户体验就太糟糕了。

断点续传的核心思路就一句话:记录进度,断点重连后从断点处继续传

具体实现上,我习惯这样做:

  1. 设备端记录已接收的包序号:每收到一个包,就把序号写入flash的一个固定区域
  2. 手机端查询进度:重连后,手机发一个“查询进度”指令
  3. 设备返回已接收的最大连续包序号:注意是“连续”的,中间有空洞的话,要从空洞处开始补
  4. 手机从断点处继续发送:跳过已确认的包,只发未收到的

这里有个细节——为什么是“连续”的?因为蓝牙传输是乱序的,设备可能先收到第10包,后收到第5包。如果设备只记录“已收到哪些包”,那手机端要维护一个位图,逻辑会复杂很多。我建议只记录“连续收到的最大序号”,这样实现简单,代价是可能多传几个包,但换来的是代码的可靠性。

经验之谈:flash写入次数有限,频繁写会磨损。我的做法是每收到10个包才写一次进度,而不是每包都写。这样既保证了断点续传的粒度,又延长了flash寿命。

19.5 整体流程一览

说了这么多,咱们用一张图把整个DFU流程串起来:

DFU固件升级整体流程 1. 准备阶段 手机获取固件包 2. 传输阶段 分包发送 + CRC校验 3. 校验阶段 SHA256整包校验 4. 激活 断点续传(可选) 记录进度 → 重连后继续 分包格式 序号(2B) + 总包数(2B) 标志位(1B) + 数据(NB) 双重校验 每包:CRC32(4B) 整包:SHA256(32B) 注:断点续传在传输阶段发生断连时触发,重连后从断点处继续传输 设备端(BLE Peripheral) 接收分包 → 写入flash → 校验 → 重启 手机端(BLE Central) 读取固件 → 分包 → 发送 → 校验摘要 BLE连接

这张图把整个流程分成了四个阶段,外加断点续传这个“保底机制”。你仔细看会发现,传输阶段和校验阶段是紧密耦合的——每包校验保证了传输的可靠性,整包校验保证了固件的完整性。两者缺一不可。

我的建议:如果你刚开始做DFU功能,不要一上来就搞断点续传。先把基础流程跑通——准备、传输、校验、激活。等这个链路稳定了,再考虑加断点续传。步子迈大了,容易扯着蛋。

好了,DFU的核心内容就这些。说白了,OTA升级就是一场“空中接力”——手机把固件一包一包扔过去,设备一包一包接住,最后检查一下有没有漏的、错的,没问题就重启换脑。整个过程看似简单,但每个环节都有坑。我踩过的那些坑,上面基本都提到了,希望能帮你少走弯路。


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