# agile_modbus **Repository Path**: tdying/agile_modbus ## Basic Information - **Project Name**: agile_modbus - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 17 - **Created**: 2023-06-02 - **Last Updated**: 2023-06-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Agile Modbus ## 1、介绍 Agile Modbus 即:轻量型 modbus 协议栈,满足用户任何场景下的使用需求。 ![ModbusProtocol](./figures/ModbusProtocol.jpg) - 在线文档:[API Manual](https://loogg.github.io/AgileModbus_API/) - `examples` 文件夹提供 PC 上的示例 - MCU 上的示例查看 [mcu_demos](https://github.com/loogg/agile_modbus_mcu_demos) - 在 AT32F437 上基于 RT-Thread 实现的支持 Modbus 固件升级的 Bootloader:[AT32F437_Boot](https://github.com/loogg/AT32F437_Boot) - 在 HPM6750 上基于 RT-Thread 实现的支持 Modbus 固件升级的 Bootloader:[HPM6750_Boot](https://github.com/loogg/HPM6750_Boot) ### 1.1、特性 1. 支持 rtu 及 tcp 协议,使用纯 C 开发,不涉及任何硬件接口,可在任何形式的硬件上直接使用。 2. 由于其使用纯 C 开发、不涉及硬件,完全可以在串口上跑 tcp 协议,在网络上跑 rtu 协议。 3. 支持符合 modbus 格式的自定义协议。 4. 同时支持多主机和多从机。 5. 使用简单,只需要将 rtu 或 tcp 句柄初始化好后,调用相应 API 进行组包和解包即可。 ### 1.2、目录结构 | 名称 | 说明 | | ---- | ---- | | doc | 文档 | | examples | 例子 | | figures | 素材 | | inc | 头文件 | | src | 源代码 | | util | 提供简单实用的组件 | ### 1.3、许可证 Agile Modbus 遵循 `Apache-2.0` 许可,详见 `LICENSE` 文件。 ## 2、使用 Agile Modbus 帮助文档请查看 [doc/doxygen/Agile_Modbus.chm](./doc/doxygen/Agile_Modbus.chm) ### 2.1、移植 - 用户需要实现硬件接口的 `发送数据` 、 `等待数据接收结束` 、 `清空接收缓存` 函数 对于 `等待数据接收结束`,提供如下几点思路: 1. 通用方法 每隔 20 / 50 ms (该时间可根据波特率和硬件设置,这里只是给了参考值) 从硬件接口读取数据存放到缓冲区中并更新偏移,直到读取不到或缓冲区满,退出读取。 这对于裸机或操作系统都适用,操作系统可通过 `select` 或 `信号量` 方式完成阻塞。 2. 串口 `DMA + IDLE` 中断方式 配置 `DMA + IDLE` 中断,在中断中使能标志,应用程序中判断该标志是否置位即可。 但该方案容易出问题,数据字节间稍微错开一点时间就不是一帧了。推荐第一种方案。 - 主机: 1. `agile_modbus_rtu_init` / `agile_modbus_tcp_init` 初始化 `RTU/TCP` 环境 2. `agile_modbus_set_slave` 设置从机地址 3. `清空接收缓存` 4. `agile_modbus_serialize_xxx` 打包请求数据 5. `发送数据` 6. `等待数据接收结束` 7. `agile_modbus_deserialize_xxx` 解析响应数据 8. 用户处理得到的数据 - 从机: 1. 实现 `agile_modbus_slave_callback_t` 类型回调函数 2. `agile_modbus_rtu_init` / `agile_modbus_tcp_init` 初始化 `RTU/TCP` 环境 3. `agile_modbus_set_slave` 设置从机地址 4. `等待数据接收结束` 5. `agile_modbus_slave_handle` 处理请求数据 6. `清空接收缓存` (可选) 7. `发送数据` - 特殊功能码 需要调用 `agile_modbus_set_compute_meta_length_after_function_cb` 和 `agile_modbus_set_compute_data_length_after_meta_cb` API 设置特殊功能码在主从模式下处理的回调。 - `agile_modbus_set_compute_meta_length_after_function_cb` `msg_type == AGILE_MODBUS_MSG_INDICATION`: 返回主机请求报文的数据元长度(uint8_t 类型),不是特殊功能码必须返回 0。 `msg_type == MSG_CONFIRMATION`: 返回从机响应报文的数据元长度(uint8_t 类型),不是特殊功能码必须返回 1。 - `agile_modbus_set_compute_data_length_after_meta_cb` `msg_type == AGILE_MODBUS_MSG_INDICATION`: 返回主机请求报文数据元之后的数据长度,不是特殊功能码必须返回 0。 `msg_type == MSG_CONFIRMATION`: 返回从机响应报文数据元之后的数据长度,不是特殊功能码必须返回 0。 - `agile_modbus_rtu_init` / `agile_modbus_tcp_init` 初始化 `RTU/TCP` 环境时需要用户传入 `发送缓冲区` 和 `接收缓冲区`,建议这两个缓冲区大小都为 `AGILE_MODBUS_MAX_ADU_LENGTH` (260) 字节。`特殊功能码` 情况用户根据协议自行决定。 但对于小内存 MCU,这两个缓冲区也可以设置小,所有 API 都会对缓冲区大小进行判断: 发送缓冲区设置:如果 `预期请求的数据长度` 或 `预期响应的数据长度` 大于 `设置的发送缓冲区大小`,返回异常。 接收缓冲区设置:如果 `主机请求的报文长度` 大于 `设置的接收缓冲区大小`,返回异常。这个是合理的,小内存 MCU 做从机肯定是需要对某些功能码做限制的。 ### 2.2、主机 见 `2.1、移植`。 ### 2.3、从机 #### 2.3.1、接口说明 - `agile_modbus_slave_handle` 介绍 ```C int agile_modbus_slave_handle(agile_modbus_t *ctx, int msg_length, uint8_t slave_strict, agile_modbus_slave_callback_t slave_cb, const void *slave_data, int *frame_length) ``` msg_length: `等待数据接收结束` 后接收到的数据长度。 slave_strict: 从机地址严格性检查 (0: 不判断地址是否一致,由用户回调处理; 1: 地址必须一致,否则不会调用回调,也不打包响应数据)。 slave_cb: `agile_modbus_slave_callback_t` 类型回调函数,用户实现并传入。如果为 NULL,所有功能码都能响应且为成功,但寄存器数据依然为 0。 slave_data: 从机回调函数私有数据。 frame_length: 获取解析出的 modbus 数据帧长度。这个参数的意义在于: 1. 尾部有脏数据: 仍能解析成功,并告诉用户真实的 modbus 帧长,用户可以进行处理 2. 数据粘包: 数据由 `一帧完整的 modbus 数据 + 部分 modbus 数据帧` 组成,用户获得真实 modbus 帧长后,可以移除处理完的 modbus 数据帧,再次读取硬件接口数据与当前 `部分 modbus 数据帧` 组成新的一帧 3. 该参数在 modbus 广播传输大数据时使用较多(如:自定义功能码广播升级固件),普通的从机响应都是一问一答式,只处理完整数据帧就行,建议在响应前执行 `清空接收缓存` - `agile_modbus_slave_callback_t` 介绍 ```C /** * @brief 从机回调函数 * @param ctx modbus 句柄 * @param slave_info 从机信息体 * @param data 私有数据 * @return =0:正常; * <0:异常 * (-AGILE_MODBUS_EXCEPTION_UNKNOW(-255): 未知异常,从机不会打包响应数据) * (其他负数异常码: 从机会打包异常响应数据) */ typedef int (*agile_modbus_slave_callback_t)(agile_modbus_t *ctx, struct agile_modbus_slave_info *slave_info, const void *data); ``` `agile_modbus_slave_info`: sft: 包含从机地址和功能码属性,回调中可利用 rsp_length: 响应数据长度指针,回调中处理 `特殊功能码` 时需要更新其值,否则 **不准更改** address: 寄存器地址 (不是所有功能码都用到) nb: 数目 (不是所有功能码都用到) buf: 不同功能码需要使用的数据域 (不是所有功能码都用到) send_index: 发送缓冲区当前索引 (不是所有功能码都用到) - `agile_modbus_slave_info` 不同功能码使用 - AGILE_MODBUS_FC_READ_COILS、AGILE_MODBUS_FC_READ_DISCRETE_INPUTS 需要使用到 `address`、`nb`、`send_index` 属性,需要调用 `agile_modbus_slave_io_set` API 将 IO 数据存放到 `ctx->send_buf + send_index` 开始的数据区域。 - AGILE_MODBUS_FC_READ_HOLDING_REGISTERS、AGILE_MODBUS_FC_READ_INPUT_REGISTERS 需要使用到 `address`、`nb`、`send_index` 属性,需要调用 `agile_modbus_slave_register_set` API 将寄存器数据存放到 `ctx->send_buf + send_index` 开始的数据区域。 - AGILE_MODBUS_FC_WRITE_SINGLE_COIL、AGILE_MODBUS_FC_WRITE_SINGLE_REGISTER 需要使用到 `address`、`buf` 属性,将 `buf` 强转为 `int *` 类型,获取值存放到寄存器中。 - AGILE_MODBUS_FC_WRITE_MULTIPLE_COILS 需要使用到 `address`、`nb`、`buf` 属性,需要调用 `agile_modbus_slave_io_get` API 获取要写入的 IO 数据。 - AGILE_MODBUS_FC_WRITE_MULTIPLE_REGISTERS 需要使用到 `address`、`nb`、`buf` 属性,需要调用 `agile_modbus_slave_register_get` API 获取要写入的寄存器数据。 - AGILE_MODBUS_FC_MASK_WRITE_REGISTER 需要使用到 `address`、`buf` 属性,通过 `(buf[0] << 8) + buf[1]` 获取 `and` 值,通过 `(buf[2] << 8) + buf[3]` 获取 `or` 值。获取寄存器值 `data`,进行 `data = (data & and) | (or & (~and))` 操作更新 `data` 值,写入寄存器。 - AGILE_MODBUS_FC_WRITE_AND_READ_REGISTERS 需要使用到 `address`、`buf`、`send_index` 属性,通过 `(buf[0] << 8) + buf[1]` 获取要读取的寄存器数目,通过 `(buf[2] << 8) + buf[3]` 获取要写入的寄存器地址,通过 `(buf[4] << 8) + buf[5]` 获取要写入的寄存器数目。需要调用 `agile_modbus_slave_register_get` API 获取要写入的寄存器数据,调用 `agile_modbus_slave_register_set` API 将寄存器数据存放到 `ctx->send_buf + send_index` 开始的数据区域。 - 自定义功能码 需要使用到 `send_index`、`nb`、`buf` 属性,用户在回调中处理数据。 send_index: 发送缓冲区当前索引 nb: PUD - 1,也就是 modbus 数据域长度 buf: modbus 数据域起始位置 **注意**: 用户在回调中往发送缓冲区填入数据后,需要更新 `agile_modbus_slave_info` 的 `rsp_length` 值。 #### 2.3.2、简易从机接入接口 Agile Modbus 提供了 `agile_modbus_slave_callback_t` 的一种实现方式,使用户能够简单方便接入。 使用示例可查看 [examples/slave](./examples/slave)。 使用方式: ```C #include "agile_modbus.h" #include "agile_modbus_slave_util.h" const agile_modbus_slave_util_t slave_util = { /* User implementation */ }; agile_modbus_slave_handle(ctx, read_len, 0, agile_modbus_slave_util_callback, &slave_util, NULL); ``` - `agile_modbus_slave_util_callback` 介绍 - Agile Modbus 提供的一种 `agile_modbus_slave_callback_t` 实现方式,需要 `agile_modbus_slave_util_t` 类型变量指针作为私有数据。 - 私有数据为 NULL,所有功能码都能响应且为成功,但寄存器数据依然为 0。 - `agile_modbus_slave_util_t` 介绍 ```C typedef struct agile_modbus_slave_util { const agile_modbus_slave_util_map_t *tab_bits; /**< 线圈寄存器定义数组 */ int nb_bits; /**< 线圈寄存器定义数组数目 */ const agile_modbus_slave_util_map_t *tab_input_bits; /**< 离散量输入寄存器定义数组 */ int nb_input_bits; /**< 离散量输入寄存器定义数组数目 */ const agile_modbus_slave_util_map_t *tab_registers; /**< 保持寄存器定义数组 */ int nb_registers; /**< 保持寄存器定义数组数目 */ const agile_modbus_slave_util_map_t *tab_input_registers; /**< 输入寄存器定义数组 */ int nb_input_registers; /**< 输入寄存器定义数组数目 */ int (*addr_check)(agile_modbus_t *ctx, struct agile_modbus_slave_info *slave_info); /**< 地址检查接口 */ int (*special_function)(agile_modbus_t *ctx, struct agile_modbus_slave_info *slave_info); /**< 特殊功能码处理接口 */ int (*done)(agile_modbus_t *ctx, struct agile_modbus_slave_info *slave_info, int ret); /**< 处理结束接口 */ } agile_modbus_slave_util_t; ``` - 寄存器相关 用户需要实现 `bits`、`input_bits`、`registers`、`input_registers` 定义。如果某个寄存器定义为 NULL,该寄存器对应的功能码能响应且为成功,但寄存器数据都为 0。 - 接口调用过程 ![SlaveCallback](./figures/SlaveCallback.jpg) - `agile_modbus_slave_util_map` 介绍 ```C typedef struct agile_modbus_slave_util_map { int start_addr; /**< 起始地址 */ int end_addr; /**< 结束地址 */ int (*get)(void *buf, int bufsz); /**< 获取寄存器数据接口 */ int (*set)(int index, int len, void *buf, int bufsz); /**< 设置寄存器数据接口 */ } agile_modbus_slave_util_map_t; ``` - **注意事项**: - 起始地址和结束地址决定的寄存器个数有限制。更改函数内部 `map_buf` 数组大小可使其变大。 - bit 寄存器 < 250 - register 寄存器 < 125 - 接口函数为 NULL,寄存器对应的功能码能响应且为成功。 - `get` 接口 将地址域内的数据全部拷贝到 `buf` 中。 - `set` 接口 - `index`: 地址域内的偏移 - `len`: 长度 根据 `index` 和 `len` 修改数据。 ### 2.4、示例 - [examples](./examples) 文件夹中提供 PC 上的示例,可以在 `WSL` 或 `Linux` 下编译运行。 - RTU / TCP 主机、从机的示例 - 特殊功能码的示例 RTU 点对点传输文件: 演示特殊功能码的使用方式 RTU 广播传输文件: 演示 `agile_modbus_slave_handle` 中 `frame_length` 的用处 - [mcu_demos](https://github.com/loogg/agile_modbus_mcu_demos) 提供在 MCU 上的例子。 - [AT32F437_Boot](https://github.com/loogg/AT32F437_Boot) 在 AT32F437 上基于 RT-Thread 实现的支持 Modbus 固件升级的 Bootloader。 - [HPM6750_Boot](https://github.com/loogg/HPM6750_Boot) 在 HPM6750 上基于 RT-Thread 实现的支持 Modbus 固件升级的 Bootloader。 ### 2.5、Doxygen 文档生成 - 使用 `Doxywizard` 打开 [Doxyfile](./doc/doxygen/Doxyfile) 运行,生成的文件在 [doxygen/output](./doc/doxygen/output) 下。 - 需要更改 `Graphviz` 路径。 - `HTML` 生成未使用 `chm` 格式的,如果使能需要更改 `hhc.exe` 路径。 ## 3、支持 ![zanshang](./figures/zanshang.jpg) 如果 Agile Modbus 解决了你的问题,不妨扫描上面二维码请我 **喝杯咖啡** ~ ## 4、联系方式 & 感谢 - 维护:马龙伟 - 主页: - 邮箱:<2544047213@qq.com>