快捷搜索:  汽车  科技

常用协议格式(分享一种灵活性很高的协议格式)

常用协议格式(分享一种灵活性很高的协议格式)没有包头做一些数据区分,也没有校验字段,只包含如上字段就能保证数据可靠传输吗?实际中,如果在物联网系统中数据传输,我们用户自定义的协议字段可能就只包含如上四个字段就可以了。比如我们公司的云平台上的用户数据格式用的就是类似ITLV这样的格式。用户在制定协议时的协议字段包含如上字段就可以了。我们这里的ITLV各字段的含义:其中,I、T、L是固定长度的,在制定具体的数据协议之前,需要评估好当前项目的数据会有多少、数据的最大长度是多少,考虑好后续数据扩展也可以保证协议通用。一般I设置为1~2字节,T设置为1字节,L设置为1~4字节。下面我们制定一个格式:

大家好,我是杂烩君。

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

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

下面我们制定一个格式:

常用协议格式(分享一种灵活性很高的协议格式)(1)

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

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

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

但是,如果应用于板与板之间的通信,只包含如上字段自然是有风险的。我们至少还需要还要包头、校验字段。实际中根据需要还可以增加其它字段,比如如果需要分包发送,还需要增加包号;如果多块板之间进行通信,还需要增加发送数据目标地址等。

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

常用协议格式(分享一种灵活性很高的协议格式)(2)

其中:

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

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

ITLV格式数据处理

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

常用协议格式(分享一种灵活性很高的协议格式)(3)

下面我们以上面我们制定的协议编写A板的组包、解析代码。

1、设计相关数据结构

首先,我们创建一个协议格式结构体:

#pragma pack(1) // 协议格式 typedef struct _PROTOCOL_format { uint16_t head; uint8_t id; uint8_t type; uint8_t length; uint8_t value[]; }protocol_format_t;

type字段的取值:

// TLV 数据类型type typedef enum _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;

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

常用协议格式(分享一种灵活性很高的协议格式)(4)

我们创建一个总的结构体,用于管理A板往B板发送及A板接受来自B板的数据:

// 总的协议数据 typedef struct _protocol_data { protocol_id_e id; protocol_value_t value; }protocol_data_t;

其中,成员id是一个枚举:

// 数据ID typedef enum _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值 typedef union _protocol_value { protocol_value_a_to_b_t a_to_b_value; protocol_value_b_to_a_t b_to_a_value; }protocol_value_t;

a_to_b_value及b_to_a_value也是联合体,用于管理更细分的数据:

// A板发往B板的数据value值 typedef union _protocol_value_a_to_b { protocol_data_ctrl_cmd_t ctrl_cmd; protocol_data_time_t date_time; }protocol_value_a_to_b_t; // B板发往A板的数据value值 typedef union _protocol_value_b_to_a { protocol_data_work_status_t work_status; }protocol_value_b_to_a_t;

更细分的数据:

// 控制命令 typedef enum _ctrl_cmd { CTRL_CMD_LED_ON CTRL_CMD_LED_OFF }ctrl_cmd_e; typedef struct _protocol_data_ctrl_cmd { ctrl_cmd_e cmd; }protocol_data_ctrl_cmd_t; // 时间数据 typedef struct _protocol_data_time { int year; int mon; int mday; int hour; int min; int sec; }protocol_data_time_t; // 工作状态 typedef enum _work_status { WORK_STATUS_NORMAL WORK_STATUS_ERROR }work_status_e; typedef struct _protocol_data_work_status { work_status_e status; }protocol_data_work_status_t;

明确了我们需要进行交互的数据的类型之后,解析来我们就可以根据它们的特点来编写组包、解析函数了。

2、组包

大致思路如下:

常用协议格式(分享一种灵活性很高的协议格式)(5)

组包函数:

int protocol_data_packet(uint8_t *buf uint16_t len protocol_data_t *protocol_data) { int ret = -1; int value_len = 0; int offset = 0; protocol_format_t *p_protocol_format = NULL; if (!buf || !protocol_data || len < PROTOCOL_MIN_LEN) { printf("Invalid input argument!\n"); return ret; } // 通过ID来获取value的长度 switch (protocol_data->id) { case PROTOCOL_ID_A_TO_B_CTRL_CMD: { printf("PROTOCOL_ID_A_TO_B_CTRL_CMD\n"); value_len = sizeof(protocol_data->value.a_to_b_value.ctrl_cmd); printf("protocol_format.length = %d\n" value_len); break; } case PROTOCOL_ID_A_TO_B_DATE_TIME: { printf("PROTOCOL_ID_A_TO_B_DATE_TIME\n"); value_len = sizeof(protocol_data->value.a_to_b_value.date_time); printf("value_len = %d\n" value_len); break; } default: break; } // 为协议格式数据申请内存 p_protocol_format = (protocol_format_t *)malloc(sizeof(protocol_format_t) value_len); if (NULL == p_protocol_format) { printf("malloc error\n"); return ret; } // 填充协议数据各字段 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\n"); } // 计算校验值 uint32_t crc_data_len = sizeof(protocol_format_t) value_len; uint16_t crc16 = crc16_x25_check((uint8_t*)p_protocol_format crc_data_len); printf("crc16 = %#x\n" 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; return offset; }3、解包

大致思路如下:

常用协议格式(分享一种灵活性很高的协议格式)(6)

解包函数:

// 解包函数 void protocol_data_parse(protocol_data_t *protocol_data uint8_t *buf uint16_t len) { protocol_format_t *p_protocol_format = NULL; if (!buf || !protocol_data || len < PROTOCOL_MIN_LEN) { printf("Invalid input argument!\n"); 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\n"); return; } // buf -> struct memcpy(p_protocol_format buf sizeof(protocol_format_t) value_len); printf("protocol_data->id = %#x\n" p_protocol_format->id); // 通过数据ID来解析各对应的数据 switch (p_protocol_format->id) { case PROTOCOL_ID_B_TO_A_WORK_STATUS: { printf("PROTOCOL_ID_B_TO_A_WORK_STATUS\n"); uint8_t work_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->length error\n"); } break; } default: break; } // 释放内存 free(p_protocol_format); p_protocol_format = NULL; }4、CRC16校验

CRC16分很多种:CRC16-X25、CRC16-MODBUS、CRC16-XMODEM等。

这里我们使用CRC16-X25:

static const unsigned short crc16_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_t crc16_x25_check(uint8_t* data uint32_t length) { unsigned short crc_reg = 0xFFFF; while (length--) { crc_reg = (crc_reg >> 8) ^ crc16_table[(crc_reg ^ *data ) & 0xff]; } return (uint16_t)(~crc_reg) & 0xFFFF; }5、测试代码

下面我们编写组包、解包测试代码:

  • 组包控制命令数据,并把组包之后的发送缓冲区中的数据打印出来。
  • 组包时间数据,并把组包之后的发送缓冲区中的数据打印出来。
  • 从一个模拟的工作状态接受缓冲区数据中解析工作状态数据并打印出来。

测试代码如:

// 嵌入式大杂烩 #include <stdio.h> #include <strings.h> #include "protocol_tlv.h" int main(int arc char *argv[]) { static uint8_t send_buf[PROTOCOL_MAX_LEN] = {0}; protocol_data_t protocol_data_send = {0}; int send_len = 0; printf("\n==============================test packet===========================================\n"); // 模拟组包发送控制命令 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("send ctrl data = "); 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("send date_time data = "); print_hex_data_frame(send_buf send_len); printf("\n==============================test parse===========================================\n"); // 模拟解析工作状态数据 uint8_t work_status_buf[11] = {0x55 0xAA 0x81 0x08 0x04 0x01 0x00 0x00 0x00 0xf2 0x88}; protocol_data_t protocol_data_recv = {0}; uint16_t calc_crc16 = crc16_x25_check(work_status_buf sizeof(work_status_buf) - 2); uint16_t recv_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\n" protocol_data_recv.value.b_to_a_value.work_status.status); } return 0; }

编译、运行:

常用协议格式(分享一种灵活性很高的协议格式)(7)

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

ITLV格式的其它用法

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

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

代码获取

大家如果需要本篇文章的demo工程,也可以在私信回复关键词: ITLV协议格式 ,即可获取。

以上本次的分享,期待你的三连支持!

私信回复【嵌入式书籍】,可获取博主精心整理的嵌入式电子书一份

猜您喜欢: