0. 引言

在嵌入式系统中,串口通信常见但易出现粘包问题难。本文分析了粘包的原因,并通过代码示例介绍了在数据包中添加头部标识符、数据长度、消息类型和校验码的方法,来有效解决串口粘包问题

1. 什么是粘包问题?

粘包问题指在串口通信中,多个独立的数据包被接收端视为一个连续的数据流,导致数据包边界不明确,解析困难。其原因包括:

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

2. 处理粘包问题的思路

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

  • 使用特殊分隔符:在数据包开始或结束添加特殊字符,标记边界。
  • 固定数据包长度:确保每个数据包长度一致,通过固定长度解析数据包。
  • 设计协议:在数据包中包含长度和校验信息,确保接收端准确解析和校验数据包。

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

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

3. 实现方案

以下方案通过在数据包中添加头部标识符和长度信息,确保接收端正确解析数据包。

3.1 数据包格式

假设数据包格式如下:

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

3.2 代码实现

以下代码展示了接收端处理粘包问题的方法:

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

#define UART_HEADER_FIRST_CHAR 0xAA
#define UART_HEADER_SECOND_CHAR 0xBB
#define MAX_DATA_LENGTH 252

typedef struct {
  uint8_t header[2];              // 头部标识符
  uint8_t len;                    // 数据长度
  uint8_t type;                   // 消息类型
  uint8_t data[MAX_DATA_LENGTH];  // 数据内容
  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;

  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: %s\n", strerror(errno));
    return -1;
  } else if (ret == 0) {
    // 超时
    return 0;
  }

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

// 计算CRC校验码
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;
  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;

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

  // 读取数据内容
  if (frame->len > 1) {
    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 += bytesRead;

  // 校验数据包完整性
  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("处理类型 0x01 的消息\n");
      break;
    case 0x02:
      printf("处理类型 0x02 的消息\n");
      break;
    default:
      printf("未知消息类型: 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("接收 %d 字节: ", len);
    for (int i = 0; i < len; i++) {
      printf("%02X ", ((uint8_t *)&frame)[i]);
    }
    printf("\n");

    // 处理数据包
    process_frame(&frame);
  } else {
    printf("未接收到数据或发生错误。\n");
  }
  return 0;
}

3.3 流程图

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

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐