在开发单片机的过程中,我们经常使用到串口用来与其他mcu进行通信的情况,有时候需要处理特殊的串口消息,如固定的帧头,帧尾,还有校验位的数据,通常这种情况可能还是不固定的。但是有个至少的长度,例如6位或者7位,或者更多,这种情况时候用这种环形缓存的问题来解决,通常情况下可以使能串口的空闲中断加上dma方式的接收处理,这样使得串口的接收更加高效。
话不多说,直接上代码:
cpp
/**
* @file uart1_data_service.c
* @brief 串口1数据接收与帧解析处理模块(含环形缓冲区读写操作及初始化)
* @version 1.3
* @date 2026-06-29
*
* 协议格式(固定帧头 4 字节):
* [0] 0x55
* [1] 0xAA
* [2] 0x03
* [3] 0x00
* [4] 命令/类型(固定,本代码未使用)
* [5] 数据长度(1字节,表示后续数据字节数)
* [6] ... 数据(长度由[5]决定)
* [最后] 校验和(前面所有字节的累加和,不含校验本身)
*
* 总帧长 = 7(固定开销:帧头4 + 命令1 + 长度1) + 数据长度 + 1(校验)
* = 8 + 数据长度
*/
#include <string.h> // 若使用标准 memcpy 可包含
#include <stdint.h> // 标准整数类型
/*--------------------------- 类型定义(与用户代码兼容) ---------------------------*/
typedef unsigned char u8;
typedef unsigned short u16;
/*--------------------------- 宏定义(需根据实际协议调整) ---------------------------*/
#define HEAD_FIRST 0x55 // 帧头第1字节
#define HEAD_SECOND 0xAA // 帧头第2字节
#define PROTOCOL_VERSION 0x03 // 帧头第3字节(固定版本)
#define FRAME_TYPE 0x00 // 帧头第4字节(固定类型)
#define LEN_INDEX 5 // 数据长度字段偏移(从帧头开始)
#define FIXED_OVERHEAD 7 // 固定开销(不含数据、不含校验): 帧头4 + 命令1 + 长度1
#define CHECKSUM_LEN 1 // 校验和字节数
#define MAX_FRAME_LEN 64 // 最大帧长(根据缓冲区大小设定)
/*--------------------------- 硬件环形缓冲区相关变量 ---------------------------*/
volatile u8 Uart_Drv_Buff[64]; // 硬件环形缓冲区
volatile u8 *rx1_buf_in; // 写入指针(由中断更新)
volatile u8 *rx1_buf_out; // 读取指针(由取数函数更新)
volatile u8 uart1_data_process_buf[64]; // 本地处理缓冲区
/*--------------------------- 外部函数声明(若使用标准库可替换) ---------------------------*/
// 若使用标准 memcpy,可注释掉下面两行并包含 <string.h>
extern void my_memcpy(u8 *dst, const u8 *src, u16 len); // 内存搬移(用户需实现)
/*--------------------------- 环形缓冲区初始化函数 ---------------------------*/
/**
* @brief 初始化串口接收环形缓冲区,将读写指针复位到缓冲区起始
* @note 应在系统启动时调用一次
*/
void uart1_rx_buffer_init(void)
{
rx1_buf_in = (u8 *)Uart_Drv_Buff;
rx1_buf_out = (u8 *)Uart_Drv_Buff;
}
/*--------------------------- 环形缓冲区写入函数(供中断调用) ---------------------------*/
/**
* @brief 串口接收中断写入函数(将接收到的字节存入环形缓冲区)
* @param value 接收到的字节值
* @note 该函数应在 UART 接收中断服务程序中调用
* 当缓冲区满时,新数据被丢弃(可根据需要添加错误计数)
*/
void uart1_receive_input(u8 value)
{
// 判断缓冲区是否已满
if (rx1_buf_out == rx1_buf_in + 1) {
// 情况1:写指针紧跟读指针之后(缓冲区满)
// 缓冲区已满,丢弃该字节(可根据需求增加溢出计数)
// 例如: rx1_overflow_count++;
} else if ((rx1_buf_in > rx1_buf_out) &&
((rx1_buf_in - rx1_buf_out) >= sizeof(Uart_Drv_Buff))) {
// 情况2:写指针在读指针之后且差值达到缓冲区大小(缓冲区满)
// 缓冲区已满,丢弃该字节
// 例如: rx1_overflow_count++;
} else {
// 缓冲区未满,写入数据
if (rx1_buf_in >= (u8 *)(Uart_Drv_Buff + sizeof(Uart_Drv_Buff))) {
// 写指针到达缓冲区末尾,回绕到开头
rx1_buf_in = (u8 *)(Uart_Drv_Buff);
}
*rx1_buf_in++ = value;
}
}
/*--------------------------- 环形缓冲区读取函数(供处理层调用) ---------------------------*/
/**
* @brief 判断环形缓冲区是否有未读数据
* @return 1:有数据, 0:无数据
*/
u8 GetMcuUartByte(void)
{
if (rx1_buf_out != rx1_buf_in)
return 1;
else
return 0;
}
/**
* @brief 从环形缓冲区取一个字节(并移动读指针)
* @return 读取的字节值(若缓冲区为空则返回 0,但正常调用前应先用 GetMcuUartByte 判断)
*/
u8 take_byte_rx1buff(void)
{
u8 value = 0;
if (rx1_buf_out != rx1_buf_in) {
// 有数据
if (rx1_buf_out >= (u8 *)(Uart_Drv_Buff + sizeof(Uart_Drv_Buff))) {
// 数据已经到末尾,回绕到开头
rx1_buf_out = (u8 *)(Uart_Drv_Buff);
}
value = *rx1_buf_out++;
}
return value;
}
/*--------------------------- 辅助函数 ---------------------------*/
/**
* @brief 计算校验和(累加和,取低8位)
* @param data 数据起始指针
* @param len 需要校验的字节数(不包含校验和字节本身)
* @return 校验和值
*/
static u8 get_check_sum(const u8 *data, u16 len)
{
u8 sum = 0;
for (u16 i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
/*--------------------------- 核心处理函数 ---------------------------*/
/**
* @brief 串口1数据接收与帧解析服务函数
* @note 需周期性调用(例如在主循环中)
*/
void uart1_data_service(void)
{
static u16 rx1_in = 0; // 本地缓冲区中有效数据字节数
u16 offset = 0; // 当前已处理的偏移
u16 fr_len = 0; // 当前帧总长度
u8 check_num = 0; // 计算出的校验和
// 1. 从硬件环形缓冲区取数据到本地处理缓冲区(保留1字节空间防止溢出)
while ((rx1_in < sizeof(uart1_data_process_buf) - 1) && GetMcuUartByte() > 0) {
uart1_data_process_buf[rx1_in++] = take_byte_rx1buff();
}
// 至少需要4字节帧头才能开始解析
if (rx1_in < 4) {
return;
}
// 2. 帧解析循环
while ((rx1_in - offset) >= 4) {
// 2.1 检查固定帧头(不匹配则跳过当前字节)
if (uart1_data_process_buf[offset + 0] != HEAD_FIRST ||
uart1_data_process_buf[offset + 1] != HEAD_SECOND ||
uart1_data_process_buf[offset + 2] != PROTOCOL_VERSION ||
uart1_data_process_buf[offset + 3] != FRAME_TYPE) {
offset++; // 不匹配,跳过1字节,继续寻找
continue;
}
// 2.2 读取数据长度字段(索引 LEN_INDEX)
fr_len = uart1_data_process_buf[offset + LEN_INDEX];
fr_len += FIXED_OVERHEAD + CHECKSUM_LEN; // 总帧长 = 数据长度 + 固定开销 + 校验
// 2.3 验证帧长合理性及数据是否完整
if (fr_len > MAX_FRAME_LEN || (rx1_in - offset) < fr_len) {
// 数据不完整或帧过长,退出循环等待更多数据(不丢弃已有数据)
break;
}
// 2.4 校验和验证(累加和,不含校验字节)
check_num = get_check_sum((const u8 *)(uart1_data_process_buf + offset), fr_len - 1);
if (check_num != uart1_data_process_buf[offset + fr_len - 1]) {
// 校验失败,跳过当前帧头尝试重新同步
offset += 1;
continue;
}
// 2.5 校验通过,处理有效帧(此处可根据业务需求扩展)
// 例如复制帧数据到上层队列或执行命令
// process_frame(&uart1_data_process_buf[offset], fr_len);
// 调试打印(示例,注意加上 offset)
// PR_DEBUG("Frame: 0x%02X 0x%02X 0x%02X ...\r\n",
// uart1_data_process_buf[offset+0],
// uart1_data_process_buf[offset+1],
// uart1_data_process_buf[offset+2]);
// 跳过整个已处理帧
offset += fr_len;
}
// 3. 移除已处理数据,将剩余未处理数据搬移到缓冲区开头
if (offset > 0) {
rx1_in -= offset;
if (rx1_in > 0) {
// 将 uart1_data_process_buf[offset] 开始的数据搬移到开头
// 若使用标准 memcpy,替换为:memcpy((void*)uart1_data_process_buf, (void*)(uart1_data_process_buf + offset), rx1_in);
my_memcpy((u8 *)uart1_data_process_buf,
(const u8 *)(uart1_data_process_buf + offset),
rx1_in);
}
// 若 rx1_in == 0,则无需搬移,缓冲区可全部重用
}
}
上述代码。的 帧头。信息可以自定义,或者更改自己合适的值,再者是固定长度根据自己合适的方式去更改。
最后,介绍使用方法:
1.周期性调用service 函数,或者在主循环里调用
2.receive_input函数为数据输入,此函数为单个字符逐步输入模式,可自行修改,比如封起来,字符数组调用的,通过循环逐步加入。
3.check_sum是用来校验一帧数据是否有错误或者漏掉的情况,当然你有更好的也可以替换,合理即可。
4.buffer_init函数在串口初始化完毕后调用即可,因为是初始化指针。另外Uart_drv_buffer的大小可根据自己情况适度更改,或者128.或者200都行。