单片机串口环形缓冲区应该怎么写,或解析串口协议
在开发单片机的过程中我们经常使用到串口用来与其他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[offset0], // uart1_data_process_buf[offset1], // uart1_data_process_buf[offset2]); // 跳过整个已处理帧 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都行。

相关新闻