大家好,我是杂烩君。
在嵌入式通信开发中,协议解析是绑定硬件和软件的关键环节。数据怎么来,就决定了怎么解析。
本文通过上篇文章简易嵌入式自定义协议设计思路!的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中的特殊字节转义处理。
我是杂烩君,专注嵌入式软件开发!
403