第1章:硬件指令集设计原则——如何定义一套高效、可扩展的美容仪指令集
做美容仪联调,说白了就是让手机APP和硬件小板子能「说上话」。
但怎么说话?说哪些话?谁先说?这就得靠一套指令集来约定。
我见过不少团队,上来就写代码,指令随便定。结果呢?联调时发现少了个功能,又得加指令,协议版本乱成一锅粥。嗯,今天我们就聊聊,怎么从一开始就把指令集设计好。
1.1 指令集的核心组成
一套完整的指令,通常包含四个部分:指令头、指令类型、参数长度、校验和。我习惯再加一个「设备地址」字段,方便以后做一拖多设备。
先看一个我常用的基础帧结构:
| 帧头(2B) | 设备地址(1B) | 指令类型(1B) | 参数长度(1B) | 参数区(NB) | 校验和(1B) |
每个字段都有讲究,我们一个个说。
1.2 指令头:怎么选?
指令头的作用很简单——告诉接收方「我要开始发数据了」。
我建议用两个固定字节,比如 0xAA 0x55 或者 0x5A 0xA5。为什么是两个?因为单字节容易误触发。我在项目中遇到过,蓝牙偶尔会丢包,如果只用一个字节做帧头,很容易把数据里的某个字节当成帧头,整个解析就乱了。
1.3 指令类型:怎么分类?
指令类型决定了这条指令是「读」还是「写」,是「查询状态」还是「执行动作」。
我一般把指令类型分成四大类:
| 类型 | 方向 | 举例 |
|---|---|---|
| 0x01 | APP → 设备 | 设置档位、开启加热 |
| 0x02 | 设备 → APP | 上报温度、电量 |
| 0x03 | APP → 设备 | 查询当前状态 |
| 0x04 | 设备 → APP | 应答查询结果 |
你想想看,如果所有指令都混在一起,APP收到一个字节,根本不知道是设备主动上报的,还是自己查询的回复。分清楚方向,解析逻辑就清晰多了。
1.4 参数长度:别省这个字节
有些工程师为了省一个字节,把参数长度固定死。比如每次只传2个字节的参数。但万一以后要传3个字节呢?协议就得改。
我建议永远保留参数长度字段。哪怕当前参数区是空的,长度填0就行。这样以后扩展功能,只需要在参数区加内容,协议结构不用动。
1.5 校验和:保命用的
蓝牙通信不像有线那么稳定。丢包、错包、乱序,我都遇到过。校验和就是最后一道防线。
最简单的做法是累加和校验:把帧头、地址、类型、长度、参数区所有字节加起来,取低8位。
// 计算校验和示例
uint8_t calc_checksum(uint8_t *data, uint8_t len) {
uint8_t sum = 0;
for (uint8_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
接收方收到数据后,用同样的算法算一遍,如果结果和收到的校验和不一致,直接丢弃整包数据。
1.6 避坑指南:我踩过的三个坑
- 坑一:指令类型用ASCII字符。 比如用'A'表示开启,'B'表示关闭。看起来直观,但大小写、空格很容易搞混。后来我全改成16进制枚举,清爽多了。
- 坑二:参数区用结构体直接打包。 不同编译器的字节对齐不一样,APP端解出来全是错的。我现在一律用逐字节拼接,虽然代码多几行,但跨平台兼容性好。
- 坑三:没有超时重传机制。 指令发出去,设备没收到,APP傻等。用户以为机器坏了。后来我在APP端加了个500ms超时重传,问题就解决了。
1.7 一个完整的指令示例
假设我们要让美容仪开启「温热模式」,档位设为3级。指令可以这样设计:
帧头: 0xAA 0x55
设备地址: 0x01
指令类型: 0x01 (APP→设备,设置指令)
参数长度: 0x02
参数区: 0x01 0x03 (0x01=温热模式, 0x03=档位3)
校验和: 0x?? (前面所有字节累加)
设备收到后,先校验,再解析。如果校验通过,执行动作,然后回复一条应答指令。
1.8 总结一下
指令集设计没有标准答案,但有几个原则是通用的:
- 帧头要稳:两个字节,交替bit模式
- 类型要清:分清楚读写方向
- 长度要留:为未来扩展留空间
- 校验要严:累加和是最低要求
下一章,我们会聊具体的蓝牙服务与特征值映射。到时候你会看到,指令集设计好了,联调工作能省一半时间。