# YMFDTP **Repository Path**: yao_mi/ymfdtp ## Basic Information - **Project Name**: YMFDTP - **Description**: 传统固定结构体方案在机器人迭代中极其痛苦:新增一个字段,所有后续字段偏移改变,上下位机必须同步重编。YMFDTP 用数据 ID + 字典解决此问题——每个数据有独立 ID,新增字段不影响旧字段解析;从机可按 1~127ms 周期自动上报高频状态,无需主机反复轮询;流式状态机自动跳过噪声和坏包。专为串口/无线通信设计,轻量、高容错,让机器人硬件和软件独立演进。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-25 - **Last Updated**: 2026-09-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # YMFDTP **YMFDTP** 的中文简称是“妖米快速数据传输协议”,英文全称为 **your mini fast data transfer protocol**。 这是一个跨平台的数据打包、解包和校验库,适用于串口等通信场景。它只负责数据如何组织和校验,不绑定 UART、USB 或其他底层通信方式,也不要求兼容 Modbus。 YMFDTP 借鉴了 Modbus 的主从通信模型、地址和 CRC 校验,但使用“数据 ID + 数据字典”替代固定寄存器布局,更适合机器人上下位机之间持续增加和调整数据。 ## 设计初衷 传统的固定数据包通常依赖字节位置: ```text 字节 0~1:速度 字节 2~3:角度 字节 4:状态 ``` 一旦在中间增加一个字段,后续字段的偏移就会改变,上位机和下位机往往都需要修改、重新编译和重新联调。 YMFDTP 为每项数据分配稳定的 ID,通信双方按 ID 查询和返回数据: ```text ID 1:速度 ID 2:角度 ID 3:状态 ``` 这样可以达到以下目的: 1. 数据含义与数据在报文中的位置解耦; 2. 上位机可以只查询当前需要的数据,减少通信量; 3. 下位机新增数据 ID 时,旧上位机仍可读取原有数据,不必重新编译; 4. 数据监控和调试工具可以按 ID 工作,不依赖固定字节偏移。 ## 兼容性原则 协议演进依赖数据 ID 的稳定性。约定如下: - 已发布的 ID 默认不修改含义、类型和长度;确需修改时必须增加修订版本; - 新增数据使用新的 ID; - 废弃的 ID 不复用; - 上位机只查询自己认识的 ID; - 下位机收到未知 ID 时忽略,或返回明确的未知 ID 结果; - 修订版本发生变化时,表示原有解析约定可能不再适用,需要更新对应解析代码。 因此,下位机增加新的数据 ID 不会破坏旧上位机;如果只是改变已有数据的含义、类型或长度,应增加修订版本,必要时创建新的 ID。 协议预留 `YMFDTP_ID_VERSION`(当前为 `0x00`)作为数据定义版本 ID。主机需要判断从机是否使用了兼容的解析约定时,可以先查询这个 ID: - 只新增数据 ID:数据定义版本可以保持不变,旧主机仍可继续工作; - 修改已有 ID 的含义、类型、长度或字节序:递增数据定义版本,并在 Excel 中更新对应 ID 的修订版本; - 主机发现版本不支持时,不应按旧规则解析可能受影响的数据,应提示更新解析配置或程序。 这里的版本是“数据定义版本”,不是底层帧格式版本。帧结构、CRC 或地址规则发生不兼容变化时,应另行升级协议实现。 ## 项目协作方式 项目使用一份公共 Excel 作为数据字典,由参与开发者共同维护。协议不强制规定 ID 的模块范围,具体分配方式根据项目场景决定。 数据字典至少应记录: | 字段 | 说明 | | --- | --- | | ID | 数据的固定编号 | | 名称 | 数据名称 | | 类型 | `uint16`、`float32` 或自定义类型 | | 字节长度 | 序列化后的固定长度 | | 单位 | 例如 mm、deg、A | | 读写属性 | 只读或可写 | | 修订版本 | 数据定义的修改版本;未修改时可留空或沿用原版本 | | 说明 | 数据含义和取值范围 | | 状态 | 使用中或已废弃 | 修订版本用于记录数据定义的变化,不要求每次新增 ID 都升级版本。新增 ID 保持旧 ID 的修订版本不变;修改已有 ID 的含义、类型、长度或字节序时,更新该 ID 的修订版本,并通知使用方更新解析约定。修订版本留空表示沿用既有定义,适用于未发生修改的旧数据。 Excel 是团队沟通和协议约定的依据,代码中使用显式数值的 `enum` 固化 ID,避免自动递增导致编号意外变化: ```c typedef enum { // 数据版本号[固定] YMFDTP_ID_VERSION = 0x00, //自定义 YMFDTP_ID_SPEED = 0x01, YMFDTP_ID_ANGLE = 0x02, YMFDTP_ID_STATUS = 0x03, //结束[固定] YMFDTP_ID_END = 0xff } YMFDTP_DataId; ``` ## 项目架构 项目由三部分组成: 1. **数据 ID 表**:记录每个 ID 的类型和字节长度; 2. **ID 数据查询**:根据 ID 列表打包查询请求; 3. **ID 数据返回**:根据查询结果打包响应,并按 ID 解包数据。 ## 数据帧格式 典型数据帧分为一级包头和业务内容: ```text 固定包头 | 包长度 | 从机地址 | 主机地址 | 查询 ID 列表 | ID 列表结束标志 | ID 对应的数据列表 | CRC 校验 ``` 当前约定: - 固定包头为 `0xA5 0x5A`,用于在字节流中寻找帧起点; - 包长度为 `uint16` 小端序,表示从地址字段开始到业务数据结束的长度,不包含包头、长度字段和 CRC; - 包体长度上限由 `YMFDTP_MAX_BODY_LENGTH` 定义,当前测试配置为 `13` 字节;总帧长度上限由 `YMFDTP_MAX_FRAME_LENGTH` 定义,当前为 `19` 字节; - CRC 覆盖固定包头、包长度和业务内容,CRC 字节低字节在前; - 接收端应先校验包头和包长度,再等待完整帧,最后校验 CRC; - 包头或长度错误时丢弃当前候选帧并重新搜索 `0xA5 0x5A`; - ID 列表结束标志为 `0xFF`; - 数据 ID 使用 `0x00~0xFE`; - CRC 使用 `CRC16-Modbus`; - 从机地址使用 `0x00~0x7F`; - 主机地址使用 `0x80~0xFF`。 例如,主机 `0x81` 查询从机 `0x01` 的数据 ID `[1, 2, 3, 4]`: ```text A5 | 5A | 长度 | 0x01 | 0x81 | 1 | 2 | 3 | 4 | 0xFF | CRC ``` 从机返回对应数据 `[10, 11, 12.3, 15]`: ```text A5 | 5A | 长度 | 0x81 | 0x01 | 1 | 2 | 3 | 4 | 0xFF | 数据列表 | CRC ``` 具体实现需要明确多字节整数、浮点数和 CRC 字段的字节序,并保证数据字典中的长度与实际序列化长度一致。 参考 C 实现使用 `ymfdtp.h` 和 `ymfdtp.c`,当前接口包括: - `YMFDTP_BuildQuery` / `YMFDTP_ParseQuery`:查询帧打包和解析; - `YMFDTP_BuildResponse` / `YMFDTP_ParseResponse`:响应帧打包和解析; - `YMFDTP_Crc16Modbus`:CRC16-Modbus 计算; - `YMFDTP_EncodeAutoUpload`:自动上传配置字节编码。 CRC 按 Modbus 习惯以低字节在前发送。解析接口要求调用方提供完整帧长度,库会校验 CRC、结束标志、ID 是否存在以及数据总长度是否符合数据字典。 `YMFDTP_ParseQuery` 和 `YMFDTP_ParseResponse` 负责对已经收齐的完整帧进行业务解析。对于串口等无法预先知道包长的字节流,使用 `YMFDTP_StreamParser` 状态机: 1. 调用 `YMFDTP_StreamInit` 初始化; 2. 每次收到任意长度子包,调用 `YMFDTP_StreamFeed`; 3. 返回 `YMFDTP_ERR_INCOMPLETE` 表示还没收齐,继续喂入后续数据; 4. 返回 `YMFDTP_OK` 后调用 `YMFDTP_StreamTake` 取出完整帧,再交给 `YMFDTP_ParseQuery` 或 `YMFDTP_ParseResponse`; 5. `YMFDTP_StreamFeed` 通过 `consumed` 告知本次输入消耗了多少字节,支持一个子包中连续包含多个帧。 当 `YMFDTP_StreamFeed` 返回 `YMFDTP_OK` 后,必须先调用 `YMFDTP_StreamTake` 取出当前帧,再继续喂入新的数据。若状态机仍处于 ready 状态,重复调用 `StreamFeed` 不会消费新数据,`consumed` 会返回 `0`,这样可以避免调用方误以为数据已经进入解析器。 状态机内部负责搜索包头、读取长度、等待完整帧和校验 CRC;业务解析仍由两个 `Parse` 函数负责。逐字节丢包、包头错误和长度错误的覆盖测试见 [TEST_REPORT.md](TEST_REPORT.md)。 实现上,状态机使用“有效数据起始偏移 + 长度”管理缓冲区。找到并取出一帧时只移动起始偏移,不搬运剩余字节;只有缓冲区尾部没有可写空间时,才将尚未消费的数据一次性整理到数组开头。扫描过程中会记录有限数量的候选包头地址用于跟踪,但候选记录满时仍会继续完整扫描,不影响解析正确性。 ### 解包流程 接收任意长度子包时,处理流程如下: ```text ┌──────────────────────┐ │ 收到新的字节子包 │ └──────────┬───────────┘ v ┌──────────────────────┐ │ 追加到滑动接收缓冲区 │ └──────────┬───────────┘ v ┌──────────────────────┐ 否 │ 搜索连续字节 A5 5A ├─────────────────┐ └──────────┬───────────┘ │ │是 v v ┌────────────────┐ ┌──────────────────────┐ │ 保留可能的 A5 │ │ 是否收到完整长度字段 │──否────>│ 等待下个子包 │ └──────────┬───────────┘ └────────────────┘ │是 v ┌──────────────────────┐ 超过上限 │ 读取 body_length ├─────────────────┐ └──────────┬───────────┘ v │合法 丢弃当前候选头 v │ ┌──────────────────────┐ │ │ 是否收齐整帧? ├──否────────────┘ └──────────┬───────────┘ │是 v ┌──────────────────────┐ CRC错误 │ 校验 CRC16-Modbus ├─────────────────┐ └──────────┬───────────┘ v │正确 丢弃当前候选头 v │ ┌──────────────────────┐ │ │ 输出完整帧给 Take() │<───────────────┘ └──────────┬───────────┘ v ┌──────────────────────┐ │ ParseQuery/Response │ │ 解析地址、ID、数据 │ └──────────────────────┘ ``` 这里的“丢弃当前候选头”不会清空整个接收缓冲区,而是继续向后搜索新的 `A5 5A`。因此,包 1 损坏时可以跳过包 1,继续找到包 2 或包 3。包内业务数据即使包含 `A5 5A`,只有同时满足合法长度和正确 CRC 才会被当作新帧。 ### 连续包恢复 例如连续收到三个包: ```text 包1 | 包2 | 包3 ``` 状态机的处理结果如下: | 接收情况 | 状态机行为 | | --- | --- | | 包 1 完整 | 输出包 1,保留后续字节 | | 包 1 从中间丢到 CRC | 包 1 候选 CRC 失败,向后搜索并输出包 2、包 3 | | 包 1 中间到包 2 中间全部丢失 | 丢失区间不会影响包 3 的包头,找到合法包 3 后输出 | | 包 2 CRC 错误 | 跳过包 2,继续寻找并输出包 3 | | 包 1、2、3 在一个子包中 | 通过 `consumed` 和多次 `StreamTake` 逐包取出 | 数据字段统一使用小端字节序。整数应按 `(value >> (8 * n)) & 0xFF` 逐字节序列化,库提供 `YMFDTP_WriteLE16`、`YMFDTP_WriteLE32`、`YMFDTP_WriteLE64` 及对应读取函数。浮点数或结构体需要先按数据字典约定转换为固定宽度的字节表示,再使用相同的字节序传输。 项目数据定义集中在 `ymfdtp_cfg.h` 和 `ymfdtp_cfg.c`: - `ymfdtp.h/c`:通用协议、CRC、帧打包和解析; - `ymfdtp_cfg.h/c`:项目数据 ID、按 ID 索引的 `YMFDTP_Field` 数据字典和默认查询列表; - 主机和从机工程共用 `ymfdtp_cfg`,修改数据定义时只维护这一处。 包格式和工程包长也集中在 `ymfdtp_cfg.h` 中。`YMFDTP_MAX_BODY_LENGTH` 应根据项目实际最大包体长度设置:当前示例查询包体为 `6` 字节、响应包体为 `13` 字节,因此配置为 `13`。如果后续新增数据使包体变长,需要同步增大这个宏;也可以预先设置为更大的值,以兼容未来扩展。总帧长度由 `YMFDTP_MAX_FRAME_LENGTH` 自动计算。 配置表使用“数组下标即 ID”的方式:例如 `YMFDTP_DefaultFields[YMFDTP_ID_SPEED].size` 表示速度数据的序列化字节长度,长度为 `0` 表示该 ID 尚未定义。项目数据枚举中显式保留 `YMFDTP_ID_END = 0xFF`,表示 ID 列表结束;因此业务 ID 只能使用 `0x00~0xFE`,字典数组长度为 `YMFDTP_ID_END`,不会为 `0xFF` 分配数据项。这样既能禁止业务 ID 使用结束值,也能直接用枚举值索引数据字典。 当前示例中,`YMFDTP_ID_VERSION` 的数据长度为 `1` 字节,当前版本由 `YMFDTP_DATA_DEFINITION_VERSION` 定义。设备应用层应将该常量作为版本 ID 的返回值,主机可以将其与自身支持的版本范围进行比较。 带配置模块的编译命令为: ```bash cc -std=c99 -Wall -Wextra -Werror -pedantic \ YMFDTP/ymfdtp.c YMFDTP/ymfdtp_cfg.c demo/test_ymfdtp.c -IYMFDTP \ -o test_ymfdtp ./test_ymfdtp ``` 完整的异常解析测试项目和实际输出见 [TEST_REPORT.md](TEST_REPORT.md)。 ## 主从机通信案例 `example_master_slave.c` 使用内存缓冲区模拟一次完整通信,不依赖真实串口: 1. 主机 `0x81` 查询从机 `0x01` 的 ID `1、2、3`; 2. 从机解析查询,根据数据字典打包速度、角度和状态; 3. 主机解析响应,并把数据字节交给应用层; 4. 主机为 ID `1` 配置 `20 ms` 自动上传。 运行案例: ```bash cc -std=c99 -Wall -Wextra -Werror -pedantic \ YMFDTP/ymfdtp.c YMFDTP/ymfdtp_cfg.c demo/example_master_slave.c -IYMFDTP \ -o example_master_slave ./example_master_slave ``` 示例输出: ```text host -> slave (12 bytes): A5 5A 06 00 01 81 01 02 03 FF B9 5F slave 0x01 received query from host 0x81 for 3 IDs slave -> host (19 bytes): A5 5A 0D 00 81 01 01 02 03 FF 34 12 00 00 80 3F 01 35 B5 host received response from slave 0x01: 3 IDs, 7 value bytes auto-upload config for ID 1: 0x94 (20 ms) ``` 示例中的值已经按数据字典顺序序列化为字节;实际项目中,串口驱动只需要负责把构造出的帧发送给对端,并把收到的完整帧交给解析接口。 ### 多线程交互测试 `demo/test_multithread.c` 提供一个三线程模拟:一个 master 和两个地址不同的 slave。master 依次执行三个 task: 1. 普通轮询两个从机; 2. 查询从机不支持的 ID,确认从机拒绝构造响应;再查询支持的 ID,确认正常返回; 3. 只给从机 1 开启主动上报,要求累计收到 3 次后才关闭,并确认从机 2 未主动发送; 4. 第 3 次主动上报期间继续轮询从机 1,验证两类响应可以同时进入接收队列,随后关闭主动上报并确认不再产生主动事件。 编译运行: ```bash cc -std=c99 -Wall -Wextra -Werror -pedantic -IYMFDTP \ YMFDTP/ymfdtp.c YMFDTP/ymfdtp_cfg.c demo/test_multithread.c \ -pthread -o build/test_multithread ./build/test_multithread ``` 该测试使用真实协议帧构造和解析;自动上传配置的下发命令属于应用层,因此在模拟总线上用控制 task 表示。 示例输出会展示每个响应的来源、类型、ID 和业务字节,例如: ```text [task 1] master polls slave 0x01 and slave 0x02 t=0ms master -> slave 0x01 QUERY IDs: 1 3 frame: A5 5A 05 00 01 80 01 03 FF 18 94 master <- slave 0x01 [poll] IDs: 1 3 values: 10 00 01 t=3ms master -> slave 0x02 QUERY IDs: 1 3 frame: A5 5A 05 00 02 80 01 03 FF 5C 94 master <- slave 0x02 [poll] IDs: 1 3 values: 10 00 02 [task 2] enable slave 0x01 auto-upload: ID 3, interval 5 ms t=6ms master -> slave 0x01 SET_AUTO enabled=1 interval=5 ms config=0x85 (OK) t=12ms master <- slave 0x01 [auto] IDs: 3 values: 02 t=18ms master <- slave 0x01 [auto] IDs: 3 values: 03 [task 3] poll slave 0x01 while auto-upload is active t=18ms master -> slave 0x01 QUERY IDs: 1 3 frame: A5 5A 05 00 01 80 01 03 FF 18 94 t=26ms master <- slave 0x01 [poll] IDs: 1 3 values: 13 00 04 t=26ms master <- slave 0x01 [auto] IDs: 3 values: 05 [task 3] disable slave 0x01 auto-upload after 3 reports t=27ms master -> slave 0x01 SET_AUTO enabled=0 interval=0 ms config=0x00 (OK) [multi-thread] poll responses=3, auto reports=3, active poll coexistence=PASS, close=PASS ``` ## 自动上传 除按请求返回数据外,从机可以对已经查询过的数据开启自动上传。主机完成一次正常查询后,从机保存该查询结果的最新值;开启自动上传后,从机按设定的周期主动回传这个缓存值,不需要主机重复发送查询命令。 每个数据 ID 可以关联一个 8 bit 的自动上传配置: ```text bit7 :自动上传开关(0=关闭,1=开启) bit6~bit0 :上传周期,单位为 ms,取值 `1~127` ``` 周期字段直接表示毫秒数,编码 `1~127` 分别表示 `1~127 ms`。当自动上传关闭时,周期字段忽略;周期为 `0` 表示无效配置。自动上传适合高频状态数据,超过该范围的数据应继续使用普通查询,或另行增加配置字节。 自动上传配置应通过专用配置 ID 或明确的写入命令下发,不能与普通数据值混用。 自动上传的数据仍使用普通响应帧格式,由从机地址发送到主机地址。缓存值来自最近一次成功查询的数据;如果数据没有重新采样或查询,重复上传的内容可能是旧值,具体行为由数据字典或项目约定说明。实际最小周期还受通信波特率、帧长度和从机任务调度影响,不能保证所有数据都能以 `1 ms` 周期发送。 普通查询与自动上传是两个独立事务。自动上传开启后,主机穿插发送普通查询不会关闭、暂停或重置从机的自动上传状态;从机可以先返回轮询响应,随后继续发送主动上报。接收端应根据从机地址、ID、配置状态或应用层上下文区分两类响应。只有明确发送自动上传关闭配置,才会结束该 ID 的主动上报。 ## 命名规则 平台通用数据类型使用全小写: ```text int8、int16、uint8、uint16、uint32、float32…… ``` 自定义类型使用 `FDTP` 前缀: - 结构体:`FDTP` + 小写类型名; - 结构体指针:`FDTP` + 大写类型名; - 枚举:`FDTP` + 单词首字母大写; - 函数:`YMFDTP` + 首字母大写。 示例: ```c typedef struct { uint16 width; uint16 height; CVImgType type; _color8_t* data; } CVimage; typedef CVimage* CVIMAGE; typedef enum { CVBinaryType = 1, CVTrimapType, CVGrayType, CVRgb16Type, } CVImgType; ```