0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

嵌入式应用研究院 来源:嵌入式大杂烩 2023-04-21 09:16 次阅读

嵌入式开发中,常常会自定义一些协议格式,比如用于板与板之间的通信、客户端与服务端之间的通信等。

自定义的协议格式可能有很多种,本篇文章我们来介绍一种很常用、实用、且灵活性很高的协议格式——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字节。

下面我们制定一个格式:

5c798d4a-dfc4-11ed-bfe3-dac502259ad0.png

实际中,如果在物联网系统中数据传输,我们用户自定义的协议字段可能就只包含如上四个字段就可以了。比如我们公司的云平台上的用户数据格式用的就是类似ITLV这样的格式。用户在制定协议时的协议字段包含如上字段就可以了。

没有包头做一些数据区分,也没有校验字段,只包含如上字段就能保证数据可靠传输吗?

因为端云通信采用MQTT,基于TCP,TCP的特点就是可靠的,网络协议中会带有校验。并且,实际在传输用户数据时,还会再用户数据之前增加一些字段区分这就是用户数据。所以,其实基于它的设备SDK来进行开发,操作的数据就是如上的数据。

但是,如果应用于板与板之间的通信,只包含如上字段自然是有风险的。我们至少还需要还要包头、校验字段。

实际中根据需要还可以增加其它字段,比如如果需要分包发送,还需要增加包号;如果多块板之间进行通信,还需要增加发送数据目标地址等。

这里我们增加包头与校验字段:

5c9d0856-dfc4-11ed-bfe3-dac502259ad0.png

其中:

(1)Head固定为0x55、0xAA。

(2)Length为1字节,即Value最大为256B。

ITLV格式数据处理

下面以例子来演示ITLV格式数据的处理。

5cc51d28-dfc4-11ed-bfe3-dac502259ad0.png

下面我们以上面我们制定的协议编写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;

下面设计我们的收、发数据结构,大致思路如下:

5ce42d3a-dfc4-11ed-bfe3-dac502259ad0.png

我们创建一个总的结构体,用于管理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、组包

大致思路如下:

5d11d35c-dfc4-11ed-bfe3-dac502259ad0.png

组包函数:

左右滑动查看全部代码>>>

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、解包

大致思路如下:

5d4000ba-dfc4-11ed-bfe3-dac502259ad0.png

解包函数:

左右滑动查看全部代码>>>

//解包函数
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;
}

编译、运行:

5d6af7de-dfc4-11ed-bfe3-dac502259ad0.png

对照着我们制定的协议,数据完全正确!

ITLV格式的其它用法

ITLV格式具有很强的灵活性,我们这里使用的数据类型Type为字节数组,其实使用字符串类型也很常用,比如为了协议具备更强的可读性、方便调试,可以在Value字段里再封装一层JSON格式数据。其实我觉得Type的选项只保留字节数组及字符串就够用了,可以满足所有情况。

当然,可能有些数据长度总是定长的,也可以用其它定长的类型。比如数据都是一些定长的类型,那么L字段也可以省略掉。实际中,比较通用的做法就是:全用字节数组或者全用字符串。别混着用,代码可能会很混乱。

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 通信协议
    +关注

    关注

    28

    文章

    722

    浏览量

    39590
  • TCP
    TCP
    +关注

    关注

    8

    文章

    1253

    浏览量

    78192
  • 函数
    +关注

    关注

    3

    文章

    3846

    浏览量

    61228
  • 嵌入式软件
    +关注

    关注

    4

    文章

    223

    浏览量

    26363
  • MQTT
    +关注

    关注

    5

    文章

    527

    浏览量

    21886

原文标题:一个小而巧的自定义嵌入式软件通信协议

文章出处:【微信号:嵌入式应用研究院,微信公众号:嵌入式应用研究院】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    【LabVIEW串口通信】串行通信协议的可配置转换问题

    本帖最后由 fantek 于 2013-12-31 18:26 编辑 目前存在的问题:主机( 计算机,PLC等)通过串口连接两台或多台通信协议样的仪器设备,而这些设备都是自定义协议
    发表于 09-29 02:26

    何进行串口通信自定义协议,真心求教

    现在需要通过串口通信向向显示屏传 温度、适度还有心跳,以便在显示屏显示出来,协议自定义。求教,怎么自定义协议呢。求高手指点。谢谢~只是
    发表于 09-23 07:38

    STM32 自定义串口协议 精选资料分享

    STM32 自定义串口协议串行通信原理与优缺点分类按通信方向按通信方式异步串行引脚连接串口外设之间ARM与PC之间字符帧格式串口
    发表于 08-17 08:58

    嵌入式领域中常用的5种通信协议是什么

    文章目录嵌入式领域中常用的5种通信协议嵌入式领域中常用的5种通信协议串口协议(UART/USART):串口总线其实就是
    发表于 12-14 06:22

    嵌入式C语言自定义标准是什么

    嵌入式C语言自定义标准文件夹架构:nano版本:┖ Project……┖ CodeuserMain.cuserConfig.hmodEc20.cmodEc20.h
    发表于 12-15 09:14

    STM32自定义数据帧连续发送错误命令后不能再接受指令怎么解决

    关于STM32自定义数据帧连续发送错误命令后不能再接受指令今天嵌入式课程学习,老师发布的小课题,通过串口和定时器做一个小项目,项目如下:上位机通过
    发表于 01-11 06:24

    使用自定义协议的USART

    是提供在AT32微控制器上创建IAP应用程序的般准则。AT32微控制器可以运行用户特定的固件来对微控制器中嵌入的闪存执行IAP。此功能可以使用产品可用和支持的任何通信接口。使用自定义
    发表于 01-14 06:14

    使用51单片机完成简单的串口通信协议

    转载自:自定义串口通信协议的实现weixin_33885253 2017-01-18 21:11:00 1926收藏 4 文章标签: 嵌入式 c/c++ java版权使用51单片机完成
    发表于 01-19 07:30

    嵌入式开发中自定义协议的解析与组包相关案例分享

    1、嵌入式开发中自定义协议的解析与组包  在嵌入式产品开发中,经常会遇到两设备之间的通信、设备
    发表于 10-27 17:01

    如何实现自定义串口通信协议

    有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学。 同时,偶尔有读者问关于串口自定义通信协议相关的问题,今天就来写写串口通信协议,并不是你想想中的那么
    的头像 发表于 06-01 10:01 4185次阅读

    C#与STM32自定义通信协议

    C#与STM32自定义通信协议功能:1.可通过C#上位机对多台STM32下位机进行控制2.自定义上位机与下位机通信协议
    发表于 12-24 18:59 36次下载
    C#与STM32<b class='flag-5'>自定义</b><b class='flag-5'>通信协议</b>

    嵌入式开发中自定义协议的解析与组包

    嵌入式产品开发中,经常会遇到两个设备之间的通信、设备与服务器的通信、设备和上位机的通信等,很多时候通信协议都是
    发表于 01-25 11:14 5次下载
    <b class='flag-5'>嵌入式</b>开发中<b class='flag-5'>自定义</b><b class='flag-5'>协议</b>的解析与组包

    拓普微智能液晶显示模块HMI自定义通信协议

    随着工业技术的发展,HMI(人机界面)的应用领域愈加广泛。通过拓普微的智能液晶显示模块实现的自定义通信协议能在一定程度上维护企业的数据隐秘性,提升产品功能的多样性,并且能够解决企业通信协议不兼容问题。
    的头像 发表于 12-09 14:01 769次阅读
    拓普微智能液晶显示模块HMI<b class='flag-5'>自定义</b><b class='flag-5'>通信协议</b>

    智能液晶显示模块HMI自定义通信协议分析

    随着工业技术的发展,HMI(人机界面)的应用领域愈加广泛。通过拓普微的智能液晶显示模块实现的自定义通信协议能在一定程度上维护企业的数据隐秘性,提升产品功能的多样性,并且能够解决企业通信协议不兼容问题。
    的头像 发表于 07-30 14:46 772次阅读
    智能液晶显示模块HMI<b class='flag-5'>自定义</b><b class='flag-5'>通信协议</b>分析

    基于PIC16F877A单片机的自定义无线传输协议和短信通信协议

    电子发烧友网站提供《基于PIC16F877A单片机的自定义无线传输协议和短信通信协议.pdf》资料免费下载
    发表于 11-08 14:47 1次下载
    基于PIC16F877A单片机的<b class='flag-5'>自定义</b>无线传输<b class='flag-5'>协议</b>和短信<b class='flag-5'>通信协议</b>