嵌入式实时通讯协议设计与实现

前言

  • 在嵌入式系统中会使用多种方式进行通讯,但是嵌入式系统往往资源受限,已有协议往往体积庞大且功能复杂,不能满足应用场景。
  • 因此会出现很多自定义协议,要求双方按照某一约定的规则进行通讯,下面以实现一个静态内存模型的点对点通讯协议为例进行讲解。

通讯协议分类

硬件协议

  • 在物理层进行数据传输时,发送端和接收端之间需要遵循相应的硬件协议,例如我们通过IO采集一个电压信号,当输入电压高于2V时候,硬件会认为采集到一个高电平,低于2V则认为是一个低电平。常见的光纤,串口,CAN总线我们将其划分为硬件协议,他们是数据传输的载体。

软件协议

  • 根据总线的物理特性,在此之上可以设计出各种各样的软件协议,此处涉及到的协议讲解部分为软件协议,后面简称协议。例如通过IO可以设计出SPI通讯协议和IIC协议,通过CAN总线可以设计出CANOpen协议,通过串口可以设计出Modbus协议。

帧格式

  • 在硬件基础上进行数据传输时需要设计一个帧格式,例如串口传输的数据通常是以字节为单位,那么协议设计时传输的最小的单位也必须是字节,通讯帧通常分为两类,一类是定长帧,一类是不定长帧。

定长帧

  • 所有帧长度都相同,接收者先接收完指定长度字符,然后根据报文类型做进一步处理,这种协议可拓展性较低,适合简单数据传输。

不定长帧

  • 第一种:通讯双方根据功能划分,划分为不同的报文类型,每种类型的长度不尽相同,但是接收者和发送者对每种类型的数据长度都是提前约定好的,传输数据中不包含长度信息。

  • 第二种:帧长度不固定,每帧可以分为三个部分,帧头,数据,校验,其中帧头和校验的长度是固定的,数据的长度是可变的,帧头中包含长度信息,当接收完一个完整的帧头后进行解析,通过读取其中的长度信息获取到剩余数据的长度,然后继续接收,直到接收到校验,一帧数据接收完成,然后进行数据的校验,校验成功进行下一步处理,这种情况下接收端在发送前并不清楚需要接收的数据长度,相比第一种协议可拓展性更强,可以用于传输文件等超长信息。

通讯帧设计

起始标识帧头数据帧尾(校验)
1 byte2 bytesn bytes1 byte
  • 对于用户而言,用户只关心数据内容。
  • 为了正确接收数据,避免传输错误,在用户传入数据后传输层会为数据加上起始标识,帧号,数据长度,校验等其他信息。

起始标志

起始标识
0xAA
  • 起始标识用于接收方识别帧起始位置,然后对数据流进行解析。
  • 在成熟的通讯协议中往往会对起始标志进行转义,在非起始字符位置如果找到一个与起始字符相同的字符会用双字符替换,但是会造成传输数据量增大的问题,与此同时也会降低因数据中包含帧头而解析出错的概率。
  • 设计者需根据应用场景选择是否需要对数据进行转义。

帧头

帧号数据长度
1 byte1 byte
  • 帧号可用于识别出过时和重发数据。
  • 数据长度则代表数据区的字节数。

数据

Data
n bytes
  • 数据长度不固定,是可变的,接收端会根据帧头中包含的长度信息接收数据,如果没有数据则直接接收帧尾

校验

CRC
1 bytes
  • 帧尾部分包含1字节CRC,用于对整个数据进行校验,如果采用TCP这种可靠通讯,可以去掉CRC,CRC是为了防止传输过程中出错

帧数据结构

#pragma pack(1)
typedef struct
{
    uint8_t seq_num;
    uint8_t data_length;
} msg_packet_header_t;

typedef struct
{
    uint8_t crc;
} msg_packet_tail_t;

typedef struct
{
    msg_packet_header_t header;
    uint8_t *payload;
    msg_packet_tail_t tail;
} msg_packet_t;
#pragma pack()
  • msg_packet_t 中未包含帧头,帧头在编码数据包时自动添加到第一个字节。
  • 按照C语言的默认边界对齐方式会产生空隙,于是对该数据结构采用1字节对齐。
  • 帧头包含帧号和数据长度。
  • 数据长度不固定此处使用一个指针来表示。
  • 帧尾包含CRC校验。

消息编码与解码

消息解析类

#define MSG_BUFFER_SIZE 128
#define MSG_START 0xAA

typedef enum
{
    MSG_ERR_NONE,
    MSG_ERR_ENCODE_OVERFLOW,
    MSG_ERR_DECODE_OVERFLOW,
    MSG_ERR_ENCODE_INPUT_INVALID
} msg_status_def;

typedef struct
{
    void *user_data;
    void (*func)(void *user_data, uint8_t *data, int len);
} msg_callback_t;

typedef struct
{
    /* encoder */
    uint8_t encode_offset;
    uint8_t encode_buffer[MSG_BUFFER_SIZE];
    uint8_t seq;

    msg_callback_t decoder_cb;
    /* decoder */
    uint8_t decoder_init;
    uint8_t decode_buffer[MSG_BUFFER_SIZE];
    /* decoder cache */
    msg_packet_t decode_cache;
    msg_packet_t *decode_msg;
    /* decoder status */
    uint8_t recved_start;
    uint8_t recved_header;
    uint8_t remain;
    /* if decode_overflow is not equal to 0, decoder will discard the data until remain redece to 0 */
    uint8_t decode_overflow;
} msg_parser_t;

void msg_register_decode_async_handler(msg_parser_t *parser, msg_callback_t *cb);
msg_status_def msg_encode(msg_parser_t *parser, uint8_t *data, uint8_t len, uint8_t **out, uint8_t *out_len);
msg_t *msg_decode(msg_parser_t *parser, msg_t *msg, uint8_t *data, uint8_t *len);
void msg_decode_async(msg_parser_t *parser, uint8_t *data, uint8_t len);
  • 在 msg_parser_t 中包含了编码和解码所需的对象。
  • 其中提供了两个缓冲区分别用于存放编码和解码结果。
  • 解码提供两种解析方式:
    • 同步解码,这种情况下解码成功会返回解码结果。
    • 异步解码,解码过程中如果解码成功会调用用户提供的回调函数。

消息编码

static void msg_encoder_init(msg_parser_t *parser)
{
    parser->encode_buffer[0] = MSG_START;
    parser->encode_offset = 1;
}

static msg_status_def msg_encode_append(msg_parser_t *parser, uint8_t *data, uint8_t len)
{
    msg_status_def ret = MSG_ERR_NONE;

    if ((parser->encode_offset + len) < MSG_BUFFER_SIZE)
    {
        memcpy(parser->encode_buffer + parser->encode_offset, data, len);
        parser->encode_offset += len;
    }
    else
    {
        ret = MSG_ERR_ENCODE_OVERFLOW;
    }

    return ret;
}

static msg_status_def msg_encode_fihish(msg_parser_t *parser)
{
    msg_status_def ret = MSG_ERR_NONE;
    return ret;
}

static msg_status_def msg_encode_packet(msg_parser_t *parser, msg_packet_t *pack)
{
    msg_status_def ret = MSG_ERR_NONE;

    /* add start byte */
    msg_encoder_init(parser);
    /* add header*/
    (MSG_ERR_NONE == ret) ? (ret = msg_encode_append(parser, (uint8_t *)&pack->header, sizeof(msg_packet_header_t))) : (0);
    /* add payload*/
    (MSG_ERR_NONE == ret) ? (ret = msg_encode_append(parser, pack->payload, pack->header.data_length)) : (0);
    /* add tail*/
    (MSG_ERR_NONE == ret) ? (ret = msg_encode_append(parser, (uint8_t *)&pack->tail, sizeof(msg_packet_tail_t))) : (0);
    /* add end byte*/
    (MSG_ERR_NONE == ret) ? (ret = msg_encode_fihish(parser)) : (0);

    return ret;
}

msg_status_def msg_encode(msg_parser_t *parser, uint8_t *data, uint8_t len, uint8_t **out, uint8_t *out_len)
{
    msg_packet_t pack = {0};
    msg_status_def ret = MSG_ERR_NONE;

    if (!data || !len)
    {
        return MSG_ERR_ENCODE_INPUT_INVALID;
    }

    pack.header.seq_num = parser->seq++;
    pack.header.data_length = len;
    pack.payload = data;
    /* calculate crc */
    pack.tail.crc = crc8(0, (uint8_t *)&(pack.header), sizeof(msg_packet_header_t));
    pack.tail.crc = crc8(pack.tail.crc, pack.payload, pack.header.data_length);
    /* encode packet into buffer */
    ret = msg_encode_packet(parser, &pack);

    if (MSG_ERR_NONE == ret)
    {
        *out = parser->encode_buffer;
        *out_len = parser->encode_offset;
    }

    return ret;
}
  • 用户通过调用 msg_encode 首先会通过 msg_encoder_init 将帧起始标志放入第一个字节,然后根据数据包格式将数据放入缓存 parser->encode_buffer 中,编码成功会将编码结果的首地址和编码后的数据长度返回。
  • 如果编码成功返回 MSG_ERR_NONE,数据过长则会返回 MSG_ERR_ENCODE_OVERFLOW。
  • 用户可将编码结果通过总线发出。

消息解码

void msg_decoder_init(msg_parser_t *parser)
{
    parser->remain = sizeof(msg_packet_header_t);
    parser->recved_start = 0;
    parser->recved_header = 0;
    parser->decode_msg = NULL;
    parser->decode_overflow = 0;
}

static msg_packet_t *msg_decode_verify(msg_parser_t *parser)
{
    uint8_t crc = 0;

    if (parser->decode_msg)
    {
        if (!parser->decode_overflow)
        {
            crc = crc8(0, (uint8_t *)&(parser->decode_msg->header), sizeof(msg_packet_header_t));
            crc = crc8(crc, parser->decode_msg->payload, parser->decode_msg->header.data_length);

            if (parser->decode_msg->tail.crc != crc)
            {
                parser->decode_msg = NULL;
            }
        }
        else
        {
            parser->decode_msg = NULL;
        }
    }

    return parser->decode_msg;
}

static msg_packet_t *msg_decode_finish(msg_parser_t *parser)
{
    msg_packet_t *ret = NULL;
    msg_packet_t *temp = parser->decode_msg;

    if (parser->recved_header && !parser->remain)
    {
        temp->payload = parser->decode_buffer;
        temp->tail.crc = parser->decode_buffer[temp->header.data_length];
        ret = msg_decode_verify(parser);
        msg_decoder_init(parser);
    }

    return ret;
}

msg_packet_t *msg_decode_packet(msg_parser_t *parser, uint8_t *data, uint8_t *len)
{
    uint8_t crc = 0;
    msg_packet_t *ret = NULL;

    if ((NULL == data) || (0 == len))
    {
        return ret;
    }

    /* check if the decoder is initialized */
    if (!parser->decoder_init)
    {
        msg_decoder_init(parser);
        parser->decoder_init = 1;
    }

    /* detect start byte */
    if (!parser->recved_start)
    {
        uint8_t *pos = strchr(data, MSG_START);

        /* find the start byte in the data buffer */
        if (pos)
        {
            if (((unsigned long)pos - (unsigned long)data) <= *len)
            {
                /* behind the start byte is header */
                *len = *len - ((unsigned long)pos - (unsigned long)data) - 1;
                /* move the data pointer to the start of the header */
                data = pos + 1;
                parser->recved_start = 1;
            }
        }
        else
        {
            /* discard */
            *len = 0;
        }
    }

    if (parser->recved_start && data && (*len > 0))
    {
        if (parser->recved_header)
        {
            if (parser->decode_msg)
            {
                uint8_t min = (parser->remain < *len) ? (parser->remain) : (*len);
                uint8_t offset = parser->decode_msg->header.data_length + sizeof(msg_packet_tail_t) - parser->remain;

                /* check if there is overflow in the decode buffer */
                if (!parser->decode_overflow)
                {
                    /* copy the received data to the parser's buffer */
                    memcpy(parser->decode_buffer + offset, data, min);
                }

                *len = *len - parser->remain;
                parser->remain -= min;
            }

            ret = msg_decode_finish(parser);
        }
        else
        {
            uint8_t min = (parser->remain < *len) ? (parser->remain) : (*len);
            uint8_t offset = sizeof(msg_packet_header_t) - parser->remain;

            /* copy the received data to the parser's buffer */
            memcpy(parser->decode_buffer + offset, data, min);

            if (min >= parser->remain)
            {
                data += parser->remain;
                *len = *len - parser->remain;
                parser->recved_header = 1;

                 /* store the message header in the parser's cache and set the expected remaining length of the message */
                memcpy(&(parser->decode_cache), parser->decode_buffer, sizeof(msg_packet_header_t));
                parser->decode_msg = &parser->decode_cache;
                parser->remain = parser->decode_msg->header.data_length + sizeof(msg_packet_tail_t);

                if (parser->remain >= MSG_BUFFER_SIZE)
                {
                    // TODO: exception inform
                    parser->decode_overflow = 1;
                }

                /* recursively call the function to receive the message body and tail */
                ret = msg_decode_packet(parser, data, len);
            }
            else
            {
                *len = *len - parser->remain;
                parser->remain -= min;
            }
        }
    }

    return ret;
}

msg_t *msg_decode(msg_parser_t *parser, msg_t *msg, uint8_t *data, uint8_t *len)
{
    msg_packet_t *ret = NULL;

    ret = msg_decode_packet(parser, data, len);

    if (ret)
    {
        msg->data_length = ret->header.data_length;
        msg->payload = ret->payload;
    }

    return (ret) ? (msg) : (NULL);
}

void msg_decode_async(msg_parser_t *parser, uint8_t *data, uint8_t len)
{
    msg_packet_t *ret = NULL;

    while (len)
    {
        ret = msg_decode_packet(parser, data, &len);

        if (ret)
        {
            if (parser->decoder_cb.func)
            {
                parser->decoder_cb.func(parser->decoder_cb.user_data, ret->payload, ret->header.data_length);
            }
        }
    }
}

void msg_register_decode_async_handler(msg_parser_t *parser, msg_callback_t *cb)
{
    if (cb)
    {
        parser->decoder_cb = *cb;
    }
}
  • 同步解码:
    • msg_t *msg_decode(msg_parser_t *parser, msg_t *msg, uint8_t *data, uint8_t *len)
    • 解码成功会返回解码结果,等待用户处理,此时用户访问的数据位于缓冲区中。
    • 由于函数一次只能返回一个解析结果,且不能在返回之前进行第二次解析,传入的数据可能有剩余部分,msg_decode 会通过修改 len 的值告知剩余未解析的数据长度。
    • 用户需要判断该值是否为0,从而再次调用 msg_decode 进行第二次解析。
  • 异步解析:
    • 用户通过 msg_register_decode_async_handler 注册回调函数。
    • 每次成功解析一条数据会调用一次回调函数,回调函数返回会继续解析剩余数据,用户不可在回调函数执行完后继续使用缓冲区中的数据,如需在返回后继续使用应进行数据拷贝。

示例

#include <unistd.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include "msg_parser.h"

void decode_handler(void *param, uint8_t *data, int len)
{
    printf("msg_len:%d\r\n", len);

    for (int i = 0; i < len; i++)
    {
        if ((i % 10) == 0)
        {
            printf("\r\n");
        }

        printf("%02x ", data[i]);
    }
    printf("\r\n");
}

void main()
{
    uint8_t raw_data[120] = {0};
    msg_parser_t parser = {0};

    msg_t decode_msg = {0};
    uint8_t encode_len = 0;
    uint8_t *encode_data = NULL;
    msg_callback_t async_cb = {.func = decode_handler, .user_data = NULL};

    for (int i = 0; i < 120; i++)
    {
        raw_data[i] = i;
    }

    msg_register_decode_async_handler(&parser, &async_cb);
    msg_encode(&parser, raw_data, 120, &encode_data, &encode_len);

    /* 同步解析一次调用只能返回一个值,当一个数据包解析完成后,
       如果后面还存在下一帧的数据,只能由下一次调用来做解析,
       因为解析器一次只能解析一条数据,新的解析会覆盖旧的数据,
       在返回之后如果len不为0,则代表还有数据未解析完,需要进行第二次解析
     */
    while (encode_len)
    {
        if (msg_decode(&parser, &decode_msg, encode_data, &encode_len))
        {
            decode_handler(NULL, decode_msg.payload, decode_msg.data_length);
        }
    }

    /* 异步解析,每次解析完成后自动调用回调函数,如果发现还有未处理的数据
       会在回调函数处理完成之后进行第二次解析。
     */
    msg_encode(&parser, raw_data, 120, &encode_data, &encode_len);
    msg_decode_async(&parser, encode_data, encode_len);
}
  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕咚.萌西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值