控制器与App通讯协议怎么制定
给嵌入式工程师的设备与 App 通讯协议设计清单:帧格式怎么定、字节序和校验怎么选、命令 / 应答 / 主动上报怎么区分、序号匹配与超时重试、错误码、版本兼容和参数表,附帧示例和 CRC16 代码。

本文目录
很多工厂的控制板做了很多年,串口协议也有一套,但那套协议通常是给上位机或者自家调试工具用的。现在要接手机 App,就会发现原来的协议不太够用:没有序号,应答对不上请求;没有版本号,固件一升级 App 就解析错;错误只回一个"失败",App 不知道该提示用户什么。
这篇文章是写给嵌入式工程师的。我们做过电动工具、灯具控制器、冷库控制器、工业控制器、BMS 等项目的 App 端,下面把和固件工程师定协议时反复确认的事项整理成一份清单。如果你还在规划整体分工,可以先看 传统设备智能化升级。
先分层,再定细节
协议吵不清楚,多半是因为几层东西混在一起讨论。建议先分成四层:
| 层 | 管什么 | 例子 |
|---|---|---|
| 链路层 | 数据走什么通道 | BLE 特征值、串口、4G 模组透传、MQTT 主题 |
| 帧层 | 一包数据怎么切、怎么校验 | 帧头、长度、校验、帧尾 |
| 命令层 | 这包数据要干什么 | 命令字、序号、应答、错误码 |
| 业务层 | 数据代表什么 | 温度、转速、运行模式、故障位 |
链路层的细节按通道单独确认,蓝牙的特征值规划和 MTU 分包可以看 蓝牙设备GATT服务和通讯协议怎么设计。帧层和命令层最好做成和链路无关的。这样同一套协议,蓝牙能用,4G 模组透传也能用,App 和云端可以共用一份解析代码。我们在电动工具项目里就是这样:手机 App 和电脑端走的都是同一种帧格式。
帧格式:一个够用的模板
下面是我们常推荐的结构,大多数控制器项目直接套用就行:
| 帧头 2B | 长度 2B | 版本 1B | 命令字 1B | 序号 1B | 数据 N B | CRC16 2B |
AA 55 从版本到数据 协议版本 功能码 0-255 业务数据 覆盖版本到数据举个例子,App 读取设备运行状态:
请求:AA 55 00 04 01 10 2A 00 [CRC16]
帧头 长度=4 版本1 读状态 序号42 无参数
应答:AA 55 00 0A 01 90 2A 00 01 0F A0 00 32 00 [CRC16]
命令字 0x90 = 0x10 | 0x80,表示读状态的应答;序号同样是 42几个细节要在文档里写死:
- 长度字段覆盖哪些字节:从版本到数据,还是包含校验。写错一个,两边就切不出完整帧。
- 字节序:多字节数值用大端还是小端,全协议统一一种。Modbus 是大端,很多 MCU 内存是小端,最容易混。
- 校验算法和覆盖范围:CRC16 用哪个多项式、初始值是多少,从哪个字节算到哪个字节。
为什么一定要有长度和校验
蓝牙通知、串口、4G 透传都会遇到粘包和半包:一次收到一帧半,或者半帧。只靠帧头找边界不可靠,因为数据里也可能出现 AA 55。正确的切帧方式是:找到帧头,读长度,等收够长度再校验,校验不通过就丢弃这一帧并往后找下一个帧头。
校验用异或最省事,但检错能力弱;条件允许的话用 CRC16。下面是常用的 CRC16/MODBUS 实现,固件和 App 用同一个算法:
uint16_t crc16_modbus(const uint8_t *buf, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= buf[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc; // 低字节在前发送还是高字节在前,也要写进文档
}三类报文:请求、应答、主动上报
App 和设备之间的报文,基本就三类:
- 请求:App 发给设备,读参数、写参数、执行动作
- 应答:设备对请求的回复,带结果和数据
- 主动上报:设备自己发的,状态变化、告警、周期数据
区分方式常见两种:应答命令字 = 请求命令字 | 0x80;或者单独用一个字段标明报文类型。主动上报要有自己的命令字,App 收到后不需要回应答(除非是告警这类必须确认的消息)。
序号:应答对得上,重发不重复执行
没有序号的协议,App 连续发两条指令,收到一个应答,不知道是哪条的。有了序号,应答带回请求的序号,App 就能准确匹配。
序号还有一个作用是去重。App 超时没收到应答会重发,如果设备其实已经执行过了,再执行一次可能出问题,比如"电机正转 10 圈"执行两遍。设备记住最近处理过的序号,收到重复序号直接回上次的结果,不再执行。
超时和重试要双方一起定
每条命令要约定:
- 设备最长多久必须应答(比如 500ms;写 Flash 的命令可以长一些)
- App 超时后重试几次、间隔多久
- 重试都失败后,App 怎么提示用户
4G 链路的时延比蓝牙大得多,远程指令的超时要单独设置。具体可以看 4G控制器通讯协议怎么设计。
写入之后要回读
App 写了一个参数,设备回"成功",界面就显示新值,这样做不够稳妥。设备可能因为超出范围、当前运行状态不允许等原因没有真正生效。我们的做法是写入应答里带回设备当前的实际值,或者写完立刻再读一次,界面以设备回读的值为准。控制器 App 的参数表和回读设计可以参考 控制器App开发怎么规划。
错误码要能变成用户看得懂的话
"失败"对用户没有意义。错误码至少要区分:
| 错误码 | 含义 | App 提示 |
|---|---|---|
| 0x00 | 成功 | — |
| 0x01 | 命令不支持 | 当前固件不支持此功能,请升级 |
| 0x02 | 参数超出范围 | 设定值超出允许范围 |
| 0x03 | 当前状态不允许 | 请先停机再修改此参数 |
| 0x04 | 校验失败 | 通讯异常,请重试 |
| 0x05 | 设备忙 | 设备正在执行其他操作 |
"当前状态不允许"这类错误对控制器特别重要,比如运行中不允许改电机极对数。
版本号:固件和 App 不会同时升级
设备卖出去以后,市场上会同时存在好几个固件版本,而用户手机上的 App 会自动更新到最新版。所以:
- 帧里带协议版本号,App 按版本解析
- 连接后先读一次固件版本和设备能力,App 按能力显示功能
- 新增字段加在数据末尾,旧版 App 忽略多出来的字节就能兼容
- 不要改已有字段的含义和位置
参数表驱动
控制器动辄几十上百个参数。如果每个参数都在 App 里写死,固件加一个参数 App 就要发版。更好的做法是用一张参数表描述:地址、名称、类型、单位、缩放系数、取值范围、读写权限。App 按表生成界面,新增参数只改表。
协议文档写什么
最后,所有这些要落到一份协议文档里,作为开发和验收的唯一依据。文档模板可以参考 智能硬件接入APP的完整流程 里的"协议文档模板"一节。我们和客户合作时,通常先给出草案,固件工程师逐条确认能不能实现,再定稿。
常见问题
原来的上位机协议能直接给 App 用吗?
可以先评估。如果已经有长度、校验和明确的应答,往往加个序号和版本号就能用。实在改不动,也可以由 App 适配旧协议,只是 App 侧的处理会复杂一些。
用 JSON 还是二进制?
MCU 资源紧张、走蓝牙或串口透传,用二进制;设备跑 MQTT、资源充足,用 JSON 调试更方便。也可以设备侧用二进制,云端转成 JSON 给 App。
Modbus 设备还需要另外定协议吗?
不一定。已有 Modbus 寄存器表的设备,可以直接按点表读写,App 和云端按寄存器解析。参考 Modbus App开发方案。
你们能帮我们审一下现有协议吗?
可以。把协议文档 发给我们,我们按上面这份清单逐项看一遍,指出 App 对接时可能出问题的地方。