当前位置:   article > 正文

嵌入式系统中串口通信粘包问题的解决方案(C语言)

嵌入式系统中串口通信粘包问题的解决方案(C语言)

0. 引言

在嵌入式系统中,串口通信是一种常见且重要的数据传输方式。然而,由于硬件和软件的限制,串口通信过程中常会出现数据包粘连(即粘包)问题。这种问题会导致接收端无法正确解析数据。本文将介绍粘包问题的成因,结合实际代码示例,详细说明如何实现解决方案。

1. 什么是粘包问题?

粘包问题是指在串口通信中,多个独立的数据包在传输过程中被接收端视为一个连续的数据流,导致数据包之间的边界不明确,从而使解析过程变得困难:

  • 发送端数据发送频率高,接收端处理速度较慢。
  • 数据包长度变化较大,接收端难以准确确定数据包边界。
  • 硬件限制导致的数据包边界模糊。

2. 粘包问题的影响

  1. 数据丢失:由于粘包问题,接收端可能会错误地丢弃某些数据包,导致数据丢失。
  2. 数据错误:粘包可能导致数据包被错误解析,从而导致数据内容出现错误。
  3. 数据重传:为了确保数据完整性,系统可能需要重传数据包,增加了通信负担。

3. 处理粘包问题的思路

为了解决粘包问题,可以采取以下措施:

  1. 使用特殊分隔符:在每个数据包的开始或结束添加特殊字符,明确标记数据包的边界。
  2. 固定数据包长度:确保每个数据包的长度一致,使接收端可以通过固定长度来解析数据包。
  3. 设计协议:在数据包中包含长度信息和校验信息,以确保接收端能够准确解析和校验数据包的完整性。

4. 不同处理方法的优缺点分析

方法优点缺点
特殊分隔符实现简单,易于检测数据边界需要处理转义字符,性能略有影响
固定数据包长度简单高效,解析速度快不适用于变长数据,浪费带宽
设计协议灵活性高,适应多种数据格式实现复杂度高,需要额外开销

5. 实现方案

以下是一个具体的实现方案,通过在数据包中添加头部标识符和长度信息,确保接收端能够正确解析数据包。

5.1 数据包格式

我们假设数据包的格式如下:

  • 头部标识符(2字节):用于标识数据包的开始。
  • 数据长度(1字节):表示数据包的长度。
  • 消息类型(1字节):表示数据包的类型。
  • 数据内容(可变长度):实际的数据内容。
  • 校验码(1字节):用于校验数据包的完整性。

5.2 代码实现

以下代码展示了如何在接收端处理粘包问题:

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <time.h>

static const uint8_t UART_HEADER_FIRST_CHAR = 0xAA;
static const uint8_t UART_HEADER_SECOND_CHAR = 0xBB;

typedef struct {
  uint8_t header[2];    // 头部标识符
  uint8_t len;          // 数据长度
  uint8_t type;         // 消息类型
  uint8_t data[252];    // 数据内容
  uint8_t crc;          // 校验码
} uart_frame_t;

static int uart_read(int fd, uint8_t *buff, int len, int timeout_us) {
  int ret;
  struct timeval tv;
  fd_set rfds;

  memset(buff, 0, len);
  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);
  tv.tv_sec = timeout_us / 1000000;
  tv.tv_usec = timeout_us % 1000000;

  ret = select(fd + 1, &rfds, NULL, NULL, &tv);

  if (ret == -1) {
    printf("select() failed. ret %d, %s\n", ret, strerror(errno));
    return -1;
  } else if (ret == 0) {
    printf("select() timed out.\n");
    return 0;
  }

  if (FD_ISSET(fd, &rfds)) {
    ssize_t bytesRead = read(fd, buff, len);
    if (bytesRead == -1) {
      printf("read() failed. errno %d, %s\n", errno, strerror(errno));
      return -1;
    } else if (bytesRead == 0) {
      return 0;
    }
    return bytesRead;
  } else {
    return 0;
  }
}

static uint8_t calculate_crc(const uint8_t *data, int len) {
  uint8_t crc = 0;
  for (int i = 0; i < len; i++) {
    crc ^= data[i];
  }
  return crc;
}

static int recv_frame(int fd, uart_frame_t *frame, int timeout_us) {
    int bytesRead = 0;
    int totalBytes = 0;

    // 等待头部标识符
    while (1) {
        bytesRead = uart_read(fd, frame->header, 2, timeout_us);
        if (bytesRead == 2 && frame->header[0] == UART_HEADER_FIRST_CHAR && frame->header[1] == UART_HEADER_SECOND_CHAR) {
            break;  // 找到头部标识符
        }
    }

    // 读取数据长度
    bytesRead = uart_read(fd, &frame->len, 1, timeout_us);
    if (bytesRead != 1) {
        return -1;  // 读取长度失败
    }
    totalBytes++;

    // 读取消息类型
    bytesRead = uart_read(fd, &frame->type, 1, timeout_us);
    if (bytesRead != 1) {
        return -1;  // 读取消息类型失败
    }
    totalBytes++;

    // 读取数据内容和校验码
    bytesRead = uart_read(fd, frame->data, frame->len - 1, timeout_us);
    if (bytesRead != frame->len - 1) {
        return -1;  // 读取数据内容失败
    }
    totalBytes += bytesRead;

    // 读取校验码
    bytesRead = uart_read(fd, &frame->crc, 1, timeout_us);
    if (bytesRead != 1) {
        return -1;  // 读取校验码失败
    }
    totalBytes++;

    // 校验数据包完整性
    if (frame->crc != calculate_crc((uint8_t*)frame, totalBytes - 1)) {
        return -1;  // 校验失败
    }

    return totalBytes;  // 返回实际读取的字节数
}

// 处理不同类型的消息
static void process_frame(const uart_frame_t *frame) {
    switch (frame->type) {
        case 0x01:
            printf("Processing type 0x01 message\n");
            // 处理类型为 0x01 的消息
            break;
        case 0x02:
            printf("Processing type 0x02 message\n");
            // 处理类型为 0x02 的消息
            break;
        default:
            printf("Unknown message type: 0x%02X\n", frame->type);
            break;
    }
}

int main(void) {
    int fd = 0; // 假设fd是已经打开的串口文件描述符
    uart_frame_t frame;
    int len = recv_frame(fd, &frame, 1000000);
    if (len > 0) {
        printf("Received %d bytes: ", len);
        for (int i = 0; i < len; i++) {
            printf("%02X ", ((uint8_t*)&frame)[i]);
        }
        printf("\n");

        // 处理接收到的数据包
        process_frame(&frame);
    } else {
        printf("No data received or an error occurred.\n");
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145

5.3 流程图

以下是更新后的流程图,展示了更复杂的接收端处理逻辑,包括根据消息类型进行不同的处理。

类型0x01
类型0x02
其他类型
开始
等待头部标识符
头部标识符匹配?
读取数据长度
读取成功?
读取消息类型
读取成功?
读取数据内容和校验码
读取成功?
校验数据包
校验通过?
处理数据
消息类型?
处理类型0x01的消息
处理类型0x02的消息
处理未知类型的消息

6. 结论

本文通过引入头部标识符、数据长度、消息类型和校验码,展示了如何在接收端处理串口粘包问题。我们详细介绍了接收端的实现过程,并通过更新的流程图展示了接收端的处理逻辑。通过这种方式,我们可以根据消息类型执行不同的操作,从而有效解决串口通信中的粘包问题。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/766006
推荐阅读
  

闽ICP备14008679号