基于串口的环形缓冲区与命令解析
传统简单的数据回发:成功接收到一包数据,触发进入中断,在中断函数中将数据读取出来,然后立即回发。这可能会导致严重的问题:数据无缓冲区,如果先前接收的的数据如果尚未发送完成(处理完成),然后串口又接收到新的数据,新接收的数据就会把尚未处理的数据覆盖,从而导致“数据丢包”。
缓冲区:当接收到数据时,把这个数据拷贝到大的数组,这个数组即称为缓冲区
环形缓冲区:就是一个带“头指针”和“尾指针”的数组。“头指针”指向环形缓冲区中可读的数据,“尾指针”指向环形缓冲区中可写的缓冲空间。通过移动“头指针”和“尾指针”就可以实现缓冲区的数据读取和写入。在通常情况下,应用程序读取环形缓冲区的数据仅仅会影响“头指针”,而串口接收数据仅仅会影响“尾指针”。当串口接收到新的数组,则将数组保存到环形缓冲区中,同时将“尾指针”加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
即能在串口接收到正确的数据
即使是发生黏包,拆包,依然能接收到正确的数据


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

更多推荐

所有评论(0)