Flexible Communication Protocols in Embedded Software

Flexible Communication Protocols in Embedded Software

In embedded development, it is common to define custom protocol formats, such as for communication between boards or between client and server.
There can be many types of custom protocol formats. In this article, we will introduce a commonly used, practical, and highly flexible protocol format—ITLV format.
01
What Is ITLV Format?

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.

The meanings of the fields in our ITLV are as follows:
  • 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.
Among them, I, T, and L are of fixed lengths. Before formulating the specific data protocol, it is necessary to evaluate how much data the current project will have and what the maximum length of the data will be. Considering future data expansion can also ensure the protocol’s universality. Generally, I is set to 1~2 bytes, T is set to 1 byte, and L is set to 1~4 bytes.
Next, we will formulate a format:
Flexible Communication Protocols in Embedded Software
In practice, if data transmission occurs in an IoT system, our custom protocol fields may only contain the four fields mentioned above. For example, the user data format on our company’s cloud platform uses a format similar to ITLV. The protocol fields defined by the user can only include the above fields.

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:

Flexible Communication Protocols in Embedded Software

Where:

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

(2) Length is 1 byte, meaning the maximum Value is 256B.
02
ITLV Format Data Processing
Next, we will demonstrate the processing of ITLV format data with examples.
Flexible Communication Protocols in Embedded Software
Next, we will write the packaging and parsing code for Board A using the protocol we defined above.

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:

Flexible Communication Protocols in Embedded Software

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;
Where, the member id is an enumeration:

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;
It contains the IDs from A to B and from B to A. Since the ID is identified by 1 byte, half is reserved for sending and receiving IDs, and new IDs are added between their respective BASE ID and END ID.
The member value is a union, used to manage the value data from A to B and from B to A:
Swipe left to view all code>>>
// 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:

Swipe left to view all code>>>
// 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:

Swipe left to view all code>>>
// 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

The general idea is as follows:
Flexible Communication Protocols in Embedded Software

Packaging function:

Swipe left to view all code>>>
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:

Flexible Communication Protocols in Embedded Software

Unpacking function:

Swipe left to view all code>>>
// 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:

Swipe left to view all code>>>
// 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:

Flexible Communication Protocols in Embedded Software

According to the protocol we defined, the data is completely correct!

03
Other Uses of ITLV Format
ITLV format is highly flexible. The data type Type we use here is a byte array; it is also common to use string types. For example, to make the protocol more readable and easier to debug, a JSON format can be encapsulated in the Value field. I believe that keeping only byte array and string as options for Type is sufficient to meet all situations.

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.

END
Prize Activity
From now on, upload materials in the Breadboard Community to have a chance to win 800 yuan reward~
Activity Time: 2023.6.1 -2023.7.31

Upload link: https://mbb.eet-china.com/download

Flexible Communication Protocols in Embedded SoftwareScan to go directly to the download center

Flexible Communication Protocols in Embedded Software

Flexible Communication Protocols in Embedded SoftwareClick“Read Original” to upload materials!

Leave a Comment