嵌入式开发中,常常会自定义一些协议格式,比如用于板与板之间的通信、客户端与服务端之间的通信等。
自定义的协议格式可能有很多种,本篇文章我们来介绍一种很常用、实用、且灵活性很高的协议格式——ITLV格式。
什么是ITLV格式?
大家可能看到网络上的很多文章用的是TLV(Tag、Length、Value)格式数据。实际中,可以根据实际需要进行修改。我们这里稍微改一下,实际上也是大同小异的。
我们这里的ITLV各字段的含义:
I:ID或Index,用于区分是什么数据。
T:Type,代表数据类型,如int、float等。
L:Length,表示数据的长度(Value的长度)。
V:Value,表示实际的数据。
其中,I、T、L是固定长度的,在制定具体的数据协议之前,需要评估好当前项目的数据会有多少、数据的最大长度是多少,考虑好后续数据扩展也可以保证协议通用。一般I设置为1~2字节,T设置为1字节,L设置为1~4字节。
下面我们制定一个格式:

实际中,如果在物联网系统中数据传输,我们用户自定义的协议字段可能就只包含如上四个字段就可以了。比如我们公司的云平台上的用户数据格式用的就是类似ITLV这样的格式。用户在制定协议时的协议字段包含如上字段就可以了。
没有包头做一些数据区分,也没有校验字段,只包含如上字段就能保证数据可靠传输吗?
因为端云通信采用MQTT,基于TCP,TCP的特点就是可靠的,网络协议中会带有校验。并且,实际在传输用户数据时,还会再用户数据之前增加一些字段区分这就是用户数据。所以,其实基于它的设备SDK来进行开发,操作的数据就是如上的数据。
但是,如果应用于板与板之间的通信,只包含如上字段自然是有风险的。我们至少还需要还要包头、校验字段。
实际中根据需要还可以增加其它字段,比如如果需要分包发送,还需要增加包号;如果多块板之间进行通信,还需要增加发送数据目标地址等。
这里我们增加包头与校验字段:

其中:
(1)Head固定为0x55、0xAA。
(2)Length为1字节,即Value最大为256B。
ITLV格式数据处理
下面以例子来演示ITLV格式数据的处理。

下面我们以上面我们制定的协议编写A板的组包、解析代码。
1、设计相关数据结构
首先,我们创建一个协议格式结构体:
#pragmapack(1)
//协议格式
typedefstruct_protocol_format
{
uint16_thead;
uint8_tid;
uint8_ttype;
uint8_tlength;
uint8_tvalue[];
}protocol_format_t;
type字段的取值:
//TLV数据类型type
typedefenum_tlv_type
{
TLV_TYPE_UINT8,
TLV_TYPE_INT8,
TLV_TYPE_UINT16,
TLV_TYPE_INT16,
TLV_TYPE_UINT32,
TLV_TYPE_INT32,
TLV_TYPE_STRING,
TLV_TYPE_FLOAT,
TLV_TYPE_BYTE_ARR,//字节数组
}tlv_type_e;
下面设计我们的收、发数据结构,大致思路如下:

我们创建一个总的结构体,用于管理A板往B板发送及A板接受来自B板的数据:
//总的协议数据
typedefstruct_protocol_data
{
protocol_id_eid;
protocol_value_tvalue;
}protocol_data_t;
其中,成员id是一个枚举:
左右滑动查看全部代码>>>
//数据ID
typedefenum_protocol_id
{
//A板发往B板
PROTOCOL_ID_A_TO_B_BASE=0x00,
PROTOCOL_ID_A_TO_B_CTRL_CMD,
PROTOCOL_ID_A_TO_B_DATE_TIME,
PROTOCOL_ID_A_TO_B_END=0x7F,
//B板发往A板
PROTOCOL_ID_B_TO_A_BASE=0x80,
PROTOCOL_ID_B_TO_A_WORK_STATUS,
PROTOCOL_ID_B_TO_A_END=0xFF,
}protocol_id_e;
包含着A->B、B->A的ID,因为ID是用1个字节标识,收、发的ID各预留一半,新增的ID在各自的BASE ID及END ID之间添加。
成员value是一个联合体,用于管理A->B、B->A的value数据:
左右滑动查看全部代码>>>
//所有协议数据value值
typedefunion_protocol_value
{
protocol_value_a_to_b_ta_to_b_value;
protocol_value_b_to_a_tb_to_a_value;
}protocol_value_t;
a_to_b_value及b_to_a_value也是联合体,用于管理更细分的数据:
左右滑动查看全部代码>>>
//A板发往B板的数据value值
typedefunion_protocol_value_a_to_b
{
protocol_data_ctrl_cmd_tctrl_cmd;
protocol_data_time_tdate_time;
}protocol_value_a_to_b_t;
//B板发往A板的数据value值
typedefunion_protocol_value_b_to_a
{
protocol_data_work_status_twork_status;
}protocol_value_b_to_a_t;
更细分的数据:
左右滑动查看全部代码>>>
//控制命令
typedefenum_ctrl_cmd
{
CTRL_CMD_LED_ON,
CTRL_CMD_LED_OFF
}ctrl_cmd_e;
typedefstruct_protocol_data_ctrl_cmd
{
ctrl_cmd_ecmd;
}protocol_data_ctrl_cmd_t;
//时间数据
typedefstruct_protocol_data_time
{
intyear;
intmon;
intmday;
inthour;
intmin;
intsec;
}protocol_data_time_t;
//工作状态
typedefenum_work_status
{
WORK_STATUS_NORMAL,
WORK_STATUS_ERROR
}work_status_e;
typedefstruct_protocol_data_work_status
{
work_status_estatus;
}protocol_data_work_status_t;
明确了我们需要进行交互的数据的类型之后,解析来我们就可以根据它们的特点来编写组包、解析函数了。
2、组包
大致思路如下:

组包函数:
左右滑动查看全部代码>>>
intprotocol_data_packet(uint8_t*buf,uint16_tlen,protocol_data_t*protocol_data)
{
intret=-1;
intvalue_len=0;
intoffset=0;
protocol_format_t*p_protocol_format=NULL;
if(!buf||!protocol_data||len< PROTOCOL_MIN_LEN)
{
printf("Invalid input argument!
");
return ret;
}
// 通过ID来获取value的长度
switch (protocol_data->id)
{
casePROTOCOL_ID_A_TO_B_CTRL_CMD:
{
printf("PROTOCOL_ID_A_TO_B_CTRL_CMD
");
value_len=sizeof(protocol_data->value.a_to_b_value.ctrl_cmd);
printf("protocol_format.length=%d
",value_len);
break;
}
casePROTOCOL_ID_A_TO_B_DATE_TIME:
{
printf("PROTOCOL_ID_A_TO_B_DATE_TIME
");
value_len=sizeof(protocol_data->value.a_to_b_value.date_time);
printf("value_len=%d
",value_len);
break;
}
default:
break;
}
//为协议格式数据申请内存
p_protocol_format=(protocol_format_t*)malloc(sizeof(protocol_format_t)+value_len);
if(NULL==p_protocol_format)
{
printf("mallocerror
");
returnret;
}
//填充协议数据各字段
p_protocol_format->head=PROTOCOL_HEAD;
p_protocol_format->id=protocol_data->id;
p_protocol_format->type=TLV_TYPE_BYTE_ARR;
p_protocol_format->length=value_len;
if(p_protocol_format->length<= PROTOCOL_VALUE_MAX_LEN)
{
memcpy(p_protocol_format->value,&protocol_data->value.a_to_b_value,p_protocol_format->length);
}
else
{
printf("protocol_format.length>PROTOCOL_VALUE_MAX_LEN
");
}
//计算校验值
uint32_tcrc_data_len=sizeof(protocol_format_t)+value_len;
uint16_tcrc16=crc16_x25_check((uint8_t*)p_protocol_format,crc_data_len);
printf("crc16=%#x
",crc16);
//struct->buf
memcpy(buf,p_protocol_format,crc_data_len);
offset+=crc_data_len;
memcpy(buf+offset,&crc16,sizeof(uint16_t));
offset+=sizeof(uint16_t);
//释放内存
free(p_protocol_format);
p_protocol_format=NULL;
returnoffset;
}
3、解包
大致思路如下:

解包函数:
左右滑动查看全部代码>>>
//解包函数 voidprotocol_data_parse(protocol_data_t*protocol_data,uint8_t*buf,uint16_tlen) { protocol_format_t*p_protocol_format=NULL; if(!buf||!protocol_data||len< PROTOCOL_MIN_LEN) { printf("Invalid input argument! "); return; } // 为协议格式数据申请内存 int value_len = buf[PROTOCOL_LENGTH_INDEX]; p_protocol_format = (protocol_format_t *)malloc(sizeof(protocol_format_t) + value_len); if (NULL == p_protocol_format) { printf("malloc p_protocol_format error "); return; } // buf ->struct memcpy(p_protocol_format,buf,sizeof(protocol_format_t)+value_len); printf("protocol_data->id=%#x ",p_protocol_format->id); //通过数据ID来解析各对应的数据 switch(p_protocol_format->id) { casePROTOCOL_ID_B_TO_A_WORK_STATUS: { printf("PROTOCOL_ID_B_TO_A_WORK_STATUS "); uint8_twork_status_len=sizeof(protocol_data->value.b_to_a_value.work_status); if(p_protocol_format->length==work_status_len) { memcpy(&protocol_data->value.b_to_a_value.work_status,p_protocol_format->value,p_protocol_format->length); } else { printf("p_protocol_format->lengtherror "); } break; } default: break; } //释放内存 free(p_protocol_format); p_protocol_format=NULL; }
4、CRC16校验
CRC16分很多种:CRC16-X25、CRC16-MODBUS、CRC16-XMODEM等。
这里我们使用CRC16-X25:
staticconstunsignedshortcrc16_table[256]=
{
0x0000,0x1189,0x2312,0x329b,0x4624,0x57ad,0x6536,0x74bf,
0x8c48,0x9dc1,0xaf5a,0xbed3,0xca6c,0xdbe5,0xe97e,0xf8f7,
0x1081,0x0108,0x3393,0x221a,0x56a5,0x472c,0x75b7,0x643e,
0x9cc9,0x8d40,0xbfdb,0xae52,0xdaed,0xcb64,0xf9ff,0xe876,
0x2102,0x308b,0x0210,0x1399,0x6726,0x76af,0x4434,0x55bd,
0xad4a,0xbcc3,0x8e58,0x9fd1,0xeb6e,0xfae7,0xc87c,0xd9f5,
0x3183,0x200a,0x1291,0x0318,0x77a7,0x662e,0x54b5,0x453c,
0xbdcb,0xac42,0x9ed9,0x8f50,0xfbef,0xea66,0xd8fd,0xc974,
0x4204,0x538d,0x6116,0x709f,0x0420,0x15a9,0x2732,0x36bb,
0xce4c,0xdfc5,0xed5e,0xfcd7,0x8868,0x99e1,0xab7a,0xbaf3,
0x5285,0x430c,0x7197,0x601e,0x14a1,0x0528,0x37b3,0x263a,
0xdecd,0xcf44,0xfddf,0xec56,0x98e9,0x8960,0xbbfb,0xaa72,
0x6306,0x728f,0x4014,0x519d,0x2522,0x34ab,0x0630,0x17b9,
0xef4e,0xfec7,0xcc5c,0xddd5,0xa96a,0xb8e3,0x8a78,0x9bf1,
0x7387,0x620e,0x5095,0x411c,0x35a3,0x242a,0x16b1,0x0738,
0xffcf,0xee46,0xdcdd,0xcd54,0xb9eb,0xa862,0x9af9,0x8b70,
0x8408,0x9581,0xa71a,0xb693,0xc22c,0xd3a5,0xe13e,0xf0b7,
0x0840,0x19c9,0x2b52,0x3adb,0x4e64,0x5fed,0x6d76,0x7cff,
0x9489,0x8500,0xb79b,0xa612,0xd2ad,0xc324,0xf1bf,0xe036,
0x18c1,0x0948,0x3bd3,0x2a5a,0x5ee5,0x4f6c,0x7df7,0x6c7e,
0xa50a,0xb483,0x8618,0x9791,0xe32e,0xf2a7,0xc03c,0xd1b5,
0x2942,0x38cb,0x0a50,0x1bd9,0x6f66,0x7eef,0x4c74,0x5dfd,
0xb58b,0xa402,0x9699,0x8710,0xf3af,0xe226,0xd0bd,0xc134,
0x39c3,0x284a,0x1ad1,0x0b58,0x7fe7,0x6e6e,0x5cf5,0x4d7c,
0xc60c,0xd785,0xe51e,0xf497,0x8028,0x91a1,0xa33a,0xb2b3,
0x4a44,0x5bcd,0x6956,0x78df,0x0c60,0x1de9,0x2f72,0x3efb,
0xd68d,0xc704,0xf59f,0xe416,0x90a9,0x8120,0xb3bb,0xa232,
0x5ac5,0x4b4c,0x79d7,0x685e,0x1ce1,0x0d68,0x3ff3,0x2e7a,
0xe70e,0xf687,0xc41c,0xd595,0xa12a,0xb0a3,0x8238,0x93b1,
0x6b46,0x7acf,0x4854,0x59dd,0x2d62,0x3ceb,0x0e70,0x1ff9,
0xf78f,0xe606,0xd49d,0xc514,0xb1ab,0xa022,0x92b9,0x8330,
0x7bc7,0x6a4e,0x58d5,0x495c,0x3de3,0x2c6a,0x1ef1,0x0f78
};
uint16_tcrc16_x25_check(uint8_t*data,uint32_tlength)
{
unsignedshortcrc_reg=0xFFFF;
while(length--)
{
crc_reg=(crc_reg>>8)^crc16_table[(crc_reg^*data++)&0xff];
}
return(uint16_t)(~crc_reg)&0xFFFF;
}
5、测试代码
下面我们编写组包、解包测试代码:
组包控制命令数据,并把组包之后的发送缓冲区中的数据打印出来。
组包时间数据,并把组包之后的发送缓冲区中的数据打印出来。
从一个模拟的工作状态接受缓冲区数据中解析工作状态数据并打印出来。
测试代码如:
左右滑动查看全部代码>>>
//微信公众号:嵌入式大杂烩 #include#include #include"protocol_tlv.h" intmain(intarc,char*argv[]) { staticuint8_tsend_buf[PROTOCOL_MAX_LEN]={0}; protocol_data_tprotocol_data_send={0}; intsend_len=0; printf(" ==============================testpacket=========================================== "); //模拟组包发送控制命令 bzero(send_buf,sizeof(send_buf)); bzero(&protocol_data_send,sizeof(protocol_data_t)); protocol_data_send.id=PROTOCOL_ID_A_TO_B_CTRL_CMD; protocol_data_send.value.a_to_b_value.ctrl_cmd.cmd=CTRL_CMD_LED_OFF; send_len=protocol_data_packet(send_buf,PROTOCOL_MAX_LEN,&protocol_data_send); printf("sendctrldata="); print_hex_data_frame(send_buf,send_len); //模拟组包发送时间数据 bzero(send_buf,sizeof(send_buf)); bzero(&protocol_data_send,sizeof(protocol_data_t)); protocol_data_send.id=PROTOCOL_ID_A_TO_B_DATE_TIME; protocol_data_send.value.a_to_b_value.date_time.year=2022; protocol_data_send.value.a_to_b_value.date_time.mon=8; protocol_data_send.value.a_to_b_value.date_time.mday=20; protocol_data_send.value.a_to_b_value.date_time.hour=8; protocol_data_send.value.a_to_b_value.date_time.min=8; protocol_data_send.value.a_to_b_value.date_time.sec=8; send_len=protocol_data_packet(send_buf,PROTOCOL_MAX_LEN,&protocol_data_send); printf("senddate_timedata="); print_hex_data_frame(send_buf,send_len); printf(" ==============================testparse=========================================== "); //模拟解析工作状态数据 uint8_twork_status_buf[11]={0x55,0xAA,0x81,0x08,0x04,0x01,0x00,0x00,0x00,0xf2,0x88}; protocol_data_tprotocol_data_recv={0}; uint16_tcalc_crc16=crc16_x25_check(work_status_buf,sizeof(work_status_buf)-2); uint16_trecv_crc16=(uint16_t)(work_status_buf[10]<< 8) | work_status_buf[9]; if (calc_crc16 == recv_crc16) { protocol_data_parse(&protocol_data_recv, work_status_buf, sizeof(work_status_buf)); printf("work_status = %d ", protocol_data_recv.value.b_to_a_value.work_status.status); } return 0; }
编译、运行:

对照着我们制定的协议,数据完全正确!
ITLV格式的其它用法
ITLV格式具有很强的灵活性,我们这里使用的数据类型Type为字节数组,其实使用字符串类型也很常用,比如为了协议具备更强的可读性、方便调试,可以在Value字段里再封装一层JSON格式数据。其实我觉得Type的选项只保留字节数组及字符串就够用了,可以满足所有情况。
当然,可能有些数据长度总是定长的,也可以用其它定长的类型。比如数据都是一些定长的类型,那么L字段也可以省略掉。实际中,比较通用的做法就是:全用字节数组或者全用字符串。别混着用,代码可能会很混乱。
审核编辑:汤梓红
-
通信协议
+关注
关注
28文章
1073浏览量
41869 -
TCP
+关注
关注
8文章
1418浏览量
83020 -
函数
+关注
关注
3文章
4406浏览量
66841 -
嵌入式软件
+关注
关注
4文章
247浏览量
27822 -
MQTT
+关注
关注
5文章
721浏览量
24786
原文标题:一个小而巧的自定义嵌入式软件通信协议
文章出处:【微信号:嵌入式应用研究院,微信公众号:嵌入式应用研究院】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
【LabVIEW串口通信】串行通信协议的可配置转换问题
使用自定义协议的USART
使用51单片机完成一个简单的串口通信协议
SOPC中自定义外设和自定义指令性能分析
基于嵌入式中央处理单元(CPU)的自定义指令
嵌入式的CPU自定义指令有什么特点
自定义串口通信协议
拓普微智能液晶显示模块HMI自定义通信协议
智能液晶显示模块HMI自定义通信协议分析

一个小而巧的自定义嵌入式软件通信协议
评论