找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2586

积分

0

好友

350

主题
发表于 3 小时前 | 查看: 1| 回复: 0

在资源有限的单片机设备间进行数据交换时,我们通常采用最基础的字节流方式通信。这种方式直接、简单,但也带来了几个典型的痛点:数据帧的“粘包”和“丢包”问题如何处理?数据域中如果恰好包含了与帧头、帧尾相同的特殊字节,又该如何避免解析错误?

为了应对MCU硬件资源(如RAM、ROM、算力)有限的特点,我们需要设计一套足够轻量且易于解析的通信协议。它需要通过校验、重传、转义等多种机制,在有限的资源开销下实现高效且可靠的数据传输。

FET3562开发板

一、协议设计原则

这套专为单片机场景设计的通信协议,核心在于平衡“可靠性”与“轻量性”,主要解决以下三类问题:

  1. 数据帧边界:通过固定的帧结构(帧头、帧尾)来解决粘包问题。
  2. 数据完整性:通过数据校验和数据重传机制,来解决丢包和错包问题。
  3. 特殊字节符:通过数据转义机制,避免数据域中的有效数据与帧头帧尾等控制字符冲突,保证解析无误。

二、数据帧结构

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

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

协议各字段说明:

  • 帧头 + 帧尾:固定的数据帧边界,用于快速定位数据帧的起始和结束位置,是解决粘包的基础。
  • 原始长度:记录未转义前的业务数据字节数(0~255),用于验证数据还原后的完整性。
  • 转义数据域:对含有特殊字节的原始业务数据进行转义处理后的数据,避免其与帧头帧尾冲突。
  • 校验码:原始数据的异或(XOR)校验结果,用于确保数据完整性,识别错包或丢包。

核心机制详解:

  1. 转义机制
    为解决特殊字节冲突,定义转义字节为 0x7D,转义异或值为 0x08

    • 发送端(转义):在数据域中,若出现 0xAA(帧头),则替换为 0x7D, 0xAB;若出现 0x55(帧尾),则替换为 0x7D, 0x54;若出现 0x7D(转义字节本身),则替换为 0x7D, 0x7C
    • 接收端(还原):遇到 0x7D 时,读取下一个字节并与 0x08 进行异或运算,即可还原为原始字节。
  2. 粘包解决机制
    接收端通过逐个字节扫描来匹配帧头,匹配成功后读取“原始数据长度”字段。随后,按照该长度校验转义还原后的数据。只有当帧头、长度、帧尾、校验码全部匹配时,才判定为一个有效数据包,从而有效避免多个包粘连在一起的情况。

  3. 丢包解决机制
    当接收端发现校验码不匹配或数据还原失败时,会向发送端回复“重传指令”。发送端维护一个数据重传计数器(通常设为3次),收到重传指令后,会重新发送对应的数据包。这种机制在保证数据可靠性的同时,也兼顾了硬件资源的有限性。

三、示例代码

以下是一份参考性的C语言实现代码,涵盖了该通信协议的封装、发送、接收与解析,完整应对了粘包、丢包及特殊字节冲突问题。

#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. 轻量高效:转义和校验均采用简单的位运算,解析逻辑不涉及复杂计算,完美适配8位或32位单片机。
  2. 兼容性强:转义机制彻底解决了数据域内特殊字节的冲突问题,帧结构中保留的原始长度字段确保了协议解析时无二义性。
  3. 可靠性高:结合帧头帧尾的边界识别(解决粘包)、校验与重传机制(解决丢包),显著提升了通信过程的可靠性。
  4. 可扩展性好:可在数据域中方便地加入数据包序号等字段,轻松适配需要连续传输多个数据包的场景。

五、总结

回顾整个设计,我们可以提炼出几个关键点:

  1. MCU字节流通信协议帧的核心是“帧头+原始长度+校验码+帧尾”的基础结构,并通过附加的转义机制来解决特殊字节冲突。
  2. 通过“帧头帧尾+长度校验”解决粘包问题;通过“有限次重传+校验码”解决丢包问题;通过“转义与还原”解决特殊字节冲突问题。
  3. 协议设计必须时刻考虑MCU的硬件资源特性,优先选择异或校验这类轻量级算法,避免过度消耗单片机有限的算力和内存。

希望这份关于轻量级嵌入式通信协议的设计与实现详解,能为你带来启发。更多嵌入式开发、系统设计相关的深度讨论和资源分享,欢迎在云栈社区与广大开发者一同交流。




上一篇:DidiWikiwiki:用25KB C代码构建零依赖的个人Wiki系统
下一篇:Oracle性能调优:根治行链化,从PCTFREE与区大小设置入手
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-23 06:36 , Processed in 0.789050 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表