# 协议收发库 **Repository Path**: SiaZhang/protocol-transceiver-driver ## Basic Information - **Project Name**: 协议收发库 - **Description**: 一个用于通信的库,自带协议 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-27 - **Last Updated**: 2026-07-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # my_protocol — 协议收发库 面向嵌入式/设备通信的轻量协议收发库。特点: - **固定帧格式**,CRC8 校验,便于跨端互通 - **无动态内存**,全静态,行为可预测 - **双向分包**:请求/回复都支持任意长度(>256 自动拆片),**库内重组,应用层无感** - **ACK/超时/重传** 完整,支持请求-回复模式与单向发送模式 - **轮询驱动**,单线程协作式,主循环周期调用 `ProtocolProcess` 即可 --- ## 1. 协议帧格式 1 字节对齐,小端: | 字段 | 字节 | 说明 | |---|---|---| | HEAD | 1 | 0x55 | | LEN | 2 | 整帧总长(含 HEAD~TAIL),小端 | | VER | 1 | 版本(当前 0x01) | | CFG | 1 | 配置位(见下) | | SEQ | 2 | 序列号,每片递增,ACK 匹配用,小端 | | CMD | 2 | 指令 ID,小端 | | DATA | 0~256 | 业务数据 | | CHECK | 1 | CRC8(多项式 0x07,初值 0x00,校验范围 HEAD~DATA,即除 CHECK/TAIL 外全部) | | TAIL | 1 | 0xAA | - 最小帧长 11 字节(无数据),最大 267 字节(满载 256 数据) - `PROTOCOL_DATALEN_MAX=256` / `PROTOCOL_FRAME_MINLEN=11` / `PROTOCOL_FRAME_MAXLEN=267` ### 配置位 CFG | Bit7~4 | Bit3~2 | Bit1 | Bit0 | |---|---|---|---| | 保留 | 分片状态 frag_state | 应答帧 is_ACK | 期望回ACK expect_ack | - **Bit0 expect_ack**:1=期望对方回ACK(请求/回复);0=不期望(单向请求/纯ACK,终止) - **Bit1 is_ACK**:0=请求类帧;1=响应类帧(纯ACK 或 回复) - **Bit3~2 frag_state**:`00`包中 / `01`包头 / `10`包尾 / `11`单帧(不分包) ### 帧类型 | 帧类型 | is_ACK | expect_ack | frag_state | 用途 | |---|---|---|---|---| | 请求帧 | 0 | 1 | 单/头/中/尾 | A 发命令,B 回复 | | 单向请求 | 0 | 0 | 单 | A 单向发(≤256),B 收到不回 | | 纯 ACK | 1 | 0 | 单 | 确认分片收到,终止(不套娃) | | 回复帧 | 1 | 1 | 单/头/中/尾 | B 回复(空/短/长),A 回纯ACK | --- ## 2. 核心 API ```c // 初始化: 注入时间/收/发函数 + 回调表 Protocol_Result_t ProtocolInit(Protocol_Ctx_t *ctx, Protocol_Config_t config, Protocol_GetTime_t get_time, Protocol_Receive_t receive, Protocol_Send_t send, const Protocol_Cmd_Callback_t *callback_table); // 主驱动: 每周期调用一次, 内部依次 RxProcess(收) + TxProcess(发) Protocol_Result_t ProtocolProcess(Protocol_Ctx_t *ctx); // 注册/批量注册命令回调(含收请求重组缓冲) Protocol_Result_t ProtocolRegisterCmdCallback(Protocol_Ctx_t *ctx, uint16_t cmd, Protocol_CmdCallback_t callback, uint8_t *rx_buf, uint16_t rx_buf_size); Protocol_Result_t ProtocolRegisterCmdCallbackTable(Protocol_Ctx_t *ctx, const Protocol_Cmd_Callback_t *callback_table); // 发送一条业务指令(任意长度, >256 自动拆片) Protocol_Result_t ProtocolSendCmd(Protocol_Ctx_t *ctx, const Protocol_Cmd_Config_t *cfg, uint16_t cmd, const uint8_t *data, uint16_t data_len); ``` ### 回调签名 ```c // 接收方: 收到完整请求(已重组), 返回回复(任意长度) typedef Protocol_Result_t (*Protocol_CmdCallback_t)( const uint8_t *data, uint16_t len, const uint8_t **reply, uint16_t *reply_len); // 请求方: 收到完整回复(已重组) typedef Protocol_Result_t (*Protocol_ReplyCallback_t)(const uint8_t *data, uint16_t len); // 超时(整包失败 / 等回复超时) typedef Protocol_Result_t (*Protocol_FrameCallback_t)(const Protocol_Frame_t *frame); // 平台接口 typedef Protocol_Time_t (*Protocol_GetTime_t)(void); // 返回 ms 时间戳 typedef uint16_t (*Protocol_Receive_t)(uint8_t *data, uint16_t readlen); typedef Protocol_Result_t (*Protocol_Send_t)(uint8_t *data, uint16_t datalen); ``` ### 发送配置 `Protocol_Cmd_Config_t` | 字段 | 说明 | |---|---| | `expect_ack` | 1=等回复(默认需显式设);0=单向发不回(仅单帧≤256) | | `retry_max` | 单片最大重试次数(不含首次),0=不重传 | | `timeout_ms` | 单片重传超时(ms),`WAIT_ACK` 用 | | `reply_timeout_ms` | 等回复总超时(ms),`WAIT_REPLY` 用;0=同 `timeout_ms` | | `reply_callback` | 收到完整回复时调 | | `timeout_callback` | 整包失败/等回复超时时调 | | `reply_buf` / `reply_buf_size` | 收回复的重组缓冲(应用提供;长回复必需) | > 注意:`expect_ack` 默认 0(单向)。**等回复模式必须显式设 `.expect_ack=1`**。 --- ## 3. 使用步骤 ### 3.1 准备平台接口 ```c static Protocol_Time_t my_get_time(void){ return HAL_GetTick(); } // ms static uint16_t my_recv(uint8_t *d, uint16_t n){ return uart_read(d, n); } static Protocol_Result_t my_send(uint8_t *d, uint16_t n){ uart_write(d, n); return eOk; } ``` ### 3.2 写命令回调(接收方侧) ```c static uint8_t rx_buf[4096]; // 收请求的重组缓冲(长请求必需; 短命令可小或与回复共用) static uint8_t echo_store[4096]; static Protocol_Result_t on_cmd(const uint8_t *data, uint16_t len, const uint8_t **reply, uint16_t *reply_len){ // data 是已重组的完整请求(可能 >256) process_request(data, len); memcpy(echo_store, data, len); // 回复数据需在库分片发送期间有效 *reply = echo_store; *reply_len = len; // 0=空回复; >256 库自动拆回复分片 return eOk; } static const Protocol_Cmd_Callback_t cb_table[] = { {0x0001, on_cmd, rx_buf, sizeof(rx_buf), 0, 0, 0}, {0x0002, on_cmd, rx_buf, sizeof(rx_buf), 0, 0, 0}, PROTOCOL_CMD_CALLBACK_END, // 结束标记 }; ``` ### 3.3 初始化 + 主循环 ```c static Protocol_Ctx_t ctx; void app_init(void){ ProtocolInit(&ctx, (Protocol_Config_t){.send_interval = 0}, my_get_time, my_recv, my_send, cb_table); } void app_loop(void){ for (;;) { ProtocolProcess(&ctx); // 周期驱动收发 // ... 其他任务 } } ``` ### 3.4 发送请求(等回复) ```c static uint8_t reply_buf[4096]; static Protocol_Result_t on_reply(const uint8_t *data, uint16_t len){ // data 是已重组的完整回复 return eOk; } static Protocol_Result_t on_timeout(const Protocol_Frame_t *f){ // 请求失败/回复超时 return eOk; } void app_request(const uint8_t *req, uint16_t len){ Protocol_Cmd_Config_t cfg = { .expect_ack = 1, // 等回复 .retry_max = 3, .timeout_ms = 300, .reply_timeout_ms = 3000, .reply_callback = on_reply, .timeout_callback = on_timeout, .reply_buf = reply_buf, .reply_buf_size = sizeof(reply_buf), }; ProtocolSendCmd(&ctx, &cfg, 0x0001, req, len); // len 任意, >256 自动拆片 } ``` ### 3.5 单向发送(不等回复,如心跳) ```c void app_heartbeat(void){ uint8_t msg[] = "PING"; Protocol_Cmd_Config_t cfg = { .expect_ack = 0, // 单向: 发一次即出队, 不重传不等 .retry_max = 0, .timeout_ms = 0, .reply_callback = NULL, .timeout_callback = NULL, .reply_buf = NULL, .reply_buf_size = 0, }; ProtocolSendCmd(&ctx, &cfg, 0x0001, msg, sizeof(msg)-1); // ≤256, 长数据会被拒绝 } ``` --- ## 4. 分包机制(库内重组,应用无感) - **发送**:`ProtocolSendCmd` 数据 >256 自动按 256 拆片(HEAD/MIDDLE/TAIL),每片独立 seq、need_ACK=1。 - **接收方**:库用注册时给的 `rx_buf` 重组请求,收齐(TAIL)才调 `CmdCallback`,回调拿到的 `data` 是完整数据。 - **回复**:回调返回 `reply_len`>256 时,库自动拆回复分片发送;请求方用 `reply_buf` 重组,收齐调 `reply_callback`。 - **去重**:HEAD/MIDDLE/TAIL 都按 `seq<=last_seq` 去重,旧/重复片回纯ACK不重组(防重传残留覆盖)。 - **长回复时序**:B 先纯ACK确认请求末片,再发回复分片(seq=回复自己);A 末片确认后转入 ctx 级等回复,不阻塞发送队列(防双向死锁)。 > 单向发送(`expect_ack=0`)不支持分片--分片依赖 ACK-gating 串行确认,与单向不兼容。单向长数据 `ProtocolSendCmd` 返回 `eFail`。 --- ## 5. 超时与重传 - **单片重传**(`WAIT_ACK`):某片发后 `timeout_ms` 内没收到 ACK,重传,最多 `retry_max` 次;耗尽触发 `timeout_callback`(整包失败,跳过本包剩余片)。 - **等回复超时**(`WAIT_REPLY`,ctx 级):末片确认后等回复,`reply_timeout_ms` 内没收齐触发 `timeout_callback`。计时只在收到**新**回复片时重置(重复片不重置,防永不超时)。 - 单向发送(`expect_ack=0`)不进 `WAIT_ACK`,发一次即出队,不重传不超时。 --- ## 6. 限制与注意事项 - **单线程协作式**:`ProtocolProcess` 需周期调用;收发共享同一节拍,吞吐与调用频率相关。底层 `send`/`receive` 不应长时间阻塞(否则拖累另一端)。 - **一个 ctx 同时只处理一条在途请求的回复**:`reply_buf`/`reply_callback` 是 ctx 级共享,新请求会覆盖;等上一条回复收齐/超时再发下一条。 - **回复分片顺序 ACK 门控**:回复分片一片片发、每片等 ACK 再发下一片。丢包多时长回复可能超时(设计特性,非 bug);要扛更高丢包需宽松 `reply_timeout_ms`,或改流水线。 - **seq 单调**:从 1 起(`ProtocolInit` 置 `tx.seq=1`),收方去重 `seq<=last_seq`。uint16 理论会回绕(极长生命周期),目前未特殊处理。 - **回调不可重入**:回调在 `ProtocolProcess` 内同步执行,回调里不要再调 `ProtocolSendCmd` 同一 ctx(会改队列状态);如需回复式触发,建议置标志、主循环再发。 --- ## 7. 缓冲与队列容量 | 项 | 容量 | 说明 | |---|---|---| | `rx_cache` | `FRAME_MAXLEN*3`(~801B) | 接收字节环形缓存 | | 接收帧 FIFO | 10 | 已校验帧队列 | | 发送命令 FIFO | 32 | 待发分片/命令(分片占多槽,故加大) | | 发送帧 FIFO | 32 | 已编码待发字节帧 | | 回调表 | 16 | 最多注册 16 条 cmd | 应用需提供的缓冲:每条 cmd 的 `rx_buf`(收请求重组)、每次请求的 `reply_buf`(收回复重组)。大小按业务最长数据定。 --- ## 8. 目录与构建 ``` my_protocol/ ├── inc/my_protocol.h # 公共接口与类型 ├── src/my_protocol.c # 实现 └── test/ ├── test_my_protocol.c # 交互式测试(选串口、CMD 收发) ├── test_auto.c # 自动测试(单进程双 ctx, 25+ 用例) ├── test_bidir.c # 双线程全双工测试(双向+丢包扫描) └── ... ``` CMake 构建(宿主测试): ```bash cmake -S . -B build && cmake --build build --config Debug ./build/test/test_auto.exe # 自动测试 ./build/test/test_bidir.exe # 双向全双工测试 ./build/test/my_protocol_test.exe # 交互式 ``` 移植到固件:把 `inc/` `src/` 加入工程,实现 `get_time`/`receive`/`send` 三个平台函数,主循环调 `ProtocolProcess`。无需 C 标准库动态内存。