扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

嵌入式设备间通信,高效可靠的字节流通信协议

7小时前
382
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!

对于资源有限的单片机设备之间的通信,我们通常用字节流的方式,因此经常会面临数据帧粘包和丢包的问题,以及数据域里面含有特殊字节(如帧头帧尾)的问题,经常容易导致数据值解析错误。

针对MCU硬件外设资源有限的特征,我们需要设计一套轻量级且容易解析的同学协议,通过多种机制(比如校验、重传、转义)来实现数据的高效可靠传输。

一、协议设计原则

这套通信协议应用在单片机场景下,需要兼顾“可靠性”与“轻量性”原则,核心主要解决三类问题:

(1)数据帧边界:通过固定的帧结构(帧头、帧尾)解决粘包问题;

(2)数据完整性:通过数据校验和数据重传的方式,解决丢包和错包的问题;

(3)特殊字节符:通过数据转义机制,避免数据域里面的帧头帧尾干扰数据解析。

二、数据帧结构

协议数据帧主要采用以下结构:“帧头+原始长度+转义数据+校验码+帧尾”,同时兼顾数据帧的边界识别与特殊的字节兼容。

帧头 原始长度 转义数据域 校验码 帧尾
0xAA 1 N 1 0x55

协议各字段说明:

1、帧头+帧尾:固定的数据帧边界,用来快速定位数据帧的起止位置,解决粘包问题。

2、原始长度:用来记录未转义的业务数据字节数(0~255),用于验证数据还原后的完整性。

3、转义数据域:对含有特殊字节的原始数据进行转义后的业务数据,避免与帧头帧尾冲突。

4、校验码:原始数据的异或校验结果,确保数据完整性,用来识别错包/丢包。

核心机制详解:

1、转义机制主要用来解决特殊字节冲突的问题,定义转义字节0x7D、转义异或值0x08,规则如下:

(1)发送端转义,数据域中出现0xAA(帧头),替换为0x7D+0xAB;出现0x55(帧尾),替换为0x7D+0x54;出现0x7D(转义字节),替换为0x7D+0x7C;

(2)接收端还原,遇到0x7D时,读取下一个字节并异或0x08,还原为原始字节。

2、粘包解决机制,接收端通过逐个字节扫描的方式,匹配帧头后读取“原始数据长度”的方式,按照长度校验转义还原后的数据,仅当帧头、长度、帧尾、校验码都匹配的时候,才判定为有效包,从而避免多包粘连。

3、丢包解决机制,接收端校验码不匹配,数据还原失败的时候,向发送端回复“重传指令”,发送端维护数据重传次数(通常是3次),收到指令后重新发送对应的数据包,从而平衡数据可靠性和硬件资源的占用。

三、示例代码

提供一份参考代码,实现该通信协议的封装、发送、接收、解析,覆盖了粘包、丢包、特殊字节的冲突问题。

#include "stdint.h"#include "string.h"
// 协议核心定义#define FRAME_HEAD    0xAA    // 帧头#define FRAME_TAIL    0x55    // 帧尾#define ESCAPE_BYTE   0x7D    // 转义字节#define ESCAPE_XOR    0x08    // 转义异或值#define MAX_DATA_LEN  255     // 最大原始数据长度#define MAX_FRAME_LEN 512     // 转义后最大帧长度#define MAX_RETRY_CNT 3       // 最大重传次数
// 数据包结构体typedef struct {    uint8_t data_len;          // 原始数据长度    uint8_t data[MAX_DATA_LEN];// 原始业务数据    uint8_t check_sum;         // 原始数据校验码} Packet_t;
// 接收全局变量uint8_t recv_buf[MAX_FRAME_LEN] = {0}; // 接收缓冲区uint8_t recv_state = 0;                // 0:未匹配帧头 1:已匹配帧头 2:解析完成uint8_t recv_idx = 0;                  // 接收缓冲区索引uint8_t is_escape = 0;                 // 转义标记
// 计算原始数据异或校验码uint8_t calc_check_sum(uint8_t *data, uint8_t len) {    uint8_t check_sum = 0;    for (uint8_t i = 0; i < len; i++) check_sum ^= data[i];    return check_sum;}
// 发送端:数据转义uint8_t escape_data(uint8_t *src, uint8_t src_len, uint8_t *dst, uint8_t *dst_len) {    if (src_len == 0 || src_len > MAX_DATA_LEN) return 0;    uint8_t idx = 0;    for (uint8_t i = 0; i < src_len; i++) {        if (idx >= MAX_FRAME_LEN - 4) return 0;        if (src[i] == FRAME_HEAD || src[i] == FRAME_TAIL || src[i] == ESCAPE_BYTE) {            dst[idx++] = ESCAPE_BYTE;            dst[idx++] = src[i] ^ ESCAPE_XOR;        } else {            dst[idx++] = src[i];        }    }    *dst_len = idx;    return 1;}
// 接收端:数据还原uint8_t unescape_data(uint8_t *src, uint8_t src_len, uint8_t *dst, uint8_t *dst_len) {    if (src_len == 0) return 0;    uint8_t idx = 0, escape_flag = 0;    for (uint8_t i = 0; i < src_len; i++) {        if (escape_flag) {            dst[idx++] = src[i] ^ ESCAPE_XOR;            escape_flag = 0;        } else if (src[i] == ESCAPE_BYTE) {            escape_flag = 1;        } else {            dst[idx++] = src[i];        }    }    if (escape_flag) return 0;    *dst_len = idx;    return 1;}// 模拟ACK收发(实际替换为硬件接口)uint8_t recv_ack(void) { return 0; }void send_ack(uint8_t ack) { uint8_t ack_byte = ack; }
// 发送端:封装并发送数据包(含转义+重传)uint8_t send_packet(uint8_t *data, uint8_t len) {    if (len > MAX_DATA_LEN) return 0;
    Packet_t pkt;    pkt.data_len = len;    memcpy(pkt.data, data, len);    pkt.check_sum = calc_check_sum(data, len);
    // 数据转义    uint8_t escaped_data[MAX_FRAME_LEN] = {0};    uint8_t escaped_len = 0;    if (!escape_data(pkt.data, pkt.data_len, escaped_data, &escaped_len)) return 0;
    // 组装帧    uint8_t frame[MAX_FRAME_LEN] = {0}, frame_idx = 0;    frame[frame_idx++] = FRAME_HEAD;    frame[frame_idx++] = pkt.data_len;    memcpy(&frame[frame_idx], escaped_data, escaped_len);    frame_idx += escaped_len;    frame[frame_idx++] = pkt.check_sum;    frame[frame_idx++] = FRAME_TAIL;
    // 重传逻辑    uint8_t retry_cnt = 0;    while (retry_cnt < MAX_RETRY_CNT) {        // HAL_UART_Transmit(&huart1, frame, frame_idx, 100); // 替换为实际发送接口        if (recv_ack() == 0) return 1;        retry_cnt++;    }    return 0;}
// 接收端:逐字节解析(含还原+粘包处理)void recv_byte_handler(uint8_t byte) {    switch (recv_state) {        case 0: // 匹配帧头            if (byte == FRAME_HEAD) {                recv_state = 1;                recv_buf[recv_idx++] = byte;                is_escape = 0;            }            break;        case 1: // 接收数据并处理转义            if (is_escape) {                recv_buf[recv_idx++] = byte ^ ESCAPE_XOR;                is_escape = 0;            } else if (byte == ESCAPE_BYTE) {                is_escape = 1;            } else {                recv_buf[recv_idx++] = byte;            }
            // 校验帧尾并解析            if (recv_idx >= 4) {                uint8_t origin_len = recv_buf[1];                if (recv_buf[recv_idx - 1] == FRAME_TAIL) {                    // 还原数据并校验                    uint8_t escaped_len = recv_idx - 4;                    uint8_t origin_data[MAX_DATA_LEN] = {0}, real_len = 0;                    if (unescape_data(&recv_buf[2], escaped_len, origin_data, &real_len)                         && real_len == origin_len) {                        uint8_t calc_check = calc_check_sum(origin_data, real_len);                        if (calc_check == recv_buf[2 + escaped_len]) {                            recv_state = 2;                            send_ack(0); // 解析成功,发送确认                        } else {                            send_ack(1); // 校验失败,请求重传                        }                    } else {                        send_ack(1); // 还原失败,请求重传                    }                } else {                    send_ack(1); // 帧尾错误,请求重传                }                // 重置状态                recv_state = 0;                recv_idx = 0;                is_escape = 0;                memset(recv_buf, 0, sizeof(recv_buf));            }            break;        default: // 异常重置            recv_state = 0;            recv_idx = 0;            is_escape = 0;            memset(recv_buf, 0, sizeof(recv_buf));            break;    }}
// 提取解析成功的数据包uint8_t get_packet(uint8_t *out_data, uint8_t *out_len) {    if (recv_state == 2) {        uint8_t origin_len = recv_buf[1];        uint8_t escaped_len = recv_idx - 4;        unescape_data(&recv_buf[2], escaped_len, out_data, out_len);        recv_state = 0;        return 1;    }    return 0;}

四、协议优势

该通信协议的优势如下:

(1)轻量高效:转义和校验均为简单的bit位运算,协议的解析没有复杂的计算逻辑,适配8位/32位单片机。

(2)兼容性强:转义机制解决了数据域特殊字节的冲突,帧结构保留了原始的长度字段,确保协议解析的时候无歧义。

(3)可靠性高:结合帧头帧尾的边界识别(解决粘包问题),校验+重传(解决丢包问题),提高协议通信的可靠性。

(4)可扩展:可以在数据域增加数据包的序号,从而适配多包连续传输的场景。

五、总结

(1)MCU字节流通信协议帧的核心结构是“帧头+原始长度+校验码+帧尾”的基础结构,附加转义机制来解决特殊字节之间的冲突。

(2)通过“帧头帧尾+校验码”来解决粘包问题;通过“有限重传+校验码”来解决丢包问题;通过“转义+还原”来解决特殊字节冲突问题。

(3)协议设计需要适配MCU的硬件资源特性,优先选择轻量级算法(比如异或校验),避免过度消耗单片机有限的算力和内存资源。

 

相关推荐