扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

通用代码模板,使用环形队列和自定义协议接收串口数据

03/31 09:27
239
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!

嵌入式软件开发领域,串口通信或网络通信,对不定长数据进行收发是比较常见的需求之一。

因为直接在串口中断函数里面处理数据,可能会导致阻塞/丢包/数据错乱等问题,而环形队列是解决异步数据缓存的最优方案。

搭配上自定义的数据通信协议,可以精准地解析帧头/帧尾/数据长度/校验位,完美地解决了不定长数据的分包/粘包问题。

本文提供一个可以直接移植的C语言代码模板,讲解一下环形队列+自定义协议的核心原理,设计一个串口数据收发的完整流程,适用于单片机(比如STM32、C51、ESP32)、Linux串口编程、网络Socket等数据接收场景。

一、核心原理

(1)为什么要用环形队列?

因为数据通常是连续和不定长的,中断接收的速度比较快,如果主程序处理数据比较慢,就需要使用先进先出(FIFO)的环形队列进行数据缓冲,并且在中断里面做入队操作,主程序进行出队解析。

(2)为什么要自定义协议?

如果不对数据进行协议封包,就无法判断一帧数据从哪里开始、到哪里结束、是否完整、是否出错。

自定义协议可以标准化数据格式,实现帧定位(帧头+帧尾)、数据长度标识、数据校验(防止传输错误)、精准解析不定长数据。

二、设计自定义通信协议

协议帧数据字段格式如下:

【帧头】【数据长度】【数据内容】【校验位】【帧尾】

字段定义:

字段 长度 说明 示例值
帧头 2字节 固定标识:0x55AA 0x55AA
数据长度 1字节 有效数据长度 0x03
数据内容 N字节 数据载荷 自定义
校验位 1字节 累加校验和 计算值
帧尾 2字节 固定标识:0xBBCC 0xBBCC

协议优势:

采用双帧头+帧尾设计,抗干扰能力强,避免错误解析,长度字段可以明确知道需要接收多少数据,校验位用来保证数据传输的完整性,完美地适配了不定长数据的解析。

三、环形队列的代码实现

环形队列是整个代码模板的核心,支持入队、出队、判空、判满、查询长度等基础操作。

#include <stdint.h>#include <string.h>// 环形队列配置#define RING_BUF_SIZE  1024  // 队列大小,可根据需求修改
// 环形队列结构体typedef struct {    uint8_t buffer[RING_BUF_SIZE];  // 数据缓冲区    uint16_t head;                  // 队头(数据出队位置)    uint16_t tail;                  // 队尾(数据入队位置)    uint16_t size;                  // 当前数据长度} RingQueue_t;
// 全局环形队列实例(串口接收专用)static RingQueue_t uart_rx_queue;
/** * @brief  初始化环形队列 */void ring_queue_init(RingQueue_t *q) {    memset(q, 0, sizeof(RingQueue_t));}
/** * @brief  判断队列是否为空 */uint8_t ring_queue_is_empty(RingQueue_t *q) {    return (q->size == 0);}
/** * @brief  判断队列是否已满 */uint8_t ring_queue_is_full(RingQueue_t *q) {    return (q->size == RING_BUF_SIZE);}
/** * @brief  数据入队(中断中调用) * @param  data: 入队字节 * @return 0:成功 1:失败(队列满) */uint8_t ring_queue_enqueue(RingQueue_t *q, uint8_t data) {    if(ring_queue_is_full(q)) return 1;
    q->buffer[q->tail] = data;    q->tail = (q->tail + 1) % RING_BUF_SIZE;    q->size++;    return 0;}
/** * @brief  数据出队(主程序中调用) * @param  data: 出队数据存储指针 * @return 0:成功 1:失败(队列空) */uint8_t ring_queue_dequeue(RingQueue_t *q, uint8_t *data) {    if(ring_queue_is_empty(q)) return 1;
    *data = q->buffer[q->head];    q->head = (q->head + 1) % RING_BUF_SIZE;    q->size--;    return 0;}
/** * @brief  获取队列中当前数据长度 */uint16_t ring_queue_get_size(RingQueue_t *q) {    return q->size;}

四、串口驱动+环形队列对接

在串口中断里面,将接收到的字节压入队列,不做任何解析处理,保证中断函数的执行时间极短。

串口中断服务函数

// 串口接收中断服务函数(平台相关,仅示例)void USART1_IRQHandler(void) {    uint8_t ch;    // 判断是否为接收中断    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {        ch = USART_ReceiveData(USART1);  // 读取串口数据        ring_queue_enqueue(&uart_rx_queue, ch);  // 数据入队    }}

五、自定义协议的解析函数

主程序的大循环从环形队列里面读取数据,按照自定义协议进行解析,自动识别完整的数据帧,处理粘包和分包问题。

1、协议解析配置

// 协议定义#define FRAME_HEAD1    0xAA    // 帧头1#define FRAME_HEAD2    0x55    // 帧头2#define FRAME_TAIL1    0xBB    // 帧尾1#define FRAME_TAIL2    0xCC    // 帧尾2#define MAX_DATA_LEN   256     // 最大数据长度
// 解析状态枚举typedef enum {    PARSE_HEAD1,    // 等待帧头1    PARSE_HEAD2,    // 等待帧头2    PARSE_LEN,      // 等待数据长度    PARSE_DATA,     // 接收数据内容    PARSE_CHECK,    // 等待校验位    PARSE_TAIL1,    // 等待帧尾1    PARSE_TAIL2     // 等待帧尾2} ParseState_t;
// 解析结构体typedef struct {    ParseState_t state;       // 当前解析状态    uint8_t data_len;         // 数据长度    uint8_t data_buf[MAX_DATA_LEN]; // 数据缓存    uint8_t data_cnt;         // 已接收数据长度    uint8_t check_sum;       // 校验和} FrameParse_t;static FrameParse_t parse;

2、协议解析主函数

/** * @brief  从环形队列解析一帧完整数据 * @param  out_data: 输出解析后的有效数据 * @param  out_len: 输出有效数据长度 * @return 0:解析成功 1:无完整帧/解析失败 */uint8_t frame_parse(uint8_t *out_data, uint8_t *out_len) {    uint8_t ch;    // 循环读取队列中的数据    while(ring_queue_dequeue(&uart_rx_queue, &ch) == 0) {        switch(parse.state) {            // 1. 匹配帧头1            case PARSE_HEAD1:                if(ch == FRAME_HEAD1) {                    parse.state = PARSE_HEAD2;                }                break;
            // 2. 匹配帧头2            case PARSE_HEAD2:                if(ch == FRAME_HEAD2) {                    parse.state = PARSE_LEN;                    parse.check_sum = 0; // 重置校验和                } else {                    parse.state = PARSE_HEAD1; // 匹配失败,重新等待帧头                }                break;
            // 3. 读取数据长度            case PARSE_LEN:                parse.data_len = ch;                parse.data_cnt = 0;                parse.state = PARSE_DATA;                break;
            // 4. 接收数据内容(核心:不定长数据接收)            case PARSE_DATA:                parse.data_buf[parse.data_cnt++] = ch;                parse.check_sum += ch; // 累加计算校验和                if(parse.data_cnt >= parse.data_len) {                    parse.state = PARSE_CHECK; // 数据接收完成                }                break;
            // 5. 校验和验证            case PARSE_CHECK:                if(ch == parse.check_sum) {                    parse.state = PARSE_TAIL1;                } else {                    parse.state = PARSE_HEAD1; // 校验失败,重置                }                break;
            // 6. 匹配帧尾1            case PARSE_TAIL1:                if(ch == FRAME_TAIL1) {                    parse.state = PARSE_TAIL2;                } else {                    parse.state = PARSE_HEAD1;                }                break;
            // 7. 匹配帧尾2,一帧解析完成            case PARSE_TAIL2:                if(ch == FRAME_TAIL2) {                    // 复制有效数据                    memcpy(out_data, parse.data_buf, parse.data_len);                    *out_len = parse.data_len;                    // 重置解析状态,准备接收下一帧                    memset(&parse, 0, sizeof(FrameParse_t));                    return 0; // 解析成功                }                parse.state = PARSE_HEAD1;                break;
            default:                parse.state = PARSE_HEAD1;                break;        }    }    return 1; // 无完整数据帧}

六、串口数据发送函数

按照自定义协议对数据进行封装然后发送,确保收发双方的数据格式统一。

/** * @brief  按自定义协议发送串口数据 * @param  data: 待发送有效数据 * @param  len: 有效数据长度 */void uart_send_frame(uint8_t *data, uint8_t len) {    uint8_t check_sum = 0;    uint8_t i;
    // 1. 发送帧头    uart_send_byte(FRAME_HEAD1);    uart_send_byte(FRAME_HEAD2);
    // 2. 发送数据长度    uart_send_byte(len);
    // 3. 发送数据内容 + 计算校验和    for(i=0; i<len; i++) {        uart_send_byte(data[i]);        check_sum += data[i];    }
    // 4. 发送校验位    uart_send_byte(check_sum);
    // 5. 发送帧尾    uart_send_byte(FRAME_TAIL1);    uart_send_byte(FRAME_TAIL2);}// 底层串口单字节发送函数(平台自行实现)void uart_send_byte(uint8_t ch) {    // STM32示例:USART_SendData(USART1, ch);    // while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);}

七、主程序使用示例

主程序无限循环调用解析函数,对数据进行解析成功后,处理业务逻辑,非阻塞并且不丢包。

int main(void) {    uint8_t rx_data[MAX_DATA_LEN];    uint8_t rx_len;    uint8_t test_tx_data[] = {0x01, 0x02, 0x03};
    // 初始化    uart_init();
    while(1) {        // 1. 解析串口数据(核心:不定长数据解析)        if(frame_parse(rx_data, &rx_len) == 0) {            // 解析成功,处理数据            // 示例:打印接收数据 / 执行指令            for(uint8_t i=0; i<rx_len; i++) {                // rx_data[i] 为有效数据            }        }
        // 2. 测试发送数据        // uart_send_frame(test_tx_data, sizeof(test_tx_data));    }}

八、代码模板的优势

(1)数据接收与处理已经解耦,中断只做入队操作,主程序进行数据解析。

(2)通过协议长度字段,自动适配任意长度的数据,完美支持不定长数据。

(3)采用帧头+帧尾+校验位,抗干扰防粘包,解决串口数据传输问题。

(4)C语言编写的代码非常通用,无平台依赖,方便移植。

(5)资源占用极少,非常适合在MCU等嵌入式环境中使用。

九、总结

总的来说,本文提供了环形队列+自定义协议的代码模板,是串口和网络不定长数据接收的通用解决方案。

环形队列解决了异步数据缓存、丢包、阻塞等问题,自定义协议解决了不定长数据解析、粘包分包、数据校验等问题。

代码结构清晰并且可以直接移植,适配了大多数嵌入式通信场景,可以直接将代码复制到项目中使用,也可以根据业务需求扩展协议功能。

相关推荐