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

简易嵌入式自定义协议设计思路!

01/09 09:24
378
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

嵌入式系统开发中,自定义通信协议是板间通信的核心基础设施。本文介绍如何设计一个高度实用且具备良好扩展性的自定义协议。

1. 简易自定义协议设计

1.1 协议设计原则

字节序一致性:跨平台通信必须明确字节序(本实现采用小端序)

固定宽度类型:使用uint8_tuint16_t等固定宽度类型,确保跨平台一致性

静态内存分配:嵌入式环境避免使用动态内存,防止内存碎片

流式解析支持:实际通信中数据是流式到达的,需要状态机处理粘包/断包

完善的错误处理:统一的错误码体系,便于问题定位

1.2 ITLV字段定义

字段 含义 典型长度 说明
I ID/Index 1~2 字节 数据标识符,区分不同类型的数据
T Type 1 字节 数据类型(uint8、int32、string等)
L Length 1~4 字节 Value字段的长度
V Value N 字节 实际数据负载(Payload)

其中,I、T、L是固定长度字段,在制定协议前需要评估:项目数据种类有多少(决定I的位宽)、单条数据最大长度是多少(决定L的位宽)。预留扩展空间可保证协议的通用性。一般I设置为1~2字节,T设置为1字节,L设置为1~4字节。

只用ITLV四个字段够吗? 这取决于应用场景:

场景一:物联网端云通信。基于MQTT/TCP的通信中,只用ITLV四个字段是可行的。因为TCP协议本身提供校验和重传机制,保证了传输可靠性;平台SDK在传输用户数据时会自动添加消息边界标识。开发者只需关注ITLV这一层即可。

场景二:嵌入式板间通信。串口、CAN等通信方式没有TCP的可靠性保障,电磁干扰可能导致数据出错。此时必须增加:

包头(Header):用于帧同步和边界识别

校验字段(CRC):用于检测数据错误

根据实际需求还可扩展:分包传输需要包序号、多板通信需要目标地址等。

1.3 协议帧格式

我们一起设计一个自定义协议,用于嵌入式板间通信。帧格式:

字段 长度 说明
Head 2 字节 固定为 0x55, 0xAA
ID 1 字节 协议标识符
Type 1 字节 数据类型
Length 1 字节 Payload长度(最大255字节)
Value/Payload N 字节 实际数据
CRC16 2 字节 CRC16-X25校验(小端序)

1.4 流式解析与一次性解析

根据数据来源的不同,协议库提供两种解析方式:

对比维度 一次性解析 流式解析
输入数据 完整帧 逐字节
状态管理 无状态 状态机驱动
缓冲区 依赖调用者提供 内部缓冲
粘包/断包 不支持 自动处理
适用场景 UDP、文件读取 串口、TCP流

一次性解析:已经拿到完整的数据帧时使用。比如从文件中读取的协议数据。优点是调用简单、无状态,缺点是必须确保输入数据完整。

流式解析:当数据是"一点一点"到达时使用。比如串口中断每次只收到1个字节。状态机会"记住"已收到的内容,自动处理粘包(多帧粘连)和断包(一帧分多次到达)。

1.5 数据结构设计

1.5.1 跨平台打包属性

编译器结构体打包属性定义,确保结构体按1字节对齐,消除填充字节

#if defined(__GNUC__) || defined(__clang__)
    #define PACKED_STRUCT __attribute__((packed))
#elif defined(_MSC_VER)
    #define PACKED_STRUCT
    #pragma pack(push, 1)
#else
    #define PACKED_STRUCT
    #warning"Unknown compiler, packed attribute may not work correctly"
#endif
1.5.2 类型定义(使用固定宽度类型)

TLV数据类型定义使用固定宽度类型。使用固定宽度uint8_t确保跨平台一致性,避免使用enum,因为enum的大小是编译器相关的

typedefuint8_ttlv_type_t;

#define TLV_TYPE_UINT8      ((tlv_type_t)0x00)  // 无符号8位整数
#define TLV_TYPE_INT8       ((tlv_type_t)0x01)  // 有符号8位整数
#define TLV_TYPE_UINT16     ((tlv_type_t)0x02)  // 无符号16位整数
#define TLV_TYPE_INT16      ((tlv_type_t)0x03)  // 有符号16位整数
#define TLV_TYPE_UINT32     ((tlv_type_t)0x04)  // 无符号32位整数
#define TLV_TYPE_INT32      ((tlv_type_t)0x05)  // 有符号32位整数
#define TLV_TYPE_STRING     ((tlv_type_t)0x06)  // 字符串类型
#define TLV_TYPE_FLOAT      ((tlv_type_t)0x07)  // 浮点类型
#define TLV_TYPE_BYTES      ((tlv_type_t)0x08)  // 字节数组
1.5.3 协议数据结构
typedefstruct
{
    protocol_id_t id;                               // 协议ID
    tlv_type_t    type;                             // 数据类型
    uint8_t       length;                           // 数据长度
    uint8_t       payload[PROTOCOL_VALUE_MAX_LEN];  // 负载数据
} protocol_data_t;

用于组包和解包的数据容器,业务层可以通过 payload 访问原始字节数据。

1.5.4 错误码定义
typedefenum
{
    PROTO_OK                =  0,   // 操作成功
    PROTO_ERR_NULL_PTR      = -1,   // 空指针错误
    PROTO_ERR_BUF_TOO_SMALL = -2,   // 缓冲区太小
    PROTO_ERR_INVALID_HEAD  = -3,   // 无效的包头
    PROTO_ERR_CRC_MISMATCH  = -4,   // CRC校验失败
    PROTO_ERR_INVALID_ID    = -5,   // 无效的协议ID
    PROTO_ERR_PAYLOAD_SIZE  = -6,   // 负载大小错误
    PROTO_ERR_IN_PROGRESS   = -7,   // 解析进行中
    PROTO_ERR_INVALID_LEN   = -8,   // 无效的数据长度
} protocol_err_e;
1.5.5 流式解析器定义

流式解析的核心是状态机。每收到一个字节,状态机根据当前状态决定下一步动作:

当遇到错误数据自动回到IDLE重新开始,这就实现了噪声过滤。

typedefenum
{
    PARSE_STATE_IDLE = 0,       // 空闲状态
    PARSE_STATE_HEAD1,          // 等待包头第一字节
    PARSE_STATE_HEAD2,          // 等待包头第二字节
    PARSE_STATE_ID,             // 接收ID
    PARSE_STATE_TYPE,           // 接收Type
    PARSE_STATE_LENGTH,         // 接收Length
    PARSE_STATE_PAYLOAD,        // 接收Payload
    PARSE_STATE_CRC_LOW,        // 接收CRC低字节
    PARSE_STATE_CRC_HIGH,       // 接收CRC高字节
} parse_state_e;

typedefstruct
{
    parse_state_e state;                    // 当前解析状态
    uint8_t       buffer[PROTOCOL_MAX_LEN]; // 接收缓冲区
    uint16_t      index;                    // 当前接收索引
    uint8_t       payload_len;              // 期望的负载长度
} protocol_parser_t;

1.6 CRC16校验

CRC(循环冗余校验)用于检测数据传输错误。本协议采用CRC16-X25算法,使用查表法提高效率。

校验范围:从包头开始到Payload结束(不含CRC本身)

1.7 API接口设计

协议库提供3类API:组包、一次性解包、流式解析。

1.7.1 组包
/**
 * @brief  协议数据组包
 * @param  buf       输出缓冲区指针
 * @param  buf_size  缓冲区大小
 * @param  data      协议数据结构
 * @param  out_len   实际输出长度(输出)
 * @return PROTO_OK: 成功, 其他: 错误码
 */
protocol_err_e protocol_pack(uint8_t *buf,
                              size_t buf_size,
                              constprotocol_data_t *data,
                              size_t *out_len);

1.7.2 一次性解包
/**
 * @brief  一次性解包
 * @param  buf   输入缓冲区指针
 * @param  len   数据长度
 * @param  data  协议数据结构(输出)
 * @return PROTO_OK: 成功, 其他: 错误码
 */
protocol_err_e protocol_unpack(constuint8_t *buf,
                                size_t len,
                                protocol_data_t *data);
1.7.3 流式解析
/**
 * @brief  初始化解析器
 * @param  parser  解析器实例指针
 * @return PROTO_OK: 成功, 其他: 错误码
 */
protocol_err_e protocol_parser_init(protocol_parser_t *parser);

/**
 * @brief  重置解析器状态
 * @param  parser  解析器实例指针
 */
void protocol_parser_reset(protocol_parser_t *parser);

/**
 * @brief  流式解析 - 逐字节输入
 * @param  parser  解析器实例指针
 * @param  byte    输入字节
 * @return PROTO_OK: 帧完成, PROTO_ERR_IN_PROGRESS: 解析中, 其他: 错误码
 */
protocol_err_e protocol_parse_byte(protocol_parser_t *parser, uint8_t byte);

/**
 * @brief  从解析器提取帧数据
 * @param  parser  解析器实例指针
 * @param  data    协议数据结构(输出)
 * @return PROTO_OK: 成功, 其他: 错误码
 */
protocol_err_e protocol_parser_get_frame(constprotocol_parser_t *parser,
                                          protocol_data_t *data);

2. ITLV组包、解包测试

本文demo可在公众号聊天对话框输入关键字:简易嵌入式自定义协议设计思路,即可获取。

下面通过两个典型场景演示协议的使用。

2.1 业务数据定义

首先定义业务层的数据结构。注意使用# pragma pack确保结构体按1字节对齐:

// 业务协议ID定义
#define CMD_ID_LED_CTRL   (protocol_id_t)0x01
#define CMD_ID_DATE_TIME  (protocol_id_t)0x02

#pragma pack(push, 1)
typedefstruct
{
    uint8_t led_id;     // LED编号
    uint8_t on_off;     // 0=关闭, 1=打开
} led_ctrl_t;

typedefstruct
{
    uint16_t year;
    uint8_t  month;
    uint8_t  day;
    uint8_t  hour;
    uint8_t  minute;
    uint8_t  second;
    uint8_t  reserved;
} datetime_t;
#pragma pack(pop)

2.2 一次性解析测试

核心代码:

// 准备数据
led_ctrl_t led_cmd = { .led_id = 1, .on_off = 1 };
tx_data.id = CMD_ID_LED_CTRL;
tx_data.type = TLV_TYPE_BYTES;
tx_data.length = sizeof(led_cmd);
memcpy(tx_data.payload, &led_cmd, sizeof(led_cmd));

// 组包
protocol_pack(tx_buf, sizeof(tx_buf), &tx_data, &frame_len);
printf("Pack: ID=0x%02X, LED%d=%s, len=%zun", 
       tx_data.id, led_cmd.led_id, 
       led_cmd.on_off ? "ON" : "OFF", frame_len);
printf("Pack: ");
protocol_print_hex(tx_buf, frame_len);

// 解包
ret = protocol_unpack(tx_buf, frame_len, &rx_data);
if (ret == PROTO_OK)
{
    led_ctrl_t *rx_led = (led_ctrl_t*)rx_data.payload;
    printf("Unpack: ID=0x%02X, LED%d=%snn", 
           rx_data.id, rx_led->led_id, 
           rx_led->on_off ? "ON" : "OFF");
}

运行结果:

2.3 流式解析测试

模拟数据逐字节到达,状态机累积处理。

核心代码:

protocol_parser_t parser;
protocol_parser_init(&parser);

// 模拟逐字节接收
for (size_t i = 0; i < frame_len; i++)
{
    protocol_err_e ret = protocol_parse_byte(&parser, tx_buf[i]);
    
    if (ret == PROTO_OK)
    {
        // 帧接收完成
        protocol_parser_get_frame(&parser, &rx_data);
        led_ctrl_t *rx_led = (led_ctrl_t*)rx_data.payload;
        printf("Unpack: ID=0x%02X, LED%d=%s (parsed at byte %zu)nn",
               rx_data.id, rx_led->led_id,
               rx_led->on_off ? "ON" : "OFF", i + 1);
        break;
    }
}

运行结果:

本文demo可在公众号聊天对话框输入关键字:简易嵌入式自定义协议设计思路,即可获取。

3. 局限性与优化方向

本ITLV协议是个最小实现,轻量简洁,可以适用于短距离、低误码率的嵌入式板间通信。如果要应用于其它更广泛的场景,会有一些局限性。

下面讨论一些局限性及可优化点:

3.1 字段容量限制

字段 当前设计 局限性 优化方向
ID 1字节 (0~255) 最多256种数据类型 扩展为2字节,支持65536种
Length 1字节 (0~255) 单帧最大255字节 扩展为2字节,或引入分包机制
Type 1字节 当前仅做标记,未强制校验 可用于自动类型转换(大小端等)

对于数据种类<256、单条数据<255字节的系统,当前设计完全够用。超出范围时需扩展字段位宽。

3.2 可靠性机制不够完善

当前协议:

可优化方向:

3.3 状态机健壮性

当前状态机无超时机制。潜在问题:

3. 总结

本文介绍的ITLV协议具有以下特点:

特性 说明
简洁高效 最小帧7字节,开销合理
静态内存 无动态分配,适合嵌入式
流式解析 状态机自动处理粘包/断包
CRC校验 保证数据完整性
跨平台 固定宽度类型、打包属性

适用场景:短距离、低误码率的嵌入式板间通信(串口、SPII2C等)。

不适用场景:高可靠性要求、大数据传输、多设备组网、安全敏感的场合。

我是杂烩君,专注嵌入式软件开发,分享实用的技术干货。

相关推荐

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

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