单片机串口环形缓冲区应该怎么写,或解析串口协议
在开发单片机的过程中,我们经常使用到串口用来与其他mcu进行通信的情况,有时候需要处理特殊的串口消息,如固定的帧头,帧尾,还有校验位的数据,通常这种情况可能还是不固定的。但是有个至少的长度,例如6位或者7位,或者更多,这种情况时候用这种环形缓存的问题来解决,通常情况下可以使能串口的空闲中断加上dma方式的接收处理,这样使得串口的接收更加高效。
话不多说,直接上代码:
/** * @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都行。