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

嵌入式协议处理:流式解析 vs 一次性解析

11小时前
403
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

嵌入式通信开发中,协议解析是绑定硬件和软件的关键环节。数据怎么来,就决定了怎么解析

本文通过上篇文章简易嵌入式自定义协议设计思路!的ITLV协议实例,深入对比一次性解析流式解析两种解析方式的本质区别。

    粘包的场景?断包的场景?数据噪声的场景?

决策树:

1. 先看一个问题

假设你要接收这样一帧数据:

55 AA 01 08 02 01 01 A5 F4  (9字节,LED控制命令)

这9个字节是怎么到达你程序的?

情况1:一次性过来,用一次性解析(Batch Parsing)方式。

情况2:一字节一字节过来,用流式解析(Stream Parsing)方式。

2. 核心区别

2.1 流式解析(Stream Parsing)

特点:

增量处理:一次处理一个字节

状态机驱动:维护解析状态(IDLE → HEAD1 → HEAD2 → ID → ...)

内部缓冲:有独立的接收缓冲区

实时响应:数据到达即处理

实现函数如:

// 逐字节输入,状态机驱动
protocol_err_e protocol_parse_byte(protocol_parser_t *parser, uint8_t byte)
{
    switch (parser->state)
    {
        case PARSE_STATE_IDLE:
            if (byte == 0x55)
            {
                parser->buffer[0] = byte;
                parser->index = 1;
                parser->state = PARSE_STATE_HEAD2;
            }
            // 其他字节直接丢弃,继续等0x55
            break;
            
        case PARSE_STATE_HEAD2:
            if (byte == 0xAA)
            {
                parser->buffer[parser->index++] = byte;
                parser->state = PARSE_STATE_ID;
            }
            else
            {
                parser->state = PARSE_STATE_IDLE;  // 包头错误,重来
            }
            break;
            
        // ... 其他状态类似 ...
        
        case PARSE_STATE_CRC_HIGH:
            parser->buffer[parser->index++] = byte;
            // CRC校验
            if (crc_ok)
            {
                parser->state = PARSE_STATE_IDLE;
                return PROTO_OK;  // 帧完成!
            }
            else
            {
                parser->state = PARSE_STATE_IDLE;
                return PROTO_ERR_CRC_MISMATCH;
            }
    }
    
    return PROTO_ERR_IN_PROGRESS;  // 还没收完,继续等
}

protocol_err_e protocol_parser_get_frame(const protocol_parser_t *parser,
                                          protocol_data_t *data)
{
    if (parser == NULL || data == NULL)
    {
        return PROTO_ERR_NULL_PTR;
    }
    
    constuint8_t *buf = parser->buffer;
    
    data->id = buf[2];           /* ID位置 */
    data->type = buf[3];         /* Type位置 */
    data->length = buf[4];       /* Length位置 */
    
    /* 复制payload数据 */
    if (data->length > 0)
    {
        memcpy(data->payload, &buf[PROTOCOL_HEADER_SIZE], data->length);
    }
    
    PROTO_LOGD("Extracted: id=0x%02X, type=%u, len=%u", data->id, data->type, data->length);
    
    return PROTO_OK;
}

使用例子如:

void USART1_IRQHandler(void)
{
    if (USART1->SR & USART_SR_RXNE) 
    {
        uint8_t byte = USART1->DR; 
        
        if (!ring_is_full(&g_rx_ring))
        {
            ring_push(&g_rx_ring, byte);
        }
    }
}

void protocol_task(void)
{
    while (!ring_is_empty(&g_rx_ring))
    {
        uint8_t byte = ring_pop(&g_rx_ring);
        
        protocol_err_e ret = protocol_parse_byte(&g_parser, byte);
        if (ret == PROTO_OK)
        {
            // 一帧完成,提取并处理
            protocol_data_t data;
            protocol_parser_get_frame(&g_parser, &data);
            process_frame(&data);       // 业务处理
        }
        elseif (ret != PROTO_ERR_IN_PROGRESS)
        {
            // 异常处理:解析出错(CRC错误等),状态机已自动复位,记录错误日志
        }
    }
}

2.2 批量解析(Batch Parsing)

特点:

一次性处理:前提是已有完整帧

无状态:不需要维护解析状态

外部缓冲:依赖调用者提供完整数据

简单直接:逻辑清晰,易于理解

实现函数如:

// 一次性解包完整帧
protocol_err_e protocol_unpack(const uint8_t *buf, size_t len, 
                                protocol_data_t *data)
{
    // 1检查包头
    if (buf[0] != PROTOCOL_HEAD_BYTE1 || buf[1] != PROTOCOL_HEAD_BYTE2)
    {
        return PROTO_ERR_INVALID_HEAD;
    }
    
    // 验证长度
    uint8_t payload_len = buf[PROTOCOL_LENGTH_INDEX];
    
    // CRC校验
    uint16_t calc_crc = crc16_x25(buf, crc_offset);
    
    // 提取数据
    data->id = buf[2];
    memcpy(data->payload, &buf[PROTOCOL_HEADER_SIZE], data->length);
    
    return PROTO_OK;
}

使用例子如:

// 假设从文件/网络已读取到完整帧
uint8_t rx_buf[] = {0x55, 0xAA, 0x01, 0x08, 0x02, 0x01, 0x01, 0xA5, 0xF4};
protocol_data_t data;

protocol_err_e ret = protocol_unpack(rx_buf, sizeof(rx_buf), &data);
if (ret == PROTO_OK)
{
    printf("解析成功: ID=0x%02Xn", data.id);
}

3. 对比实例

3.1 典型场景

3.1.1 处理粘包

什么是粘包? 多个帧的数据粘在一起到达。

收到的数据: 55 AA 01 08 02 01 01 A5 F4 55 AA 02 08 08 02 02 00 FE A3

一次性解析:只能解析第一帧,剩余数据需要自己处理偏移。

流式解析:自动处理!

void demo_sticky_packets(void)
{
    printf("n流式解析粘包处理n");
    
    // 两帧数据粘在一起
    uint8_t sticky[] = {
        0x55, 0xAA, 0x01, 0x08, 0x02, 0x01, 0x01, 0xA5, 0xF4,  // 帧1
        0x55, 0xAA, 0x01, 0x08, 0x02, 0x02, 0x00, 0x44, 0xCF   // 帧2
    };
    
    protocol_parser_t parser;
    protocol_parser_init(&parser);
    
    int frame_count = 0;
    for (size_t i = 0; i < sizeof(sticky); i++)
    {
        if (protocol_parse_byte(&parser, sticky[i]) == PROTO_OK)
        {
            frame_count++;
            protocol_data_t data;
            protocol_parser_get_frame(&parser, &data);
            
            uint8_t *payload = data.payload;
            printf("帧%d: ID=0x%02X, LED%d=%sn", 
                   frame_count, data.id, 
                   payload[0], payload[1] ? "ON" : "OFF");
        }
    }

    printf("n微信公众号:嵌入式大杂烩n");
}

3.1.2 处理断包

什么是断包? 一帧数据分多次到达。

一次性解析:前两批数据无法解析,需要自己拼接。

流式解析:天然支持!状态机"记住"已收到的部分。

void demo_break_packets(void)
{
    printf("n流式解析断包处理n");
    
    // 模拟断包场景
    uint8_t part1[] = {0x55, 0xAA, 0x01};           // 第1批
    uint8_t part2[] = {0x08, 0x02, 0x01};           // 第2批  
    uint8_t part3[] = {0x01, 0xA5, 0xF4};           // 第3批

    protocol_parser_t parser;
    protocol_parser_init(&parser);

    // 喂入第1批
    for (int i = 0; i < 3; i++)
    {
        protocol_parse_byte(&parser, part1[i]);
    }
    printf("第1批后状态: %dn", parser.state);  // 输出: 4 (WAIT_TYPE)

    // 喂入第2批
    for (int i = 0; i < 3; i++)
    {
        protocol_parse_byte(&parser, part2[i]);
    }
    printf("第2批后状态: %dn", parser.state);  // 输出: 7 (WAIT_CRC_L)

    // 喂入第3批
    for (int i = 0; i < 3; i++)
    {
        protocol_err_e ret = protocol_parse_byte(&parser, part3[i]);
        if (ret == PROTO_OK)
        {
            printf("帧解析完成!n");  // 第3字节时触发
            protocol_data_t data;
            protocol_parser_get_frame(&parser, &data);
            
            uint8_t *payload = data.payload;
            printf("ID=0x%02X, LED%d=%sn", 
                   data.id, payload[0], payload[1] ? "ON" : "OFF");
        }
    }

    printf("n微信公众号:嵌入式大杂烩n");
}

3.1.3 噪声过滤

真实环境中,串口可能收到干扰噪声:

一次性解析:无法处理噪声数据,解析会失败。

流式解析:自动过滤噪声。

void demo_noise_filter(void)
{
    printf("n流式解析过滤噪声n");
    
    // 有效帧前面有噪声的数据
    uint8_t noisy[] = {
        0xFF, 0xFF,                                            // 噪声
        0x55, 0xAA, 0x01, 0x08, 0x02, 0x01, 0x01, 0xA5, 0xF4   // 有效帧
    };
    
    protocol_parser_t parser;
    protocol_parser_init(&parser);
    
    for (size_t i = 0; i < sizeof(noisy); i++)
    {
        protocol_err_e ret = protocol_parse_byte(&parser, noisy[i]);    
        if (ret == PROTO_OK)
        {
            protocol_data_t data;
            protocol_parser_get_frame(&parser, &data);
            uint8_t *payload = data.payload;
            printf("解析结果: ID=0x%02X(噪声被自动过滤), LED%d=%sn", 
                   data.id, payload[0], payload[1] ? "ON" : "OFF");
        }
        elseif (ret != PROTO_ERR_IN_PROGRESS)
        {
            printf("Parse error: %sn", protocol_err_str(ret));
            break;
        }
    }
    printf("n微信公众号:嵌入式大杂烩n");
}

3.2 流式解析适用场景

3.2.1 串口通信(最典型)
void USART1_IRQHandler(void)
{
    if (USART1->SR & USART_SR_RXNE) 
    {
        uint8_t byte = USART1->DR; 
        
        if (!ring_is_full(&g_rx_ring))
        {
            ring_push(&g_rx_ring, byte);
        }
    }
}

void protocol_task(void)
{
    while (!ring_is_empty(&g_rx_ring))
    {
        uint8_t byte = ring_pop(&g_rx_ring);
        
        protocol_err_e ret = protocol_parse_byte(&g_parser, byte);
        if (ret == PROTO_OK)
        {
            // 一帧完成,提取并处理
            protocol_data_t data;
            protocol_parser_get_frame(&g_parser, &data);
            process_frame(&data);       // 业务处理
        }
        elseif (ret != PROTO_ERR_IN_PROGRESS)
        {
            // 异常处理:解析出错(CRC错误等),状态机已自动复位,记录错误日志
        }
    }
}

优势:

中断触发,立即处理

不需要等待完整帧

自动处理粘包问题

3.2.2 低速网络通信(TCP流式传输)
// TCP接收回调(数据分批到达)
void tcp_recv_callback(uint8_t *data, size_t len)
{
    for (size_t i = 0; i < len; i++)
    {
        protocol_err_e ret = protocol_parse_byte(&g_parser, data[i]);
        
        if (ret == PROTO_OK)
        {
            // 处理完整帧
            process_frame();
        }
    }
}
3.2.3 嵌入式实时系统
void protocol_task(void)
{
    while (1)
    {
        if (uart_has_data())
        {
            uint8_t byte = uart_get_byte();
            protocol_parse_byte(&parser, byte);
        }
        
        os_delay(1);
    }
}

3.3 批量解析适用场景

3.3.1 高速网络通信(UDP/以太网)
// UDP接收(每次接收完整报文)
void udp_recv_callback(uint8_t *buf, size_t len)
{
    protocol_data_t data;
    
    // UDP保证报文完整性,直接解包
    protocol_err_e ret = protocol_unpack(buf, len, &data);
    
    if (ret == PROTO_OK)
    {
        handle_data(&data);
    }
}

优势:

一次性处理,效率高

逻辑简单,易于调试

UDP保证了帧的完整性

3.3.2 文件/存储读取
void read_config_from_file(void)
{
    FILE *fp = fopen("config.bin", "rb");
    if (fp == NULL) return;

    uint8_t frame_buf[PROTOCOL_MAX_LEN];

    // 从文件读取完整帧
    size_t read_len = fread(frame_buf, 1, sizeof(frame_buf), fp);
    fclose(fp);

    protocol_data_t config;
    if (protocol_unpack(frame_buf, read_len, &config) == PROTO_OK)
    {
        apply_config(&config);
    }
}
3.3.3 单次通信交互
void recv_cmd(void)
{
    uint8_t rx_buf[256];
    size_t rx_len;
    protocol_data_t response;

    // 接收完整响应
    rx_len = recv(rx_buf, sizeof(rx_buf));

    // 一次性解包
    protocol_unpack(rx_buf, rx_len, &response);
}

4. 总结

详细对比

维度 流式解析 批量解析
适用场景 串口通信、低速网络通信、实时环境等 文件读取、高速网络通信、当次通信交互等
数据要求 可处理不完整数据 必须是完整帧
状态保持 需要状态机 无状态
内存占用 固定缓冲区 无额外缓冲区
粘包处理 自动分离 需要上层处理
断包处理 跨次接收无缝衔接 无法处理
实时性 极佳 一般
CPU开销 较高(每字节一次调用) 较低(一次调用)
复杂度 高(状态机逻辑) 低(顺序处理)
错误恢复 自动重新同步 需要手动处理

选型要点

数据

一次到齐 → 一次性解析数据

滴滴答答 → 流式解析

两种方式没有优劣之分,匹配数据到达方式才是关键。

QA

Q1: 流式解析状态机遇到"帧头假象"时如何处理?

payload数据中恰好包含 0x55 0xAA(与帧头相同),状态机会不会误判?

不会。收到帧头后,根据Length字段确定帧长度,不会在payload中间重新寻找帧头;即使状态机被假帧头干扰,CRC校验会失败,状态机自动复位重新同步。

如果协议对可靠性要求极高,可以考虑转义编码,将payload中的特殊字节转义处理。


我是杂烩君,专注嵌入式软件开发!

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!