单片机串口环形缓冲区应该怎么写,或解析串口协议

在开发单片机的过程中,我们经常使用到串口用来与其他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都行。