传统简单的数据回发:成功接收到一包数据,触发进入中断,在中断函数中将数据读取出来,然后立即回发。这可能会导致严重的问题:数据无缓冲区,如果先前接收的的数据如果尚未发送完成(处理完成),然后串口又接收到新的数据,新接收的数据就会把尚未处理的数据覆盖,从而导致“数据丢包”。

缓冲区:当接收到数据时,把这个数据拷贝到大的数组,这个数组即称为缓冲区

环形缓冲区:就是一个带“头指针”和“尾指针”的数组。“头指针”指向环形缓冲区中可读的数据,“尾指针”指向环形缓冲区中可写的缓冲空间。通过移动“头指针”和“尾指针”就可以实现缓冲区的数据读取和写入。在通常情况下,应用程序读取环形缓冲区的数据仅仅会影响“头指针”,而串口接收数据仅仅会影响“尾指针”。当串口接收到新的数组,则将数组保存到环形缓冲区中,同时将“尾指针”加1,以保存下一个数据;应用程序在读取数据时,“头指针”加1,以读取下一个数据。当“尾指针”超过数组大小,则“尾指针”重新指向数组的首元素,从而形成“环形缓冲区”。初始状态头指针和尾指针重叠表示空,当尾指针的下一个位置等于头指针时,表示缓冲区已满。

我们假设数据帧格式为

  • 包头0xAA
    固定值,用来表示一条指令的开始。

  • 包长度0x06
    表示整条指令的字节数(包含包头、长度本身、数据、校验和)。
    这里长度是 6 字节。

  • 数据0x01 0xFA 0x1B
    这就是你传输的实际有效数据。

  • 校验和0xC6
    用来校验这条指令是否完整、无错误。采用逐字节累加和0xAA + 0x06 + 0x01 + 0xFA + 0x1B = 0x01C6,取最后一字节作为校验和,即0xC6。

下面是一个环形缓冲区.c和.h文件示例

下面是.c文件

#include "command.h"

// 指令的最小长度
#define COMMAND_MIN_LENGTH 4

// 循环缓冲区大小
#define BUFFER_SIZE 128
// 循环缓冲区
uint8_t buffer[BUFFER_SIZE];
// 循环缓冲区读索引
uint8_t readIndex = 0;
// 循环缓冲区写索引
uint8_t writeIndex = 0;

/**
* @brief 增加读索引
* @param length 要增加的长度
*/
void Command_AddReadIndex(uint8_t length) {
    readIndex += length;
    readIndex %= BUFFER_SIZE;
}

/**
* @brief 读取第i位数据 超过缓存区长度自动循环
* @param i 要读取的数据索引
*/

uint8_t Command_Read(uint8_t i) {
    uint8_t index = i % BUFFER_SIZE;
    return buffer[index];
}

//下面代码被注释是因为有更简单写法
/**
* @brief 计算未处理的数据长度
* @return 未处理的数据长度
* @retval 0 缓冲区为空
* @retval 1~BUFFER_SIZE-1 未处理的数据长度
* @retval BUFFER_SIZE 缓冲区已满
*/
//uint8_t Command_GetLength() {
//  // 读索引等于写索引时,缓冲区为空
//  if (readIndex == writeIndex) {
//    return 0;
//  }
//  // 如果缓冲区已满,返回BUFFER_SIZE
//  if (writeIndex + 1 == readIndex || (writeIndex == BUFFER_SIZE - 1 && readIndex == 0)) {
//    return BUFFER_SIZE;
//  }
//  // 如果缓冲区未满,返回未处理的数据长度
//  if (readIndex <= writeIndex) {
//    return writeIndex - readIndex;
//  } else {
//    return BUFFER_SIZE - readIndex + writeIndex;
//  }
//}

uint8_t Command_GetLength() {
    return (writeIndex + BUFFER_SIZE - readIndex) % BUFFER_SIZE;
}


/**
* @brief 计算缓冲区剩余空间
* @return 剩余空间
* @retval 0 缓冲区已满
* @retval 1~BUFFER_SIZE-1 剩余空间
* @retval BUFFER_SIZE 缓冲区为空
*/
uint8_t Command_GetRemain() {
    return BUFFER_SIZE - Command_GetLength();
}

/**
* @brief 向缓冲区写入数据
* @param data 要写入的数据指针
* @param length 要写入的数据长度
* @return 写入的数据长度
*/
uint8_t Command_Write(uint8_t *data, uint8_t length) {
    // 如果缓冲区不足 则不写入数据 返回0
    if (Command_GetRemain() < length) {
        return 0;
    }
    // 使用memcpy函数将数据写入缓冲区
    if (writeIndex + length < BUFFER_SIZE) {
        memcpy(buffer + writeIndex, data, length);
        writeIndex += length;
    } else {
        uint8_t firstLength = BUFFER_SIZE - writeIndex;
        memcpy(buffer + writeIndex, data, firstLength);
        memcpy(buffer, data + firstLength, length - firstLength);
        writeIndex = length - firstLength;
    }
    return length;
}

/**
* @brief 尝试获取一条指令
* @param command 指令存放指针
* @return 获取的指令长度
* @retval 0 没有获取到指令
*/
uint8_t Command_GetCommand(uint8_t *command) {
    // 寻找完整指令
    while (1) {
        // 如果缓冲区长度小于COMMAND_MIN_LENGTH 则不可能有完整的指令
        if (Command_GetLength() < COMMAND_MIN_LENGTH) {
        return 0;
        }
        // 如果不是包头 则跳过 重新开始寻找
        if (Command_Read(readIndex) != 0xAA) {
        Command_AddReadIndex(1);
        continue;
        }
        // 如果缓冲区长度小于指令长度 则不可能有完整的指令
        uint8_t length = Command_Read(readIndex + 1);
        if (Command_GetLength() < length) {
        return 0;
        }
        // 如果校验和不正确 则跳过 重新开始寻找
        uint8_t sum = 0;
        for (uint8_t i = 0; i < length - 1; i++) {
        sum += Command_Read(readIndex + i);
        }
        if (sum != Command_Read(readIndex + length - 1)) {
        Command_AddReadIndex(1);
        continue;
        }
        // 如果找到完整指令 则将指令写入command 返回指令长度
        for (uint8_t i = 0; i < length; i++) {
        command[i] = Command_Read(readIndex + i);
        }
        Command_AddReadIndex(length);
        return length;
    }
}

上面代码当数据处理速度慢,发送速度太快,缓冲区太小可能导致数据写不下,丢弃该帧数据,可考虑增大缓冲区,减慢发送方速度,加快数据处理速度。

下面是.h文件

#ifndef INC_COMMAND_H_
#define INC_COMMAND_H_

#include "main.h"
#include <string.h>

uint8_t Command_Write(uint8_t *data, uint8_t length);

uint8_t Command_GetCommand(uint8_t *command);

#endif /* INC_COMMAND_H_ */

这里供外部使用的函数只有

uint8_t Command_Write(uint8_t *data, uint8_t length);

uint8_t Command_GetCommand(uint8_t *command);

串口使用示例

使用串口接收空闲中断回调函数

//环形缓冲区
uint8_t readBuffer[10];

/* USER CODE BEGIN 0 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
	if (huart == &huart2){
		Command_Write(readBuffer, Size);//数据被放入缓冲区readBuffer
		HAL_UARTEx_ReceiveToIdle_IT(&huart2, readBuffer, sizeof(readBuffer));
	}
}
/* USER CODE END 0 */

main函数代码

HAL_UARTEx_ReceiveToIdle_IT(&huart2, readBuffer, sizeof(readBuffer));
uint8_t command[50];
int commandLength = 0;


/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
    commandLength = Command_GetCommand(command);//将环形缓冲区的数据读出到command数组
    if (commandLength != 0){
        HAL_UART_Transmit(&huart2, command, commandLength, HAL_MAX_DELAY);
    }
    HAL_Delay(10);
}

使用串口助手发送正确格式的包

AA 05 01 01 B1

即能在串口接收到正确的数据

即使是发生黏包,拆包,依然能接收到正确的数据

发送超过缓冲区大小的数据,依然能正确接收

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐