
分享自己写的一个通信协议源码,支持C++和C,双包头+不定长

一、源码分享
1、protocol.h
cpp
#ifndef __PROTOCOL_H__
#define __PROTOCOL_H__
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PROTO_HEAD0 0xAAU
#define PROTO_HEAD1 0xBBU
#define PROTO_MAX_PAYLOAD_LEN 256U
typedef enum
{
PROTO_WAIT_HEAD0,
PROTO_WAIT_HEAD1,
PROTO_READ_LEN_H,
PROTO_READ_LEN_L,
PROTO_READ_PAYLOAD,
PROTO_READ_CRC_H,
PROTO_READ_CRC_L
}ProtoState_e;
typedef void (*ProtoFrameCb)(const uint8_t* payload, uint16_t len);
typedef struct
{
ProtoState_e state;
uint16_t expect_payload_len;
uint16_t crc_read;
uint16_t payload_recv_cnt;
uint8_t payload_buf[PROTO_MAX_PAYLOAD_LEN];
ProtoFrameCb frame_cb;
}ProtocolDecoder_t;
uint16_t crc16_modbus(const uint8_t* data, size_t len);
void proto_decoder_init(ProtocolDecoder_t* dec, ProtoFrameCb cb);
void proto_decoder_put_byte(ProtocolDecoder_t* dec, uint8_t byte);
// 批量输入buffer
void proto_decoder_put_buffer(ProtocolDecoder_t* dec, const uint8_t* buf, size_t len);
// 编码,返回生成帧总字节数;0代表失败
size_t proto_encode_frame(uint8_t* out_buf, size_t out_buf_sz,
const uint8_t* payload, uint16_t payload_len);
#ifdef __cplusplus
}
#endif
#endif
2、protocol.c
cpp
#include "protocol.h"
static const uint16_t crc16_table[256] = {
0x0000,0xC0C1,0xC181,0x0140,0xC301,0x03C0,0x0280,0xC241,0xC601,0x06C0,0x0780,0xC741,0x0500,0xC5C1,0xC481,0x0440,
0xCC01,0x0CC0,0x0D80,0xCD41,0x0F00,0xCFC1,0xCE81,0x0E40,0x0A00,0xCAC1,0xCB81,0x0B40,0xC901,0x09C0,0x0880,0xC841,
0xD801,0x18C0,0x1980,0xD941,0x1B00,0xDBC1,0xDA81,0x1A40,0x1E00,0xDEC1,0xDF81,0x1F40,0xDD01,0x1DC0,0x1C80,0xDC41,
0x1400,0xD4C1,0xD581,0x1540,0xD701,0x17C0,0x1680,0xD641,0xD201,0x12C0,0x1380,0xD341,0x1100,0xD1C1,0xD081,0x1040,
0xF001,0x30C0,0x3180,0xF141,0x3300,0xF3C1,0xF281,0x3240,0x3600,0xF6C1,0xF781,0x3740,0xF501,0x35C0,0x3480,0xF441,
0x3C00,0xFCC1,0xFD81,0x3D40,0xFF01,0x3FC0,0x3E80,0xFE41,0xFA01,0x3AC0,0x3B80,0xFB41,0x3900,0xF9C1,0xF881,0x3840,
0x2800,0xE8C1,0xE981,0x2940,0xEB01,0x2BC0,0x2A80,0xEA41,0xEE01,0x2EC0,0x2F80,0xEF41,0x2D00,0xEDC1,0xEC81,0x2C40,
0xE401,0x24C0,0x2580,0xE541,0x2700,0xE7C1,0xE681,0x2640,0x2200,0xE2C1,0xE381,0x2340,0xE101,0x21C0,0x2080,0xE041,
0xA001,0x60C0,0x6180,0xA141,0x6300,0xA3C1,0xA281,0x6240,0x6600,0xA6C1,0xA781,0x6740,0xA501,0x65C0,0x6480,0xA441,
0x6C00,0xACC1,0xAD81,0x6D40,0xAF01,0x6FC0,0x6E80,0xAE41,0xAA01,0x6AC0,0x6B80,0xAB41,0x6900,0xA9C1,0xA881,0x6840,
0x7800,0xB8C1,0xB981,0x7940,0xBB01,0x7BC0,0x7A80,0xBA41,0xBE01,0x7EC0,0x7F80,0xBF41,0x7D00,0xBDC1,0xBC81,0x7C40,
0xB401,0x74C0,0x7580,0xB541,0x7700,0xB7C1,0xB681,0x7640,0x7200,0xB2C1,0xB381,0x7340,0xB101,0x71C0,0x7080,0xB041,
0x5000,0x90C1,0x9181,0x5140,0x9301,0x53C0,0x5280,0x9241,0x9601,0x56C0,0x5780,0x9741,0x5500,0x95C1,0x9481,0x5440,
0x9C01,0x5CC0,0x5D80,0x9D41,0x5F00,0x9FC1,0x9E81,0x5E40,0x5A00,0x9AC1,0x9B81,0x5B40,0x9901,0x59C0,0x5880,0x9841,
0x8801,0x48C0,0x4980,0x8941,0x4B00,0x8BC1,0x8A81,0x4A40,0x4E00,0x8EC1,0x8F81,0x4F40,0x8D01,0x4DC0,0x4C80,0x8C41,
0x4400,0x84C1,0x8581,0x4540,0x8701,0x47C0,0x4680,0x8641,0x8201,0x42C0,0x4380,0x8341,0x4100,0x81C1,0x8081,0x4040
};
uint16_t crc16_modbus(const uint8_t* data, size_t len)
{
uint16_t crc = 0xFFFFU;
size_t i;
for (i = 0; i < len; i++)
{
uint8_t byte = data[i];
crc = (uint16_t)((crc >> 8U) ^ crc16_table[(crc ^ byte) & 0xFFU]);
}
return crc;
}
void proto_decoder_init(ProtocolDecoder_t* dec, ProtoFrameCb cb)
{
dec->state = PROTO_WAIT_HEAD0;
dec->expect_payload_len = 0U;
dec->crc_read = 0U;
dec->payload_recv_cnt = 0U;
dec->frame_cb = cb;
}
void proto_decoder_put_buffer(ProtocolDecoder_t* dec, const uint8_t* buf, size_t len)
{
size_t i;
for (i = 0; i < len; i++)
{
proto_decoder_put_byte(dec, buf[i]);
}
}
void proto_decoder_put_byte(ProtocolDecoder_t* dec, uint8_t byte)
{
switch (dec->state)
{
case PROTO_WAIT_HEAD0:
if (byte == PROTO_HEAD0)
{
dec->state = PROTO_WAIT_HEAD1;
}
break;
case PROTO_WAIT_HEAD1:
if (byte == PROTO_HEAD1)
{
dec->state = PROTO_READ_LEN_H;
}
else
{
dec->state = PROTO_WAIT_HEAD0;
}
break;
case PROTO_READ_LEN_H:
dec->expect_payload_len = ((uint16_t)byte) << 8U;
dec->state = PROTO_READ_LEN_L;
break;
case PROTO_READ_LEN_L:
dec->expect_payload_len |= byte;
dec->payload_recv_cnt = 0U;
if (dec->expect_payload_len > PROTO_MAX_PAYLOAD_LEN)
{
dec->state = PROTO_WAIT_HEAD0;
}
else if (dec->expect_payload_len == 0U)
{
dec->state = PROTO_READ_CRC_H;
}
else
{
dec->state = PROTO_READ_PAYLOAD;
}
break;
case PROTO_READ_PAYLOAD:
{
dec->payload_buf[dec->payload_recv_cnt++] = byte;
if (dec->payload_recv_cnt >= dec->expect_payload_len)
{
dec->state = PROTO_READ_CRC_H;
}
break;
}
case PROTO_READ_CRC_H:
dec->crc_read = ((uint16_t)byte) << 8U;
dec->state = PROTO_READ_CRC_L;
break;
case PROTO_READ_CRC_L:
{
dec->crc_read |= byte;
uint8_t len_buf[2];
len_buf[0] = (uint8_t)(dec->expect_payload_len >> 8U);
len_buf[1] = (uint8_t)(dec->expect_payload_len & 0xFFU);
uint16_t calc_crc = crc16_modbus(len_buf, 2U);
uint16_t i;
for (i = 0; i < dec->expect_payload_len; i++)
{
uint8_t b = dec->payload_buf[i];
calc_crc = (uint16_t)((calc_crc >> 8U) ^ crc16_table[(calc_crc ^ b) & 0xFFU]);
}
if (calc_crc == dec->crc_read)
{
if (dec->frame_cb != NULL)
{
dec->frame_cb(dec->payload_buf, dec->expect_payload_len);
}
}
dec->state = PROTO_WAIT_HEAD0;
break;
}
default:
proto_decoder_init(dec, dec->frame_cb);
break;
}
}
size_t proto_encode_frame(uint8_t* out_buf, size_t out_buf_sz,
const uint8_t* payload, uint16_t payload_len)
{
const size_t total_frame_size = 2U + 2U + payload_len + 2U;
if (out_buf_sz < total_frame_size)
{
return 0U;
}
if (payload_len > PROTO_MAX_PAYLOAD_LEN)
{
return 0U;
}
uint8_t* p = out_buf;
*p++ = PROTO_HEAD0;
*p++ = PROTO_HEAD1;
uint8_t len_buf[2];
len_buf[0] = (uint8_t)(payload_len >> 8U);
len_buf[1] = (uint8_t)(payload_len & 0xFFU);
*p++ = len_buf[0];
*p++ = len_buf[1];
uint16_t i;
for (i = 0; i < payload_len; i++)
{
*p++ = payload[i];
}
// CRC:先算2字节长度,再接续算payload
uint16_t crc = crc16_modbus(len_buf, 2U);
for (i = 0; i < payload_len; i++)
{
uint8_t byte = payload[i];
crc = (uint16_t)((crc >> 8U) ^ crc16_table[(crc ^ byte) & 0xFFU]);
}
*p++ = (uint8_t)(crc >> 8U);
*p++ = (uint8_t)(crc & 0xFFU);
return total_frame_size;
}
3、ProtocolParser.hpp
cpp
#ifndef PROTOCOL_PARSER_HPP
#define PROTOCOL_PARSER_HPP
#include <cstdint>
#include <cstddef>
#include <functional>
#define PROTO_HEAD0 0xAAU
#define PROTO_HEAD1 0xBBU
#define PROTO_MAX_PAYLOAD_LEN 256U
enum class ProtoState
{
WAIT_HEAD0,
WAIT_HEAD1,
READ_LEN_H,
READ_LEN_L,
READ_PAYLOAD,
READ_CRC_H,
READ_CRC_L
};
class ProtocolParser
{
public:
using FrameCallback = std::function<void(const uint8_t* payload, uint16_t len)>;
ProtocolParser();
void setCallback(FrameCallback cb);
void reset();
// 单字节喂入
void feedByte(uint8_t byte);
// 批量buffer喂入(方便测试)
void feedBuffer(const uint8_t* buf, size_t len);
static uint16_t crc16Modbus(const uint8_t* data, size_t len);
// 编码输出完整帧,返回帧总字节;返回0代表失败
static size_t encodeFrame(uint8_t* outBuf, size_t outBufSize, const uint8_t* payload, uint16_t payloadLen);
private:
ProtoState m_state;
uint16_t m_expectPayloadLen; // 需要接收的payload剩余字节数
uint16_t m_crcRead;
uint8_t m_payloadBuf[PROTO_MAX_PAYLOAD_LEN];
uint16_t m_payloadRecvCnt; // 已经收到payload字节数【新增修复】
FrameCallback m_cb;
};
#endif
4、ProtocolParser.cpp
cpp
#include "ProtocolParser.hpp"
static const uint16_t crc16_table[256] = {
0x0000,0xC0C1,0xC181,0x0140,0xC301,0x03C0,0x0280,0xC241,0xC601,0x06C0,0x0780,0xC741,0x0500,0xC5C1,0xC481,0x0440,
0xCC01,0x0CC0,0x0D80,0xCD41,0x0F00,0xCFC1,0xCE81,0x0E40,0x0A00,0xCAC1,0xCB81,0x0B40,0xC901,0x09C0,0x0880,0xC841,
0xD801,0x18C0,0x1980,0xD941,0x1B00,0xDBC1,0xDA81,0x1A40,0x1E00,0xDEC1,0xDF81,0x1F40,0xDD01,0x1DC0,0x1C80,0xDC41,
0x1400,0xD4C1,0xD581,0x1540,0xD701,0x17C0,0x1680,0xD641,0xD201,0x12C0,0x1380,0xD341,0x1100,0xD1C1,0xD081,0x1040,
0xF001,0x30C0,0x3180,0xF141,0x3300,0xF3C1,0xF281,0x3240,0x3600,0xF6C1,0xF781,0x3740,0xF501,0x35C0,0x3480,0xF441,
0x3C00,0xFCC1,0xFD81,0x3D40,0xFF01,0x3FC0,0x3E80,0xFE41,0xFA01,0x3AC0,0x3B80,0xFB41,0x3900,0xF9C1,0xF881,0x3840,
0x2800,0xE8C1,0xE981,0x2940,0xEB01,0x2BC0,0x2A80,0xEA41,0xEE01,0x2EC0,0x2F80,0xEF41,0x2D00,0xEDC1,0xEC81,0x2C40,
0xE401,0x24C0,0x2580,0xE541,0x2700,0xE7C1,0xE681,0x2640,0x2200,0xE2C1,0xE381,0x2340,0xE101,0x21C0,0x2080,0xE041,
0xA001,0x60C0,0x6180,0xA141,0x6300,0xA3C1,0xA281,0x6240,0x6600,0xA6C1,0xA781,0x6740,0xA501,0x65C0,0x6480,0xA441,
0x6C00,0xACC1,0xAD81,0x6D40,0xAF01,0x6FC0,0x6E80,0xAE41,0xAA01,0x6AC0,0x6B80,0xAB41,0x6900,0xA9C1,0xA881,0x6840,
0x7800,0xB8C1,0xB981,0x7940,0xBB01,0x7BC0,0x7A80,0xBA41,0xBE01,0x7EC0,0x7F80,0xBF41,0x7D00,0xBDC1,0xBC81,0x7C40,
0xB401,0x74C0,0x7580,0xB541,0x7700,0xB7C1,0xB681,0x7640,0x7200,0xB2C1,0xB381,0x7340,0xB101,0x71C0,0x7080,0xB041,
0x5000,0x90C1,0x9181,0x5140,0x9301,0x53C0,0x5280,0x9241,0x9601,0x56C0,0x5780,0x9741,0x5500,0x95C1,0x9481,0x5440,
0x9C01,0x5CC0,0x5D80,0x9D41,0x5F00,0x9FC1,0x9E81,0x5E40,0x5A00,0x9AC1,0x9B81,0x5B40,0x9901,0x59C0,0x5880,0x9841,
0x8801,0x48C0,0x4980,0x8941,0x4B00,0x8BC1,0x8A81,0x4A40,0x4E00,0x8EC1,0x8F81,0x4F40,0x8D01,0x4DC0,0x4C80,0x8C41,
0x4400,0x84C1,0x8581,0x4540,0x8701,0x47C0,0x4680,0x8641,0x8201,0x42C0,0x4380,0x8341,0x4100,0x81C1,0x8081,0x4040
};
uint16_t ProtocolParser::crc16Modbus(const uint8_t *data, size_t len)
{
uint16_t crc = 0xFFFFU;
for(size_t i = 0; i < len; i++)
{
uint8_t byte = data[i];
crc = static_cast<uint16_t>((crc >> 8U) ^ crc16_table[(crc ^ byte) & 0xFFU]);
}
return crc;
}
size_t ProtocolParser::encodeFrame(uint8_t* outBuf, size_t outBufSize, const uint8_t* payload, uint16_t payloadLen)
{
const size_t totalFrameSize = 2U + 2U + payloadLen + 2U;
if(outBufSize < totalFrameSize) return 0U;
if(payloadLen > PROTO_MAX_PAYLOAD_LEN) return 0U;
uint8_t* p = outBuf;
// 包头
*p++ = PROTO_HEAD0;
*p++ = PROTO_HEAD1;
// 长度大端
uint8_t lenBuf[2];
lenBuf[0] = static_cast<uint8_t>(payloadLen >> 8U);
lenBuf[1] = static_cast<uint8_t>(payloadLen & 0xFFU);
*p++ = lenBuf[0];
*p++ = lenBuf[1];
// payload
for(uint16_t i = 0; i < payloadLen; i++)
{
*p++ = payload[i];
}
// =========修复CRC:crc = lenBuf(2字节) + payload 全部连续数据========
uint16_t crc = crc16Modbus(lenBuf, 2);
if(payloadLen > 0)
{
// 注意:不能重新从头算payload,要接续crc状态
for(uint16_t i = 0; i < payloadLen; i++)
{
uint8_t byte = payload[i];
crc = static_cast<uint16_t>((crc >> 8U) ^ crc16_table[(crc ^ byte) & 0xFFU]);
}
}
*p++ = static_cast<uint8_t>(crc >> 8U);
*p++ = static_cast<uint8_t>(crc & 0xFFU);
return totalFrameSize;
}
ProtocolParser::ProtocolParser()
{
reset();
}
void ProtocolParser::setCallback(FrameCallback cb)
{
m_cb = std::move(cb);
}
void ProtocolParser::reset()
{
m_state = ProtoState::WAIT_HEAD0;
m_expectPayloadLen = 0U;
m_crcRead = 0U;
m_payloadRecvCnt = 0U;
}
void ProtocolParser::feedBuffer(const uint8_t* buf, size_t len)
{
for(size_t i = 0; i < len; i++)
{
feedByte(buf[i]);
}
}
void ProtocolParser::feedByte(uint8_t byte)
{
switch(m_state)
{
case ProtoState::WAIT_HEAD0:
if(byte == PROTO_HEAD0)
{
m_state = ProtoState::WAIT_HEAD1;
}
break;
case ProtoState::WAIT_HEAD1:
if(byte == PROTO_HEAD1)
{
m_state = ProtoState::READ_LEN_H;
}
else
{
m_state = ProtoState::WAIT_HEAD0;
}
break;
case ProtoState::READ_LEN_H:
m_expectPayloadLen = static_cast<uint16_t>(byte) << 8U;
m_state = ProtoState::READ_LEN_L;
break;
case ProtoState::READ_LEN_L:
m_expectPayloadLen |= byte;
m_payloadRecvCnt = 0U;
if(m_expectPayloadLen > PROTO_MAX_PAYLOAD_LEN)
{
// 超长帧丢弃
m_state = ProtoState::WAIT_HEAD0;
}
else if(m_expectPayloadLen == 0U)
{
m_state = ProtoState::READ_CRC_H;
}
else
{
m_state = ProtoState::READ_PAYLOAD;
}
break;
case ProtoState::READ_PAYLOAD:
{
m_payloadBuf[m_payloadRecvCnt++] = byte;
if(m_payloadRecvCnt >= m_expectPayloadLen)
{
m_state = ProtoState::READ_CRC_H;
}
break;
}
case ProtoState::READ_CRC_H:
m_crcRead = static_cast<uint16_t>(byte) << 8U;
m_state = ProtoState::READ_CRC_L;
break;
case ProtoState::READ_CRC_L:
{
m_crcRead |= byte;
// 重建参与crc校验的数据流:2字节长度 + payload
uint8_t lenBuf[2];
lenBuf[0] = static_cast<uint8_t>(m_expectPayloadLen >> 8U);
lenBuf[1] = static_cast<uint8_t>(m_expectPayloadLen & 0xFFU);
uint16_t calcCrc = crc16Modbus(lenBuf,2);
for(uint16_t i = 0; i < m_expectPayloadLen; i++)
{
uint8_t b = m_payloadBuf[i];
calcCrc = static_cast<uint16_t>((calcCrc >> 8U) ^ crc16_table[(calcCrc ^ b) &0xFFU]);
}
if(calcCrc == m_crcRead)
{
if(m_cb)
{
m_cb(m_payloadBuf, m_expectPayloadLen);
}
}
// 无论成功失败,重置,等待下一帧
m_state = ProtoState::WAIT_HEAD0;
break;
}
default:
reset();
break;
}
}
5、main.cpp
cpp
#include <iostream>
#include <cstdio>
#include <cstdint>
#include <cstddef>
#include <cstring>
#include "ProtocolParser.hpp"
#include <Windows.h>
#include "protocol.h"
/**
* @brief 打印十六进制数据
* @param title 标题前缀
* @param payload 数据指针
* @param len 数据长度
*/
static void print_hex(const char* title, const uint8_t* payload, uint16_t len)
{
printf("%s len=%u data: ", title, (unsigned int)len);
if (len == 0)
{
printf("[empty payload]");
}
else
{
for (uint16_t i = 0; i < len; i++)
{
printf("%02X ", payload[i]);
}
}
printf("\n");
}
// C版本回调
static void frame_callback(const uint8_t* payload, uint16_t len)
{
print_hex("[C解析成功]", payload, len);
}
int main()
{
SetConsoleOutputCP(CP_UTF8);
// ===================== C++ Parser 测试组 =====================
std::cout << "==================== [C++ ProtocolParser 测试] ====================" << std::endl;
ProtocolParser parser;
parser.setCallback([](const uint8_t* payload, uint16_t len) {
print_hex("[C++解析成功]", payload, len);
});
uint8_t buf[128] = { 0 };
size_t flen;
// 测试1:普通payload
{
uint8_t payload[]{ 0x55,0x66,0x77,0x88 };
flen = ProtocolParser::encodeFrame(buf, sizeof(buf), payload, sizeof(payload));
std::cout << "\n----测试1 普通帧----" << std::endl;
if (flen == 0)
{
std::cout << "encodeFrame 失败!" << std::endl;
}
else
{
parser.feedBuffer(buf, flen);
}
}
// 测试2:payload内部包含包头魔数 AA BB
{
uint8_t payload[]{ 0x01,0xAA,0xBB,0x02,0x03 };
flen = ProtocolParser::encodeFrame(buf, sizeof(buf), payload, sizeof(payload));
std::cout << "\n----测试2 payload包含包头AA BB----" << std::endl;
if (flen == 0)
{
std::cout << "encodeFrame 失败!" << std::endl;
}
else
{
parser.feedBuffer(buf, flen);
}
}
// 测试3:payload长度=0
{
flen = ProtocolParser::encodeFrame(buf, sizeof(buf), nullptr, 0);
std::cout << "\n----测试3 零长度payload----" << std::endl;
if (flen == 0)
{
std::cout << "encodeFrame 失败!" << std::endl;
}
else
{
parser.feedBuffer(buf, flen);
}
}
// 测试4:前置乱码
{
uint8_t payload[]{ 0x11,0x22 };
flen = ProtocolParser::encodeFrame(buf, sizeof(buf), payload, sizeof(payload));
std::cout << "\n----测试4 前置乱码----" << std::endl;
if (flen == 0)
{
std::cout << "encodeFrame 失败!" << std::endl;
}
else
{
parser.feedByte(0x10);
parser.feedByte(0x20);
parser.feedBuffer(buf, flen);
}
}
// ===================== C Decoder 测试组 =====================
std::cout << "\n==================== [C ProtocolDecoder_t 测试] ====================" << std::endl;
ProtocolDecoder_t dec;
proto_decoder_init(&dec, frame_callback);
uint8_t frame_buf[128] = { 0 };
size_t frame_len;
printf("\n----测试1 正常完整帧----\n");
{
uint8_t payload1[] = { 0x11,0x22,0x33 };
frame_len = proto_encode_frame(frame_buf, sizeof(frame_buf), payload1, sizeof(payload1));
if (frame_len != 0)
{
proto_decoder_put_buffer(&dec, frame_buf, frame_len);
}
}
printf("\n----测试2 payload内部包含包头0xAA 0xBB----\n");
{
uint8_t payload2[] = { 0x01,0xAA,0xBB,0x02,0x03 };
frame_len = proto_encode_frame(frame_buf, sizeof(frame_buf), payload2, sizeof(payload2));
if (frame_len != 0)
{
proto_decoder_put_buffer(&dec, frame_buf, frame_len);
}
}
printf("\n----测试3 前置乱码再跟正确帧----\n");
{
uint8_t payload3[] = { 0x55,0x66 };
frame_len = proto_encode_frame(frame_buf, sizeof(frame_buf), payload3, sizeof(payload3));
if (frame_len != 0)
{
proto_decoder_put_byte(&dec, 0x00);
proto_decoder_put_byte(&dec, 0x12);
proto_decoder_put_byte(&dec, 0x34);
proto_decoder_put_buffer(&dec, frame_buf, frame_len);
}
}
printf("\n----测试4 CRC错误帧(篡改字节,无输出)----\n");
{
uint8_t bad_frame[128];
uint8_t payload4[] = { 0x77,0x88 };
frame_len = proto_encode_frame(frame_buf, sizeof(frame_buf), payload4, sizeof(payload4));
if (frame_len != 0)
{
memcpy(bad_frame, frame_buf, frame_len);
bad_frame[4] ^= 0x01; // 修改一个字节制造CRC错误
proto_decoder_put_buffer(&dec, bad_frame, frame_len);
}
}
printf("\n----测试5 分片接收模拟粘包----\n");
{
uint8_t payload5[] = { 0xA1,0xA2,0xA3,0xA4 };
frame_len = proto_encode_frame(frame_buf, sizeof(frame_buf), payload5, sizeof(payload5));
if (frame_len != 0)
{
size_t half = frame_len / 2;
proto_decoder_put_buffer(&dec, frame_buf, half);
proto_decoder_put_buffer(&dec, frame_buf + half, frame_len - half);
}
}
printf("\n----测试6 零长度payload----\n");
{
frame_len = proto_encode_frame(frame_buf, sizeof(frame_buf), NULL, 0);
if (frame_len != 0)
{
proto_decoder_put_buffer(&dec, frame_buf, frame_len);
}
}
// ---------------- 混用场景1:C++编码组帧 → C语言解码器解析 ----------------
std::cout << "\n========== 场景1:C++编码 → C解码器解析 ==========\n";
{
// C++编码器组帧
uint8_t buf[128] = { 0 };
uint8_t cpp_payload[] = { 0xA0,0xA1,0xA2,0xA3,0xA4 };
size_t frame_len = ProtocolParser::encodeFrame(buf, sizeof(buf), cpp_payload, sizeof(cpp_payload));
if (frame_len == 0)
{
std::cout << "C++ encodeFrame 失败\n";
}
else
{
// 初始化C解码器
ProtocolDecoder_t c_dec;
proto_decoder_init(&c_dec, frame_callback);
// 将C++生成的完整帧,喂给C语言解析器
proto_decoder_put_buffer(&c_dec, buf, frame_len);
}
}
// ---------------- 混用场景2:C语言编码组帧 → C++解码器解析 ----------------
std::cout << "\n========== 场景2:C编码 → C++解码器解析 ==========\n";
{
uint8_t buf[128] = { 0 };
uint8_t c_payload[] = { 0xB0,0xB1,0xB2,0xB3 };
// C编码器组帧
size_t frame_len = proto_encode_frame(buf, sizeof(buf), c_payload, sizeof(c_payload));
if (frame_len == 0)
{
std::cout << "C proto_encode_frame 失败\n";
}
else
{
// C++解码器实例
ProtocolParser cpp_parser;
cpp_parser.setCallback([](const uint8_t* payload, uint16_t len) {
print_hex("[C++解码器收到] ", payload, len);
});
// C生成的二进制帧直接送入C++解析器
cpp_parser.feedBuffer(buf, frame_len);
}
}
// ---------------- 场景3:分片数据流跨实现测试(模拟串口字节流) ----------------
std::cout << "\n========== 场景3:分片字节流 C++编码 → C解码器分片喂入 ==========\n";
{
uint8_t buf[128] = { 0 };
uint8_t payload[] = { 0xCC,0xDD,0xEE };
size_t frame_len = ProtocolParser::encodeFrame(buf, sizeof(buf), payload, sizeof(payload));
if (frame_len > 0)
{
ProtocolDecoder_t c_dec;
proto_decoder_init(&c_dec, frame_callback);
// 模拟串口分包,一字节一字节喂给C解析器
for (size_t i = 0; i < frame_len; i++)
{
proto_decoder_put_byte(&c_dec, buf[i]);
}
}
}
// ---------------- 场景4:零长度payload跨实现互测 ----------------
std::cout << "\n========== 场景4:零长度payload C编码 → C++解析 ==========\n";
{
uint8_t buf[128] = { 0 };
size_t frame_len = proto_encode_frame(buf, sizeof(buf), nullptr, 0);
if (frame_len > 0)
{
ProtocolParser cpp_parser;
cpp_parser.setCallback([](const uint8_t* payload, uint16_t len) {
print_hex("[C++解码器收到(空帧)] ", payload, len);
});
cpp_parser.feedBuffer(buf, frame_len);
}
}
printf("\n====所有测试执行完毕====\n");
return 0;
}
6、测试结果
cpp
==================== [C++ ProtocolParser 测试] ====================
----测试1 普通帧----
[C++解析成功] len=4 data: 55 66 77 88
----测试2 payload包含包头AA BB----
[C++解析成功] len=5 data: 01 AA BB 02 03
----测试3 零长度payload----
[C++解析成功] len=0 data: [empty payload]
----测试4 前置乱码----
[C++解析成功] len=2 data: 11 22
==================== [C ProtocolDecoder_t 测试] ====================
----测试1 正常完整帧----
[C解析成功] len=3 data: 11 22 33
----测试2 payload内部包含包头0xAA 0xBB----
[C解析成功] len=5 data: 01 AA BB 02 03
----测试3 前置乱码再跟正确帧----
[C解析成功] len=2 data: 55 66
----测试4 CRC错误帧(篡改字节,无输出)----
----测试5 分片接收模拟粘包----
[C解析成功] len=4 data: A1 A2 A3 A4
----测试6 零长度payload----
[C解析成功] len=0 data: [empty payload]
========== 场景1:C++编码 → C解码器解析 ==========
[C解析成功] len=5 data: A0 A1 A2 A3 A4
========== 场景2:C编码 → C++解码器解析 ==========
[C++解码器收到] len=4 data: B0 B1 B2 B3
========== 场景3:分片字节流 C++编码 → C解码器分片喂入 ==========
[C解析成功] len=3 data: CC DD EE
========== 场景4:零长度payload C编码 → C++解析 ==========
[C++解码器收到(空帧)] len=0 data: [empty payload]
====所有测试执行完毕====
d:\user\01417804\桌面\CppProject\cppTest\x64\Debug\cppTest.exe (进程 57548)已退出,代码为 0 (0x0)。
要在调试停止时自动关闭控制台,请启用"工具"->"选项"->"调试"->"调试停止时自动关闭控制台"。
按任意键关闭此窗口. . .

二、实现原理
1、帧格式设计
本协议采用固定结构的二进制帧,帧格式如下:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头0 | 1 | 固定为 0xAA,用于帧同步 |
| 帧头1 | 1 | 固定为 0xBB,用于帧同步 |
| 长度高字节 | 1 | payload 长度的高 8 位(大端序) |
| 长度低字节 | 1 | payload 长度的低 8 位(大端序) |
| payload | 0~256 | 实际业务数据,长度由前两字节决定 |
| CRC 高字节 | 1 | CRC16-Modbus 校验值的高 8 位 |
| CRC 低字节 | 1 | CRC16-Modbus 校验值的低 8 位 |
帧头使用 0xAA 0xBB 双字节魔数,相比单字节帧头能显著降低误同步概率。长度字段采用大端序(高字节在前),便于在串口等字节流中逐字节解析。payload 最大长度为 256 字节,由 PROTO_MAX_PAYLOAD_LEN 宏统一约束。
2、CRC16-Modbus 校验算法
CRC 校验覆盖 2 字节长度字段 + payload 全部数据 ,不包含帧头。算法采用查表法实现,多项式为 CRC16-Modbus 标准多项式 0x8005(反射形式 0xA001),初始值为 0xFFFF。
cpp
uint16_t crc16_modbus(const uint8_t* data, size_t len)
{
uint16_t crc = 0xFFFFU;
size_t i;
for (i = 0; i < len; i++)
{
uint8_t byte = data[i];
crc = (uint16_t)((crc >> 8U) ^ crc16_table[(crc ^ byte) & 0xFFU]);
}
return crc;
}
查表法将每个字节的 CRC 计算压缩为一次查表和两次异或运算,相比逐位计算效率更高,适合在嵌入式等资源受限场景下使用。编码时先对 2 字节长度字段计算 CRC,再接续该 CRC 状态继续计算 payload,保证收发两端校验范围完全一致。
3、状态机解析原理
解码器采用**有限状态机(FSM)**逐字节解析,核心状态定义如下:
cpp
typedef enum
{
PROTO_WAIT_HEAD0, // 等待帧头0(0xAA)
PROTO_WAIT_HEAD1, // 等待帧头1(0xBB)
PROTO_READ_LEN_H, // 读取长度高字节
PROTO_READ_LEN_L, // 读取长度低字节
PROTO_READ_PAYLOAD, // 读取 payload 数据
PROTO_READ_CRC_H, // 读取 CRC 高字节
PROTO_READ_CRC_L // 读取 CRC 低字节
}ProtoState_e;
状态迁移流程如下:
#mermaid-svg-fSQfgeb8RO4jiYm3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fSQfgeb8RO4jiYm3 .error-icon{fill:#552222;}#mermaid-svg-fSQfgeb8RO4jiYm3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fSQfgeb8RO4jiYm3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .marker.cross{stroke:#333333;}#mermaid-svg-fSQfgeb8RO4jiYm3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fSQfgeb8RO4jiYm3 p{margin:0;}#mermaid-svg-fSQfgeb8RO4jiYm3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .cluster-label text{fill:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .cluster-label span{color:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .cluster-label span p{background-color:transparent;}#mermaid-svg-fSQfgeb8RO4jiYm3 .label text,#mermaid-svg-fSQfgeb8RO4jiYm3 span{fill:#333;color:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .node rect,#mermaid-svg-fSQfgeb8RO4jiYm3 .node circle,#mermaid-svg-fSQfgeb8RO4jiYm3 .node ellipse,#mermaid-svg-fSQfgeb8RO4jiYm3 .node polygon,#mermaid-svg-fSQfgeb8RO4jiYm3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .rough-node .label text,#mermaid-svg-fSQfgeb8RO4jiYm3 .node .label text,#mermaid-svg-fSQfgeb8RO4jiYm3 .image-shape .label,#mermaid-svg-fSQfgeb8RO4jiYm3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-fSQfgeb8RO4jiYm3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .rough-node .label,#mermaid-svg-fSQfgeb8RO4jiYm3 .node .label,#mermaid-svg-fSQfgeb8RO4jiYm3 .image-shape .label,#mermaid-svg-fSQfgeb8RO4jiYm3 .icon-shape .label{text-align:center;}#mermaid-svg-fSQfgeb8RO4jiYm3 .node.clickable{cursor:pointer;}#mermaid-svg-fSQfgeb8RO4jiYm3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .arrowheadPath{fill:#333333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fSQfgeb8RO4jiYm3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fSQfgeb8RO4jiYm3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fSQfgeb8RO4jiYm3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fSQfgeb8RO4jiYm3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .cluster text{fill:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 .cluster span{color:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fSQfgeb8RO4jiYm3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fSQfgeb8RO4jiYm3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-fSQfgeb8RO4jiYm3 .icon-shape,#mermaid-svg-fSQfgeb8RO4jiYm3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fSQfgeb8RO4jiYm3 .icon-shape p,#mermaid-svg-fSQfgeb8RO4jiYm3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fSQfgeb8RO4jiYm3 .icon-shape .label rect,#mermaid-svg-fSQfgeb8RO4jiYm3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fSQfgeb8RO4jiYm3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fSQfgeb8RO4jiYm3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fSQfgeb8RO4jiYm3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收到 0xAA
其他字节
收到 0xBB
其他字节
读取长度高字节
长度 > 256
长度 = 0
正常长度
收满 payload
读取 CRC 高字节
CRC 校验通过
CRC 校验失败
WAIT_HEAD0
WAIT_HEAD1
READ_LEN_H
READ_LEN_L
READ_CRC_H
READ_PAYLOAD
READ_CRC_L
回调通知
状态机设计的关键点:
- 逐字节驱动 :
proto_decoder_put_byte每收到一个字节就驱动状态机前进一步,天然适配串口中断或 DMA 接收等流式场景。 - 自动同步 :任何状态收到非法字节都会回退到
WAIT_HEAD0重新搜索帧头,因此即使数据流中间出现乱码,也能在下一次完整帧到来时自动恢复同步。 - 长度校验 :读取长度低字节后立即判断
expect_payload_len > PROTO_MAX_PAYLOAD_LEN,超长帧直接丢弃并回到等待帧头状态,防止缓冲区溢出。 - CRC 校验:收齐 CRC 后重新计算校验值并与接收值比对,只有校验通过才触发回调,保证数据完整性。
4、C 与 C++ 双实现的设计考量
本文同时提供 C 和 C++ 两套实现,二者在协议层面完全兼容,可交叉编解码:
| 对比项 | C 实现(protocol.c) | C++ 实现(ProtocolParser.cpp) |
|---|---|---|
| 封装方式 | 结构体 + 函数 | 类 + 成员函数 |
| 回调机制 | 函数指针 | std::function |
| 适用场景 | 嵌入式 C 工程、单片机 | C++ 工程、桌面应用 |
| 编码接口 | proto_encode_frame |
ProtocolParser::encodeFrame |
| 解码接口 | proto_decoder_put_byte/buffer |
feedByte/feedBuffer |
两套实现共享相同的帧格式、CRC 算法和状态机逻辑,因此 C 编码器生成的帧可以被 C++ 解码器正确解析,反之亦然。main.cpp 中的场景 1~4 正是为了验证这种跨实现互操作性。
5、关键实现细节
5.1 payload 接收计数
C 实现中 payload_recv_cnt 和 C++ 实现中 m_payloadRecvCnt 用于记录已接收的 payload 字节数。在 READ_LEN_L 状态收到完整长度后必须清零,否则连续接收多帧时计数会残留上一帧的值,导致 payload 接收不完整。
5.2 零长度 payload 处理
当长度字段为 0 时,解码器跳过 READ_PAYLOAD 状态直接进入 READ_CRC_H,此时 CRC 仅覆盖 2 字节长度字段。编码端 proto_encode_frame 对 payload_len == 0 同样只计算长度字段的 CRC,保证收发一致。
5.3 粘包与分片处理
- 粘包 :多个帧连续到达时,状态机解析完一帧后自动回到
WAIT_HEAD0,继续解析下一帧,无需额外拆包逻辑。 - 分片:一帧数据被拆成多次到达时,状态机在中间状态挂起,等待剩余字节补齐后再完成校验和回调,天然支持串口分片接收。
6、总结
本协议通过「双字节帧头 + 长度字段 + CRC16 校验」的组合,在简单性和可靠性之间取得了良好平衡。状态机解析方式让解码器具备自动同步、抗干扰能力,且逐字节驱动模型非常适合嵌入式串口通信场景。C 与 C++ 双实现既满足了不同工程的技术栈需求,又通过交叉测试验证了协议实现的正确性。
