4、通信协议层设计:BLE GATT服务设计、自定义OTA服务UUID、MTU与分包策略
好,咱们进入通信协议层。这一层说白了,就是耳机和手机之间怎么“说话”。你想想看,OTA升级说白了就是往耳机里灌数据。数据怎么灌?灌的时候怎么保证不出错?这就是我们要解决的问题。
我个人习惯,先把GATT服务搭起来。就像盖房子先搭框架一样。GATT是BLE协议栈里最常用的通信模型。它定义了“服务”和“特征值”的概念。服务是一组功能的集合,特征值就是具体的数据通道。
4.1 自定义OTA服务UUID
标准服务有标准UUID,比如电池服务是0x180F。但OTA升级是咱们自己的事,得用自定义UUID。我建议用128位的UUID,格式像这样:
// OTA服务UUID
#define OTA_SERVICE_UUID "0000FFE0-0000-1000-8000-00805F9B34FB"
// OTA控制特征值
#define OTA_CONTROL_CHAR_UUID "0000FFE1-0000-1000-8000-00805F9B34FB"
// OTA数据特征值
#define OTA_DATA_CHAR_UUID "0000FFE2-0000-1000-8000-00805F9B34FB"
这里我解释一下为什么这么设计。三个特征值就够了:
- 控制特征值:用来发命令,比如“开始升级”、“暂停”、“查询版本”。
- 数据特征值:用来传固件包。这是最忙的通道。
- 状态特征值:耳机往手机上报状态,比如“收到第100包了”、“校验失败”。
我在项目中遇到过一个问题:有些手机对UUID的大小写敏感。你写的是小写,手机端解析成大写了,就匹配不上。嗯,这里要注意,统一用小写,两边保持一致。
4.2 MTU与分包策略
MTU,全称是Maximum Transmission Unit。说白了就是一次能传多少字节。BLE 4.0默认是23字节,去掉3字节的协议头,实际有效载荷只有20字节。
20字节?你想想看,一个固件包动辄几百KB。20字节一包,那得传到猴年马月?
所以我们要做两件事:
- 协商MTU:让手机和耳机都支持更大的MTU。BLE 4.2以后,MTU可以扩展到247字节。我建议直接协商到最大。
- 分包策略:把固件切成合适大小的包,一包一包发。
MTU协商流程:
- 手机发起MTU请求,比如请求247字节。
- 耳机回复同意,或者回复自己能支持的最大值。
- 双方取最小值,作为实际MTU。
我曾经踩过一个坑:耳机端MTU协商成功了,但手机端没处理好,实际还是用20字节在发。结果耳机收包收得乱七八糟。后来我加了一个握手确认的步骤,手机发第一包数据前,先发一个“MTU确认包”,耳机收到后回复“OK”,这才稳定下来。
4.3 分包策略详解
分包策略,说白了就是怎么切。切大了,丢包重传成本高。切小了,效率低。我一般这么切:
| MTU大小 | 有效载荷 | 建议分包大小 | 说明 |
|---|---|---|---|
| 23字节 | 20字节 | 16字节 | 留4字节做包头 |
| 247字节 | 244字节 | 240字节 | 留4字节做包头 |
你看,我习惯留4个字节做包头。包头里放什么?
// 数据包格式
typedef struct {
uint16_t seq; // 包序号,2字节
uint8_t flag; // 标志位:0x00正常包,0x01最后一包
uint8_t len; // 本包有效数据长度
uint8_t data[0]; // 实际数据
} ota_packet_t;
这个结构很轻量。seq用来排序和去重。flag告诉接收方“这是最后一包了”。len是实际数据长度,因为最后一包可能不满。
小技巧:我建议在包头里加一个CRC校验字段。虽然BLE底层有CRC,但那是链路层的。应用层再加一层校验,更保险。我一般用CRC16,只校验数据部分,不校验包头。这样接收方收到后先算CRC,不对就直接丢,不用等全部收完再校验。
4.4 流控与重传机制
数据发得快,耳机收不过来怎么办?这就是流控要解决的问题。我常用的策略是滑动窗口:
- 手机一次发N包,不用等回复。
- 耳机每收到一包,回复一个ACK。
- 手机收到ACK后,窗口往前滑动,继续发。
- 如果超时没收到ACK,重传那包。
N取多少合适?我建议取4到8。太小了效率低,太大了耳机内存扛不住。我在一个项目里试过N=16,结果耳机内存爆了,直接死机。后来改成N=4,稳得很。
注意:重传次数要有限制。我一般设3次。3次都失败,就认为链路断了,直接报错退出升级。别死循环重传,那会把耳机卡死。
4.5 实际项目中的经验总结
嗯,说了这么多,我总结几条实战经验:
- MTU协商一定要做。别偷懒用默认值。20字节一包,升级一个1MB的固件,要发5万多次。用240字节一包,只要发4000多次。效率差了一个数量级。
- 包头设计要精简。每多一个字节,就少传一个字节的数据。我见过有人把包头设计成16字节,那真是浪费。
- ACK不要丢。耳机回复ACK时,如果手机没收到,会重传。但耳机已经收过了,再收到重复包怎么办?我建议耳机维护一个已收包序号位图,收到重复包直接回复ACK,不处理数据。
- 超时时间要合理。BLE的通信延迟比WiFi大。我一般设500ms到1s。太短了容易误判丢包,太长了升级慢。
最后说一句,通信协议层是OTA升级的“血管”。血管堵了,再好的升级逻辑也白搭。所以这块一定要稳,要经得起各种手机、各种环境的考验。我在测试时,会故意把手机放远,或者中间隔一堵墙,看看重传机制能不能扛住。嗯,只有扛住这些极端情况,才能放心发布。