You may have seen many articles online using the TLV (Tag, Length, Value) format. In practice, it can be modified according to actual needs. Here, we slightly modify it, which is essentially similar.
-
I: ID or Index, used to distinguish what data it is. -
T: Type, representing data types such as int, float, etc. -
L: Length, indicating the length of the data (length of Value). -
V: Value, representing the actual data.

Can reliable data transmission be guaranteed with only the above fields without a header for data differentiation and without a checksum field?
Because the edge-cloud communication uses MQTT, based on TCP, which is reliable. The network protocol includes a checksum. Moreover, when transmitting user data, additional fields are added before the user data to distinguish it as user data. Thus, actually, when developing based on its device SDK, the data operated is as mentioned above.
However, if applied to communication between boards, naturally having only the above fields poses risks. We at least need a header and checksum fields.
In practice, additional fields can be added according to needs. For example, if packet sending is required, a packet number needs to be added; if communication occurs between multiple boards, a target address for sending data needs to be added, etc.
Here we add header and checksum fields:

Where:
(1) Head is fixed as 0x55, 0xAA.

1. Design Related Data Structures
First, we create a protocol format structure:
#pragma pack(1)
// Protocol format
typedef struct _protocol_format
{
uint16_t head;
uint8_t id;
uint8_t type;
uint8_t length;
uint8_t value[];
}protocol_format_t;
Possible values for the type field:
// TLV data 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, // Byte array
}tlv_type_e;
Next, we design our receiving and sending data structures with the following general idea:

We create a general structure to manage the data sent from Board A to Board B and the data received from Board B:
// Overall protocol data
typedef struct _protocol_data
{
protocol_id_e id;
protocol_value_t value;
}protocol_data_t;
Swipe left to view all code>>>
// Data ID
typedef enum _protocol_id
{
// From A board to B board
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,
// From B board to A board
PROTOCOL_ID_B_TO_A_BASE = 0x80,
PROTOCOL_ID_B_TO_A_WORK_STATUS,
PROTOCOL_ID_B_TO_A_END = 0xFF,
}protocol_id_e;
// All protocol data 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 and b_to_a_value are also unions, used to manage more detailed data:
// Data value from A board to B board
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;
// Data value from B board to A board
typedef union _protocol_value_b_to_a
{
protocol_data_work_status_t work_status;
}protocol_value_b_to_a_t;
More detailed data:
// Control commands
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;
// Time data
typedef struct _protocol_data_time
{
int year;
int mon;
int mday;
int hour;
int min;
int sec;
}protocol_data_time_t;
// Working status
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;
After clarifying the types of data we need to interact with, we can write the packaging and parsing functions based on their characteristics.
2. Packaging

Packaging function:
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;
}
// Get value length by ID
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;
}
// Allocate memory for protocol format data
p_protocol_format = (protocol_format_t *)malloc(sizeof(protocol_format_t) + value_len);
if (NULL == p_protocol_format)
{
printf("malloc error\n");
return ret;
}
// Fill in protocol data fields
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");
}
// Calculate checksum
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 memory
free(p_protocol_format);
p_protocol_format = NULL;
return offset;
}
3. Unpacking
The general idea is as follows:

Unpacking function:
// Unpacking function
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;
}
// Allocate memory for protocol format data
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);
// Parse corresponding data by data 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 memory
free(p_protocol_format);
p_protocol_format = NULL;
}
4. CRC16 Check
There are many types of CRC16: CRC16-X25, CRC16-MODBUS, CRC16-XMODEM, etc.
Here we use 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. Test Code
Next, we will write the test code for packaging and unpacking:
-
Package control command data and print the data in the sending buffer after packaging. -
Package time data and print the data in the sending buffer after packaging. -
Parse work status data from a simulated receiving buffer and print it out.
Test code as follows:
// WeChat Official Account: Embedded Miscellaneous
#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");
// Simulate packaging and sending control command
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);
// Simulate packaging and sending time data
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");
// Simulate parsing work status data
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;
}
Compile and run:

According to the protocol we defined, the data is completely correct!
Of course, some data lengths are always fixed, and other fixed-length types can also be used. For example, if the data consists of fixed-length types, the L field can also be omitted. In practice, a more common approach is to use either byte arrays or strings consistently. Mixing them may result in disorganized code.
This concludes this sharing. Looking forward to your support!
Author:ZhengNL, Source: Embedded Miscellaneous
Statement: This article is reprinted with permission from the “Embedded Miscellaneous” official account. Reprinting is for learning reference only and does not represent the views of this account. This account does not bear any infringement liability for its content, text, or images.
Upload link: https://mbb.eet-china.com/download
Scan to go directly to the download center
Click“Read Original” to upload materials!