# protocol_lib **Repository Path**: zhao-zhangbo/protocol_lib ## Basic Information - **Project Name**: protocol_lib - **Description**: 一种嵌入式轻量级通用hex协议解析器 - **Primary Language**: C - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 5 - **Created**: 2024-03-21 - **Last Updated**: 2026-04-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # b_protocol_core / protocol_lib `protocol_lib` 是一个用于嵌入式 C 工程的轻量级通用 HEX 协议解析库。核心模块 `b_protocol_core` 基于 `ringbuffer` 对连续字节流进行缓存、同步、组帧、帧尾匹配和校验,适合 UART、SPI、OTA、私有总线等按字节接收数据的场景。 上游仓库:https://gitee.com/zhao-zhangbo/protocol_lib 增加了skills\protocol-lib技能,可直接添加技能让AI自动部署仓库 ## 主要特点 - 支持多协议并存,每个协议使用独立的 `b_frame_type` 实例 - 支持自定义帧头 `head` 和帧尾 `end` - 支持变长帧,通过 `get_frame_len_cb` 回调计算完整帧长 - 支持自定义校验,通过 `check_cb` 回调校验完整帧 - 输入缓冲区和输出缓冲区在初始化时分别配置 - 使用环形缓冲区缓存输入字节流 - 支持空闲超时恢复,降低异常数据导致解析器卡住的概率 - 支持按实例 `log_level` 控制日志等级,且可通过 `EN_FRAME_DEBUG` 全局关闭日志 ## 仓库结构 ```text protocol_lib/ ├── b_protocol_core.c # 协议解析核心实现 ├── b_protocol_core.h # 协议解析公共 API 和类型定义 ├── ringbuffer.c # 环形缓冲区实现 ├── ringbuffer.h # 环形缓冲区接口 ├── LICENSE # Apache-2.0 License └── README.md ``` ## 快速接入 将以下源码加入工程编译: ```text b_protocol_core.c ringbuffer.c ``` 在需要解析协议的模块中包含头文件: ```c #include "b_protocol_core.h" ``` 每个协议实例至少需要: - 一个 `b_frame_type` 对象 - 一个输入缓冲区 `in_frame_buffer` - 一个输出缓冲区 `out_frame_buffer` - 一个帧长解析回调 `get_frame_len_cb` - 一个帧校验回调 `check_cb` 典型调用链: ```text b_frame_init() -> b_frame_idie_timer() 周期调用,通常 1 ms -> b_frame_put() 收到字节后写入解析器 -> b_frame_check_get() 主循环或任务中轮询完整帧 ``` ## 核心类型 ### b_frame_init_type `b_frame_init_type` 用于描述一个协议格式和它的缓冲区配置。 ```c typedef struct { const uint8_t *pname; const char *head; const char *end; uint16_t head_len; uint16_t end_len; b_get_frame_len_cb_t *get_frame_len_cb; b_check_cb_t *check_cb; uint8_t *in_frame_buffer; uint8_t *out_frame_buffer; uint16_t in_buffer_len; uint16_t out_buffer_len; uint8_t log_level; } b_frame_init_type; ``` 字段说明: - `pname`:协议名称,主要用于日志输出 - `head` / `head_len`:帧头内容和长度;`head == NULL` 时初始化会将 `head_len` 置 0 - `end` / `end_len`:帧尾内容和长度;`end == NULL` 时初始化会将 `end_len` 置 0 - `get_frame_len_cb`:帧长解析回调 - `check_cb`:完整帧校验回调 - `in_frame_buffer` / `in_buffer_len`:输入环形缓冲区 - `out_frame_buffer` / `out_buffer_len`:输出组帧缓冲区 - `log_level`:日志等级,`1` 输出原始信息,`2` 输出解析过程信息 ### b_frame_type `b_frame_type` 是协议解析器实例,内部保存协议配置、环形缓冲区、空闲计时和帧头匹配状态。 用户代码应通过公共 API 操作它,不建议直接修改 `_frame_ring`、`_idie_timer`、`_systick`、`head_match_flg` 等内部字段。 ## 回调函数 ### 帧长解析回调 ```c typedef uint16_t b_get_frame_len_cb_t(uint8_t *buffer, uint16_t len); ``` 该回调根据当前已缓存的数据计算完整帧长度。 约定: - 返回完整帧长度,包含帧头、数据、校验、帧尾等所有字节 - 当数据不足以判断帧长时返回 `0` - 返回值不应超过 `out_buffer_len` 示例: ```c static uint16_t a5_get_frame_len(uint8_t *buffer, uint16_t len) { if (len < 4) { return 0; } return (uint16_t)(2 + 1 + 1 + buffer[3] + 1 + 2); } ``` ### 帧校验回调 ```c typedef uint8_t b_check_cb_t(uint8_t *buffer, uint16_t len); ``` 该回调用于校验完整帧。 约定: - 校验成功返回 `B_SUCCESS` - 校验失败返回 `B_ERROR` 注意:当前上游源码中,`b_frame_check_get()` 只有在 `check_cb` 存在并返回 `B_SUCCESS` 时才会返回完整帧。如果协议本身没有 CRC、Checksum 等校验字段,也建议提供一个基础合法性检查函数。 ## API 说明 ### b_frame_init ```c uint8_t b_frame_init(b_frame_type *pframe, b_frame_init_type *pframeinit); ``` 初始化协议解析器。 返回值: - `B_SUCCESS`:初始化成功 - `B_ERROR`:参数错误或环形缓冲区初始化失败 初始化时会: - 检查 `pframe`、`pframeinit`、输入缓冲区、输出缓冲区是否为空 - 将 `head == NULL` 对应的 `head_len` 置 0 - 将 `end == NULL` 对应的 `end_len` 置 0 - 保存协议配置 - 初始化内部 ringbuffer - 清空输入 FIFO ### b_frame_idie_timer ```c void b_frame_idie_timer(b_frame_type *pframe); ``` 空闲计时函数。建议按 `IDIE_TIMER_US` 周期调用,当前默认配置为: ```c #define IDIE_TIMER_US 1000 ``` 也就是通常每 1 ms 调用一次。该计时用于在异常帧、长度解析失败、数据迟迟不完整时恢复解析状态。 ### b_frame_put ```c uint8_t b_frame_put(b_frame_type *pframe, uint8_t *dat, uint32_t len); ``` 将收到的原始字节写入协议解析器的输入 ringbuffer。 返回值: - `B_SUCCESS`:全部数据写入成功 - `B_ERROR`:参数错误或 ringbuffer 空间不足 当前实现中,如果 `ring_buf_put()` 实际写入长度不等于请求长度,会清空整个输入 ringbuffer 并返回 `B_ERROR`。 ### b_frame_check_get ```c const uint8_t *b_frame_check_get(b_frame_type *pframe, uint16_t *len); ``` 从输入 ringbuffer 中尝试解析一帧完整数据。 返回值: - 成功:返回 `out_frame_buffer` 指针,并通过 `len` 输出完整帧长度 - 失败或数据不足:返回 `NULL` 返回的指针指向内部输出缓冲区,下一次解析可能覆盖其内容。如果后续处理需要长期保存该帧,应及时拷贝。 ### b_frame_fifo_clear ```c void b_frame_fifo_clear(b_frame_type *pframe); ``` 清空协议解析器输入 ringbuffer。通常只在异常恢复或主动丢弃当前缓存数据时使用。 ### b_frame_fifo_get ```c uint32_t b_frame_fifo_get(b_frame_type *pframe, uint8_t *dat, uint32_t len, uint32_t timeout); ``` 从输入 FIFO 中获取指定长度数据,直到数据足够或超时。 该接口更像底层 FIFO 辅助函数,不是主解析流程必须使用的接口。常规协议解析优先使用 `b_frame_check_get()`。 ## b_frame_check_get 解析流程 `b_frame_check_get()` 是协议解析的核心。它的主要流程如下: 1. 如果当前还没有匹配帧头,则调用内部 `b_check_head()`。 2. `b_check_head()` 从 ringbuffer 中逐字节读取并匹配 `head`。 3. 帧头匹配成功后,将帧头复制到 `out_frame_buffer`,并设置 `head_match_flg = 1`。 4. 使用 `ring_buf_check_get()` 偷看 ringbuffer 中剩余数据,不立即消费。 5. 调用 `get_frame_len_cb()` 计算完整帧长度。 6. 内部用完整帧长度减去 `head_len`,得到除帧头外还需要的长度 `unhead_frame_len`。 7. 如果数据不足或回调返回 0,则继续等待;若 `_idie_timer` 超过阈值,则重置当前匹配状态。 8. 数据足够后,根据 `end` 和 `end_len` 检查帧尾。 9. 帧尾匹配后调用 `check_cb()`。 10. `check_cb()` 返回 `B_SUCCESS` 时,消费 ringbuffer 中对应数据,返回完整帧指针。 11. 任意步骤失败时,返回 `NULL`,并在部分场景下丢弃 1 字节或重置帧头匹配状态以恢复同步。 ## 示例:变长协议 假设协议格式为: ```text A5 5A CMD LEN DATA... XOR 0D 0A ``` 其中: - 帧头:`A5 5A` - `CMD`:命令字,1 字节 - `LEN`:数据区长度,1 字节 - `DATA`:负载,N 字节 - `XOR`:从 `CMD` 到最后一个 `DATA` 字节的异或校验 - 帧尾:`0D 0A` 示例代码: ```c #include "b_protocol_core.h" static b_frame_type a5_frame; static uint8_t a5_in_buf[256]; static uint8_t a5_out_buf[256]; static uint16_t a5_get_frame_len(uint8_t *buffer, uint16_t len) { if (len < 4) { return 0; } return (uint16_t)(2 + 1 + 1 + buffer[3] + 1 + 2); } static uint8_t a5_check_cb(uint8_t *buffer, uint16_t len) { uint8_t xor_value = 0; if (len < 7) { return B_ERROR; } for (uint16_t i = 2; i < len - 3; i++) { xor_value ^= buffer[i]; } return (xor_value == buffer[len - 3]) ? B_SUCCESS : B_ERROR; } void a5_protocol_init(void) { b_frame_init_type init = { .pname = (const uint8_t *)"A5", .head = "\xA5\x5A", .head_len = 2, .end = "\x0D\x0A", .end_len = 2, .get_frame_len_cb = a5_get_frame_len, .check_cb = a5_check_cb, .in_frame_buffer = a5_in_buf, .out_frame_buffer = a5_out_buf, .in_buffer_len = sizeof(a5_in_buf), .out_buffer_len = sizeof(a5_out_buf), .log_level = 0, }; (void)b_frame_init(&a5_frame, &init); } void a5_rx_bytes(uint8_t *data, uint16_t len) { (void)b_frame_put(&a5_frame, data, len); } void a5_1ms_timer(void) { b_frame_idie_timer(&a5_frame); } void a5_poll(void) { uint16_t frame_len = 0; const uint8_t *frame = b_frame_check_get(&a5_frame, &frame_len); if (frame != NULL) { handle_a5_frame(frame, frame_len); } } ``` ## 示例:定长协议 假设协议为固定 8 字节: ```text 68 ADDR CMD D0 D1 D2 D3 SUM ``` 示例代码: ```c #include "b_protocol_core.h" static b_frame_type fixed8_frame; static uint8_t fixed8_in_buf[128]; static uint8_t fixed8_out_buf[8]; static uint16_t fixed8_get_frame_len(uint8_t *buffer, uint16_t len) { (void)buffer; (void)len; return 8; } static uint8_t fixed8_check_cb(uint8_t *buffer, uint16_t len) { uint8_t sum = 0; if (len != 8) { return B_ERROR; } for (uint16_t i = 0; i < 7; i++) { sum += buffer[i]; } return (sum == buffer[7]) ? B_SUCCESS : B_ERROR; } void fixed8_protocol_init(void) { b_frame_init_type init = { .pname = (const uint8_t *)"FIXED8", .head = "\x68", .head_len = 1, .end = NULL, .end_len = 0, .get_frame_len_cb = fixed8_get_frame_len, .check_cb = fixed8_check_cb, .in_frame_buffer = fixed8_in_buf, .out_frame_buffer = fixed8_out_buf, .in_buffer_len = sizeof(fixed8_in_buf), .out_buffer_len = sizeof(fixed8_out_buf), .log_level = 0, }; (void)b_frame_init(&fixed8_frame, &init); } ``` ## RingBuffer 接口 `ringbuffer` 是协议解析器底层 FIFO。 ```c bool ring_buf_init(ring_buf_t *r, unsigned char *buf, unsigned int size); void ring_buf_clr(ring_buf_t *r); unsigned int ring_buf_len(ring_buf_t *r); unsigned int ring_buf_free_space(ring_buf_t *r); unsigned int ring_buf_put(ring_buf_t *r, unsigned char *buf, unsigned int len); unsigned int ring_buf_get(ring_buf_t *r, unsigned char *buf, unsigned int len); unsigned int ring_buf_check_get(ring_buf_t *r, unsigned char *buf, unsigned int len); unsigned int ring_buf_clr_len(ring_buf_t *r, unsigned int len); unsigned char *ring_buf_peek(ring_buf_t *r, unsigned int offset, unsigned int *len); ``` 说明: - `ring_buf_put()` 写入数据,返回实际写入长度 - `ring_buf_get()` 读取并消费数据 - `ring_buf_check_get()` 读取但不消费数据 - `ring_buf_clr_len()` 丢弃指定长度数据 - `ring_buf_peek()` 返回从指定偏移开始的一段连续内存 ## 配置宏 ```c #define B_SUCCESS 0 #define B_ERROR 1 #define EN_FRAME_DEBUG 0 #define IDIE_TIMER_US 1000 #define FRAME_BAUD_RATE 9600 #define IDIE_FACTOR 4 ``` 说明: - `EN_FRAME_DEBUG`:为 0 时日志宏被编译为空 - `IDIE_TIMER_US`:空闲计时调用周期,默认 1000 us - `FRAME_BAUD_RATE`:用于计算空闲恢复阈值 - `IDIE_FACTOR`:空闲因子,用于降低误匹配导致的卡死风险 启用 `EN_FRAME_DEBUG` 时,工程需要提供 `xprintf()`。 ## 多协议使用建议 每个协议都应独立配置: - 独立的 `b_frame_type` - 独立的输入缓冲区 - 独立的输出缓冲区 - 独立的 `b_frame_init_type` - 独立的 `get_frame_len_cb` - 独立的 `check_cb` 不要让多个协议共用同一个 `b_frame_type`,否则会共享 ringbuffer、空闲计时和帧头匹配状态,导致解析互相影响。 ## 注意事项 - 建议 `in_buffer_len` 直接使用 2 的 N 次幂,例如 64、128、256、512 - `out_buffer_len` 必须能容纳最大完整帧 - `get_frame_len_cb()` 返回完整帧长度,不是 payload 长度 - 当前源码中 `check_cb == NULL` 时不会返回完整帧 - `b_frame_put()` 在 ringbuffer 空间不足时会清空输入缓存 - 若协议没有帧头,设置 `head = NULL`;若协议没有帧尾,设置 `end = NULL` - 当前 Gitee `ringbuffer.c` 的变更记录写明“非 2 的 N 次幂会向下取最近的 2 的 N 次幂”,但建议仍直接传入 2 的 N 次幂,避免不同版本实现差异 - `b_frame_fifo_get()` 内部使用 `uint8_t ring_len` 和 `uint8_t getlen`,如果输入 FIFO 可能超过 255 字节,建议检查该实现是否满足项目需求 ## License 本项目使用 Apache-2.0 License。详见 `LICENSE`。