自定义协议就是自己指定一套通信的语言格式。
一、为什么用它(自定义协议)?
虽然目前有很多成熟的协议(如Modbus、MQTT、HTTP、CANopen...),但是嵌入式往往资源有限、功能定制化强,这时就需要自定义的协议,汇总一下原因主要有以下几种:
1、系统资源受限;(标准协议太复杂,占内存大,单片机难实现,如HTTP、MQTT)
2、功能需求独特;(需要传输特定数据,如传感器值、命令、状态等)
3、速度和效率要求高;(通信简洁、响应迅速)
4、硬件接口简单;(串口、485、IIC、SPI等通信接口不自带高级协议,需要自己封装)
5、多设备配合工作。(主控与多节点或模块通信时需要统一自定义格式)
先举个例子:
STM32通过串口与ESP8266通信,想上传温湿度和报警状态到上位机,可以定义一个数据格式,如:
$TEMP,25.6,HUM,60.2,ALARM,0#
上位机根据$开头,#结束,逗号分隔来解析。这就是一个简单的自定义协议。
二、应用场景
1、单片机与传感器节点通信;(如主控STM32与多个从设备通信IIC或SPI或UART)
2、主机与上位机通信;(如STM32通过串口或WIFI发送数据给Qt或PC端软件)
3、多机协同系统;(例如路灯控制系统中主控与多个节点通信)
4、无线通信模块中数据封装;(如LoRa、nRF24L01、蓝牙模块,自己规定帧结构)
5、网关或云端上传前的数据整合;(物联网系统中,网关在上传数据前封装成统一协议格式)
三、怎么设计一个合理的通信协议?
1、定义帧格式

举个例子说明:
AA 55 05 01 19 32 2A 0D 0A
AA 55:帧头 05:数据长度 01:命令字(表示上传温度) 19 32:温度数据(25.62℃)
2A:校验 0D 0A:帧尾
关于命令字:
相当于是一个函数的编号,如:0x01 表示上传温湿度数据;0x02表示上传报警状态;0x10表示设置报警阈值;0x11表示查询设备状态。
关于校验:
这是防止传输数据错误的关键,常用的方法有:
1.累加和校验。这种方法简单快速,即将所有字节求和取低8位
2.CRC16:工业级常用(例如Modbus CRC16)
解析和打包代码:
发送端需要有打包函数,将数据组装成协议的格式后发送;
接收端,解析函数,将接收缓存中提取完整的帧并进行校验。
四、示例:STM32和上位机使用自定义串口协议通信

案例背景:
在智能控制或检测类系统中,例如实现以下功能:
STM32负责采集温湿度、气体浓度、火焰等数据;
上位机(Qt编写)负责实时显示数据、发送控制命令(如控制继电器、蜂鸣器、设置阈值等);
通信方式:串口(UART)双向通信。
我们思考,怎么实现它?怎么保证STM32和上位机可靠通信?自定义协议?
由于串口传输的只是原始的数据流,没有边界和含义区分。如果不自定义协议(这是我们思考问题的一种方式,就是不使用它会出现什么问题),就会出现以下问题:
1.收到数据无法判断哪个是数据头,哪个是数据尾;
2.多条命令放一块无法区分
3.无法判断指令的类型(几位传输)
4.校验也没有(无法判断数据是否传输正确)
综上所述,自定义协议是一个中肯的选择。
那么针对这个案例,怎么合理的自定义通信协议呢?别着急往下看...
将定义固定的帧格式,包括:帧头、设备ID、命令字、数据长度、数据区、校验、帧尾。
详细的定义如下:

命令字定义:

传输数据帧示例:
cpp
AA 55 01 01 04 19 28 00 64 D1 0D 0A
帧头 ID 命令 数据区长度 温湿度数据 校验 帧尾
STM32端实现代码:
cpp
//打包发送函数
#include "stdint.h"
#include "string.h"
#define FRAME_HEAD1 0xAA
#define FRAME_HEAD2 0x55
#define FRAME_TAIL1 0x0D
#define FRAME_TAIL2 0x0A
uint8_t tx_buf[64];
// 累加和校验函数
uint8_t Get_Sum(uint8_t *data, uint8_t len)
{
uint8_t sum = 0;
for (uint8_t i = 0; i < len; i++)
sum += data[i];
return sum;
}
// 打包上传温湿度数据
uint8_t Pack_Temp_Humi(uint8_t id, float temp, float hum)
{
uint8_t len = 0;
uint16_t t = (uint16_t)(temp * 10);
uint16_t h = (uint16_t)(hum * 10);
tx_buf[len++] = FRAME_HEAD1;
tx_buf[len++] = FRAME_HEAD2;
tx_buf[len++] = id;
tx_buf[len++] = 0x01; // 功能码:上传温湿度
tx_buf[len++] = 0x04; // 数据长度
tx_buf[len++] = (t >> 8) & 0xFF;
tx_buf[len++] = t & 0xFF;
tx_buf[len++] = (h >> 8) & 0xFF;
tx_buf[len++] = h & 0xFF;
uint8_t sum = Get_Sum(tx_buf, len);
tx_buf[len++] = sum;
tx_buf[len++] = FRAME_TAIL1;
tx_buf[len++] = FRAME_TAIL2;
return len;
}
// 串口发送函数
void UART_Send(uint8_t *data, uint8_t len)
{
for(uint8_t i=0; i<len; i++)
{
USART_SendData(USART1, data[i]);
while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET);
}
}
说明一下(思路理清它):将固定的帧头、帧尾使用宏定义。定义一个接收缓冲区。将检验和的校验函数封装起来,传参分别为数组指针和长度。编写温湿度数据打包函数,参数为设备ID,温度数据(浮点型数据)和湿度数据(也是浮点型数据),浮点型数据是32位的。先定义一个数组的下标值(len)为0,再将传入的参数1(温度数据)和参数2(湿度数据),扩大10倍,小数点消了,再强转为16位无符号整型数据,方便后面传输数据(将这16位无符号整型数据分结尾两个字节数据因为传输是按字节传输的),依次传输缓冲区数组第1、第2...一直传输到第12个字节数据,他们分别是:帧头(2字节)、id(1字节)、功能码(1字节)、数据区长度(4字节)、温度数据(2字节)、湿度数据(2字节)、帧校验(1字节)、帧尾(2字节),len的值到最后帧尾还要自增,自增后的值就是12,这个12也就是总共传输的字节数,当做返回值。最后就是串口发送函数,我们使用UART1,参数也是传入两个,参数1是数组指针(也就是缓冲区)和长度(也就是上面需要发送的字节数,因为UART一次传输一个字节),放一个循环到里面,因为要循环传输len次,所以条件是<len,变量每次循环自增,每次发送一个字节,发送的缓冲区是变量做数组下标的哪个数组内容,最后加一个判断也就是等待发送完成,才能进入下一次循环,再次进行发送。
那么怎么调用它?
cpp
uint8_t len = Pack_Temp_Humi(1, 25.3, 60.2);
UART_Send(tx_buf, len);
接收区解析函数:
思路就是:STM32打开UART中断依次接收字节数据,并把它放到接收缓冲区里(这时我们就要定义一个接收缓冲区),这个里面要增加判断语句,就是判断一帧数据(自定义的协议里面的)是否发送完,可以判断标号是否大于1并且标号的前两个标号的数组内容是帧尾的两个帧尾字节数据。成立的话我们就给一个标志说明一帧数据已经接收完成了。(中断函数不易过长,所以帧解析函数就另封装),帧解析函数无参无返回值,先判断标志位是否是1(也就是一帧传输完成),先将标志位复位0,判断帧头,将接收缓冲区的数据赋给响应的变量(如命令字,数据区长度,校验),将校验函数的返回值赋给一个变量,将这个变量与传过来的校验值对应的变量比较对不,如果OK,则通过命令字的值执行相应的操作。最后将接收缓冲区的下标置0,并且清空接收缓冲区的值都为0.
cpp
void TCP_Parse_Recv(uint8_t *buf, uint16_t len)
{
if(len < 8) return; // 最小长度检查
if(buf[0]==0xAA && buf[1]==0x55)
{
uint8_t cmd = buf[4];
uint8_t datalen = buf[5];
uint8_t checksum = buf[6 + datalen];
uint8_t sum = Get_Sum(buf, 6 + datalen);
if(checksum == sum && buf[7 + datalen] == 0x0D && buf[8 + datalen] == 0x0A)
{
switch(cmd)
{
case 0x10: // 设置阈值
// buf[6], buf[7]等解析
break;
case 0x11: // 控制继电器
if(buf[6] == 0x01)
Relay_On();
else
Relay_Off();
break;
}
}
}
}
学到这里,应用起来。
实现:STM32使用串口1发送数据(以自定义的协议类型)给上位机,看看上位机收到了什么(这里前提是上位机自己已经做好了,目前还没做),也可以验证一下。所以真正需要验证的是:上位机根据这个格式发送给STM32特定格式的数据,随后下位机接收后根据协议格式,返回给上位机真实数据。
自定义的帧格式是:
cpp
AA 55 01 02 00 02 01 02 00 00 00 00 07 0D 0A
帧头 命令 数据区长度 数据区 保留区 校验 帧尾
(2B) (2B) (2B) (2B) (4B) (1B) (2B)
验证现象一:STM32-->串口助手发送验证成功!

验证现象二:串口助手-->STM32发送验证成功!
发数据1:AA 55 01 02 00 02 01 02 00 00 00 00 07 0D 0A
发数据2:AA 55 02 03 00 02 01 02 00 00 00 00 09 0D 0A(记得校验码要正确,否则不会执行)
两个现象不一致!
测试PASS!说明我们写的程序没有问题!继续再接再厉💪