欢迎光临
我们一直在努力

87.嵌入式串口协议解析:环形缓冲区+状态机实现拆分包处理

一、核心问题分析

串口接收的痛点在于数据生产(中断/回调)与消费(解析任务)速度不匹配,以及一次回调不等于一帧数据。例如:

  • 一帧数据 55 7E 03 10 A4 5B CRC 被拆成三次回调:55 7E 03、10 A4、5B CRC
  • 解析任务需要在数据不连续的情况下,仍能正确识别帧头、长度、数据体和校验位

二、解决方案架构

采用生产者-消费者模型:

  • 生产者:串口中断/回调,将接收到的字节写入环形缓冲区
  • 消费者:主循环解析任务,从环形缓冲区逐字节取出数据,通过状态机解析完整帧
  • 2.1 环形缓冲区设计

    环形缓冲区用于暂存未处理的字节,解耦生产与消费速度:

    #define RING_BUF_SIZE 256 // 缓冲区大小,根据实际需求调整

    typedef struct {
    uint8_t buffer[RING_BUF_SIZE];
    volatile uint16_t head; // 写指针(生产者修改)
    volatile uint16_t tail; // 读指针(消费者修改)
    } RingBuffer;

    RingBuffer rx_ring_buf = {0};

    // 生产者:向环形缓冲区写入字节(串口中断调用)
    void ring_buf_write(uint8_t byte) {
    uint16_t next_head = (rx_ring_buf.head + 1) % RING_BUF_SIZE;
    if (next_head != rx_ring_buf.tail) { // 缓冲区未满
    rx_ring_buf.buffer[rx_ring_buf.head] = byte;
    rx_ring_buf.head = next_head;
    } else {
    // 缓冲区溢出处理:可记录错误日志
    }
    }

    // 消费者:从环形缓冲区读取字节(解析任务调用)
    bool ring_buf_read(uint8_t *byte) {
    if (rx_ring_buf.head == rx_ring_buf.tail) {
    return false; // 缓冲区为空
    }
    *byte = rx_ring_buf.buffer[rx_ring_buf.tail];
    rx_ring_buf.tail = (rx_ring_buf.tail + 1) % RING_BUF_SIZE;
    return true;
    }

    2.2 状态机设计

    状态机用于记录解析进度,核心状态包括:

    • WAIT_SOF:等待帧头(示例中帧头为 0x7E)
    • READ_LEN:读取数据体长度
    • READ_BODY:读取数据体
    • READ_CRC:读取并校验CRC

    // 协议帧格式示例:[帧头(1B)] + [长度(1B)] + [数据体(NB)] + [CRC(1B)]
    typedef enum {
    WAIT_SOF, // 等待帧头
    READ_LEN, // 读取长度
    READ_BODY, // 读取数据体
    READ_CRC // 读取CRC
    } ParseState;

    typedef struct {
    ParseState state; // 当前状态
    uint8_t expected_len; // 期望的数据体长度
    uint8_t body_pos; // 数据体接收位置
    uint8_t body_buf[64]; // 数据体缓冲区(根据实际需求调整大小)
    } Parser;

    Parser parser = {0};

    // 重置解析器
    void parser_reset(void) {
    parser.state = WAIT_SOF;
    parser.expected_len = 0;
    parser.body_pos = 0;
    memset(parser.body_buf, 0, sizeof(parser.body_buf));
    }

    // 解析单个字节
    void parser_feed(uint8_t byte) {
    switch (parser.state) {
    case WAIT_SOF:
    if (byte == 0x7E) { // 匹配帧头
    parser.state = READ_LEN;
    }
    break;

    case READ_LEN:
    if (byte > sizeof(parser.body_buf)) { // 长度超出缓冲区
    parser_reset();
    break;
    }
    parser.expected_len = byte;
    parser.body_pos = 0;
    parser.state = READ_BODY;
    break;

    case READ_BODY:
    parser.body_buf[parser.body_pos++] = byte;
    if (parser.body_pos == parser.expected_len) { // 数据体接收完成
    parser.state = READ_CRC;
    }
    break;

    case READ_CRC:
    // 校验CRC(示例为简单异或校验,可替换为CRC16/CRC32)
    uint8_t calc_crc = 0;
    for (uint8_t i = 0; i < parser.expected_len; i++) {
    calc_crc ^= parser.body_buf[i];
    }
    if (calc_crc == byte) { // CRC校验成功
    // 处理完整帧:parser.body_buf 为有效数据
    printf("解析成功,数据长度:%d\\n", parser.expected_len);
    // 这里可添加数据处理逻辑
    } else {
    printf("CRC校验失败\\n");
    }
    parser_reset(); // 无论成功失败,重置状态机
    break;

    default:
    parser_reset();
    break;
    }
    }

    2.3 主循环解析任务

    在主循环中,持续从环形缓冲区读取字节并喂给状态机:

    void protocol_poll(void) {
    uint8_t byte;
    while (ring_buf_read(&byte)) { // 逐字节读取
    parser_feed(byte); // 喂给状态机解析
    }
    }

    2.4 串口中断回调

    在串口接收中断中,将接收到的字节写入环形缓冲区:

    // 串口接收中断回调函数(以STM32 HAL库为例)
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
    uint8_t rx_byte;
    HAL_UART_Receive_IT(huart, &rx_byte, 1); // 继续接收下一个字节
    ring_buf_write(rx_byte); // 写入环形缓冲区
    }
    }

    三、关键优化点

  • 缓冲区大小:根据串口波特率和主循环处理速度调整,避免溢出
  • 状态机复位:异常场景(如长度超限、CRC错误)必须重置状态机,避免状态混乱
  • 线程安全:环形缓冲区的head和tail需用volatile修饰,防止编译器优化
  • 错误处理:添加缓冲区溢出、帧头丢失、CRC错误等日志记录,便于调试
  • 四、扩展与适配

    • 多帧支持:如果一次回调包含多帧数据,状态机会自动处理,无需额外修改
    • 变长协议:只需修改状态机的READ_LEN和READ_BODY逻辑,即可适配不同协议
    • DMA接收:可将环形缓冲区与DMA结合,进一步降低CPU占用

    五、总结

    通过环形缓冲区+状态机的组合,我们实现了一套稳定的串口协议解析方案,能够有效处理拆分包场景。这套方案已在多个嵌入式项目中验证,可直接落地使用。

    代码写得好,其实是一种艺术。稳定、清楚、可维护的代码,才是嵌入式开发的核心追求。


    参考资料:

  • 嵌入式串口全双工中断驱动:环形缓冲区与信号量实战解析
  • STM32串口接收数据包(自定义帧头帧尾)
  • 基于STM32 HAL库实现串口DMA+状态机接收的代码框架
  • 如果你需要适配特定协议(如Modbus、自定义协议),或需要优化性能,欢迎在评论区交流。

     

    赞(0)
    未经允许不得转载:171主机测评 » 87.嵌入式串口协议解析:环形缓冲区+状态机实现拆分包处理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址