# lhc_modbus **Repository Path**: RT-Thread-Mirror/lhc_modbus ## Basic Information - **Project Name**: lhc_modbus - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 2 - **Created**: 2023-10-28 - **Last Updated**: 2025-05-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Lightweight and high-performance modbus library based on C language(轻量级高性能基于C语言的modbus库) ![GitHub](https://img.shields.io/github/license/LHC324/lhc_modbus)![Read the Docs](https://img.shields.io/readthedocs/pip)![GitHub language count](https://img.shields.io/github/languages/count/LHC324/lhc_modbus)![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/LHC324/lhc_modbus?color=abcdef)![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/t/LHC324/lhc_modbus?color=c75762) [EN](README.md) | 中文 ## 简介 [lhc modbus](https://github.com/armink/FlashDB) (*Lightweight and high-performance modbus library based on C language*)是一款超轻量级、高性能和低资源占用率的modbus协议栈,专注于提供工业环境中设备间数据通信和数据交换功能。虽然都是modbus协议,但是不同的是,比freemodbus、samll modbus等其他协议栈更加轻便和易用,且具有很大的可裁剪性。关键是可以很容易移植到裸机、freertos和其他平台。 ## 为什么开发这个库 关于modbus相关的协议栈,多如牛毛,但实际在项目开发过程中大多数协议栈都很难实际应用,原因如下: - 客户给的工期短,学习比较全面的协议栈花费时间多,且接口复杂,对于新手难于快速应用。 - 老板开发产品预算紧张,尽管资源丰富的mcu很多,但是很多时候,老板不愿意多花哪怕一分钱。 - 配合市面上大多数云协议和应用场景,modbus协议上云,串口屏幕同步显示。 - 这个库主要用于满足上面这些场景,在我实际开发项目过程中,我深知客户对于产品要求的多变性,很难有一套软件包能真正适配所有的应用场景,而不加以调整,我想那是不可能的,至少我目前没遇到过。 - 当然,这套协议也是如此,实际使用时需结合公司应用场景酌情考虑。 - 把它独立出来,并整合到开源生态,方便后续自己、公司或者其他有需要的人能一键式配置使用。 ## 主要特性 - 资源占用极低。 - 性能优越,数据处理几乎**0**拷贝。 - 主机和从机都基于单一的操作接口`operatex`接口便可以操作所有。 - 经过裁剪后可直接运行到51单片机、stm32f030之类的小资源单片机上。 - 特色功能:modbus端口支持shell控制台转移。 - 特别支持有人云的0x46指令。 ## 目录结构 | 名称 | 说明 | | :-----: | :--------------: | | doc | 文档 | | samples | 例子 | | figures | 素材 | | inc | 头文件 | | src | 源代码 | | port | 平台移植相关接口 | ## 如何使用 下面分别提供在**rt-thread**和**freertos**下使用例子 ### **rt-thread** 重点关注`lhc_modbus_port.c` - 定义modbus对象 ```c /*定义Modbus对象*/ p_lhc_modbus_t lhc_modbus_test_object; ``` - 定义一个modbus寄存器池 ```c /*定义一个共用寄存器池*/ static lhc_modbus_pools_t Spool; ``` - 实现crc校验接口(**必须**) ```c /** * @brief modbus协议栈获取crc16校验码 * @note 可以自己实现,也可以直接使用该接口 * @param ptr 数据缓冲区指针 * @param len 数据缓冲区长度 * @param init_val 校验码初值 * @retval None */ static unsigned short lhc_modbus_get_crc16(unsigned char *ptr, unsigned short len, unsigned short init_val) { uint16_t i; uint16_t crc16 = init_val; if (ptr == NULL) return 0; for (i = 0; i < len; i++) { crc16 ^= *ptr++; for (uint16_t j = 0; j < 8; j++) { crc16 = (crc16 & 0x0001) ? ((crc16 >> 1) ^ 0xa001) : (crc16 >> 1U); } } return crc16; } ``` - 实现内部数据处理回调函数接口(**非必须**) ```c /** * @brief modbus协议栈外部回调函数 * @param pd 需要初始化对象指针 * @param code 功能码 * @retval None */ static void lhc_modbus_callback(p_lhc_modbus_t pd, lhc_modbus_func_code code) { } ``` - 在多线程或者中断中使用时,需要加锁保护(**非必须**) ```c #if (LHC_MODBUS_RTOS) /** * @brief modbus协议栈加锁函数 * @param pd 需要初始化对象指针 * @param code 功能码 * @retval None */ static void lhc_modbus_lock(void) { rt_mutex_take(modbus_mutex, RT_WAITING_FOREVER); } /** * @brief modbus协议栈解锁函数 * @param pd 需要初始化对象指针 * @param code 功能码 * @retval None */ static void lhc_modbus_ulock(void) { rt_mutex_release(modbus_mutex); } #endif ``` - 实现modbus错误处理函数(**非必须**) ```c /** * @brief modbus协议栈接收帧错误处理 * @param pd 需要初始化对象指针 * @param error_code 错误码 * @retval None */ static void lhc_modbus_error_handle(p_lhc_modbus_t pd, lhc_modbus_state_code error_code) { /* * @note 主cpu切换一次finsh后,modbus接收的size会丢失,怀疑是rtt底层驱动有问题 */ if (pd && pd->dev) //解决数据帧错位后,连续错位问题 rt_device_read(pd->dev, 0, pd->Uart.rx.pbuf, pd->Uart.rx.size); pd->old_rx_indicate = pd->dev->rx_indicate; //备份接收完成通知(串口关闭前) if(RT_EOK == rt_device_close(pd->dev)) { /*挂接目标接收中断函数*/ rt_device_set_rx_indicate(pd->dev, (rt_err_t(*)(rt_device_t, rt_size_t))pd->old_rx_indicate); rt_device_open(pd->dev, RT_DEVICE_FLAG_TX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING); LHC_MODBUS_DEBUG_R("device[%s] restarted successfully.", pd->dev->parent.name); } } ``` - 实现modbus底层发送函数(**必须**) ```c /** * @brief Modbus协议发送 * @param pd 需要初始化对象指针 * @retval None */ static void lhc_modbus_send(p_lhc_modbus_t pd, enum lhc_modbus_crc crc) { uint16_t crc16 = 0; #if (LHC_MODBUS_RTOS == 2U) if (NULL == pd || NULL == pd->dev) #else if (NULL == pd || NULL == pd->Uart.huart) #endif return; if (crc == lhc_used_crc) { if (pd->crc16) crc16 = pd->crc16(lhc_modbus_tx_buf, lhc_modbus_tx_count(pd), 0xffff); memcpy(&lhc_modbus_tx_buf[lhc_modbus_tx_count(pd)], (uint8_t *)&crc16, sizeof(crc16)); lhc_modbus_tx_count(pd) += sizeof(crc16); } if (pd->Uart.tx.wmode & uart_using_rs485) HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_SET); #if (LHC_MODBUS_RTOS == 2U) rt_device_write(pd->dev, 0, lhc_modbus_tx_buf, lhc_modbus_tx_count(pd)); #else switch (pd->Uart.tx.wmode) { case uart_using_it: { HAL_UART_Transmit((UART_HandleTypeDef *)pd->Uart.huart, lhc_modbus_tx_buf, lhc_modbus_tx_count(pd), 0x100); } break; case uart_using_dma: { HAL_UART_Transmit_DMA((UART_HandleTypeDef *)pd->Uart.huart, lhc_modbus_tx_buf, lhc_modbus_tx_count(pd)); while (__HAL_UART_GET_FLAG((UART_HandleTypeDef *)pd->Uart.huart, UART_FLAG_TC) == RESET) { } } break; default: break; } #endif if (pd->Uart.tx.wmode & uart_using_rs485) HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_RESET); } ``` **注意:以上接口均可以使用现成的。** 如果使用**freertos**,则以上**api**仅仅需要替换操作系统相关的部分即可。 下面演示,具体使用: - 根据自身需求,完善**lhc_modbus_init**函数 ```c #define __init_modbus(__name, __type, __master_id, __slave_id, __get_crc16, \ __callback, __lock, __unlock, __ota_update, __error_handle, \ __transmit, __uart, __pools, __user_handle) \ lhc_modbus_t __name##_lhc_modbus = { \ //临时变量名 .type = __type, \ //对象类型(主/从) .master.id = __master_id, \ //主机id .slave.id = __slave_id, \ //从机id .crc16 = __get_crc16, \ //自定义crc函数 .callback = __callback, \ //数据处理时回调函数 .lock = __lock, \ //加锁 .ulock = __unlock, \ //解锁 .ota = __ota_update, \ //自定义ota或者其他功能 .error = __error_handle, \ //错误处理函数 .transmit = __transmit, \ //数据发送函数 .Uart = __uart, \ //串口参数 .pPools = &__pools, \ //此对象关联的寄存器池 .slave.p_handle = __user_handle, \ //从机使用的外部句柄 }; /** * @brief 初始化Modbus协议库 * @retval None */ int rt_lhc_modbus_init(void) { int ret = 0; /* 创建一个动态互斥量 */ modbus_mutex = rt_mutex_create("modbus_mutex", RT_IPC_FLAG_PRIO); /* 创建控制信号量 */ console_sem = rt_sem_create("console_sem", 0, RT_IPC_FLAG_FIFO); modbus_uart_t lhc_modbus_uart = { //挂接对应串口句柄 .huart = &huart2, .phdma = &hdma_usart2_rx, //如果使用了dma,则挂接dma句柄 #if (LHC_MODBUS_RTOS) .semaphore = NULL, #else .recive_finish_flag = false, #endif .tx = { .wmode = uart_using_it | uart_using_rs485, .size = LHC_MODBUS_TX_BUF_SIZE, .count = 0, .pbuf = NULL, }, .rx = { .wmode = uart_using_dma, .size = LHC_MODBUS_RX_BUF_SIZE, .count = 0, .pbuf = NULL, }, }; __init_modbus(temp, lhc_modbus_Slave, LHC_MODBUS_MASTER_ADDR, LHC_MODBUS_SLAVE_ADDR, lhc_modbus_get_crc16, lhc_modbus_callback, lhc_modbus_lock, lhc_modbus_ulock, lhc_console_transfer, lhc_modbus_error_handle, lhc_modbus_send, lhc_modbus_uart, Spool, NULL); #if (LHC_MODBUS_RTOS == 2) temp_lhc_modbus.dev = NULL; temp_lhc_modbus.old_console = NULL; #endif ret = create_lhc_modbus(&lhc_modbus_test_object, &temp_lhc_modbus); if (ret != RT_EOK) return ret; rt_thread_pools_init(); return 0; } /*在内核对象中初始化:https://blog.csdn.net/yang1111111112/article/details/93982354*/ INIT_DEVICE_EXPORT(rt_lhc_modbus_init); ``` - 如果支持os,则创建一个数据接收处理线程 ```c /* * @note 此线程只是简单示例,不一定适用一些复杂情况,也不涉及具体的粘包、分包处理,如遇特殊情况需要处理, * 请自行实现,比较灵活。 */ /** * @brief modbus 从机接收回调函数 * @details * @param dev 设备句柄 * @param size 当前尺寸 * @retval None */ static rt_err_t modbus_rtu_rx_ind(rt_device_t dev, rt_size_t size) { p_lhc_modbus_t pd = lhc_modbus_test_object; if (pd && pd->Uart.semaphore) { pd->Uart.rx.count = size; rt_sem_release((rt_sem_t)pd->Uart.semaphore); //假设此通知为收到完整数据帧 } return RT_EOK; } /** * @brief modbus 从机接收线程 * @details rt thread V1和V2串口驱动说明:https://club.rt-thread.org/ask/article/8e1d18464219fae7.html * @note V版本使用DMA+idle数据被拆包解决办法:https://club.rt-thread.org/ask/article/8f4b74aa754fcc93.html * 粘包、封包解析器:https://github.com/aeo123/upacker * @param parameter:线程初始参数 * @retval None */ void modbus_slave_thread_entry(void *parameter) { rt_thread_pools_t *p_rt_thread_pool = (rt_thread_pools_t *)parameter; p_lhc_modbus_t pd = lhc_modbus_test_object; rt_device_t dev = RT_NULL; RT_ASSERT(pd); /*确认目标串口设备存在*/ dev = rt_device_find(LHC_MODBUS_DEVICE_NAME); if (dev) { // 记录当前设备指针if (RT_NULL == pd->dev) pd->dev = dev; pd->Uart.semaphore = p_rt_thread_pool->semaphore; rt_device_open(dev, RT_DEVICE_FLAG_TX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING); /*挂接目标接收中断函数*/ rt_device_set_rx_indicate(dev, modbus_rtu_rx_ind); } else rt_kprintf("@error: Target device [%s] not found.^_^\r\n", LHC_MODBUS_DEVICE_NAME); for (;;) { /* 永久方式等待信号量,获取到信号量则解析modbus协议*/ if ((rt_sem_take(p_rt_thread_pool->semaphore, RT_WAITING_FOREVER) == RT_EOK) && p_rt_thread_pool->semaphore) { rt_device_read(dev, 0, pd->Uart.rx.pbuf, pd->Uart.rx.count); if (pd) { lhc_modbus_handler(pd); } } } } ``` #### 效果演示 ### 控制台转移(对rt-thread原生支持) #### **用途** - 用于直接转移shell控制台到任意串口端,然后支持本地、远程直接使用shell原生生态。 - 使用`ymodem`协议通过不同串口升级mcu内置固件。 - 自定义shell指令,然后通过不同的串口本地、远程控制mcu,来实现一些特殊功能。 #### **配置** - 重载`lhc modbus`的内置`ota`方法 ```c /** * @brief modbus协议栈进行控制台转移 * @param pd modbus协议站句柄 * @retval None */ static void lhc_console_transfer(p_lhc_modbus_t pd) { extern void finsh_set_device(const char *device_name); rt_err_t ret = RT_EOK; if (NULL == pd || NULL == pd->Uart.huart || NULL == pd->dev) return; pd->old_rx_indicate = pd->dev->rx_indicate; //备份接收完成通知(串口关闭前) ret = rt_device_close(pd->dev); if ((RT_EOK == ret) && (RT_EOK == rt_sem_trytake(g_console_sem))) { modbus_console_handle = pd; //记录当前正在使用控制台的modbus句柄 pd->old_console = rt_console_set_device(pd->dev->parent.name); finsh_set_device(pd->dev->parent.name); rt_kprintf("@note: enter finsh mode.\r\n"); } else //转移失败后必须重启当前串口和挂接接收中断,否则该串口将无法使用 { rt_device_open(pd->dev, RT_DEVICE_FLAG_TX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING); /*挂接目标接收中断函数*/ rt_device_set_rx_indicate(pd->dev, (rt_err_t(*)(rt_device_t, rt_size_t))pd->old_rx_indicate); rt_kprintf("@error: finsh already locked.\r\n"); } // 没有挂起的必要:中断函数转移后将会永久挂起 } ``` - 添加finsh控制台自定义指令 ```c #ifdef FINSH_USING_MSH #include /** * @brief finsh进行控制台还原 * @param pd modbus协议站句柄 * @retval None */ void re_console(void) { rt_err_t ret = RT_EOK; p_lhc_modbus_t pd = modbus_console_handle; rt_device_t console = pd->old_console; if ((RT_NULL == pd) || (RT_NULL == pd->dev) || (RT_NULL == console)) return; ret = rt_device_close(pd->dev); if (RT_EOK == ret) { finsh_set_device(console->parent.name); rt_console_set_device(console->parent.name); ret = rt_device_open(pd->dev, RT_DEVICE_FLAG_TX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING); if (RT_EOK == ret) { rt_device_set_rx_indicate(pd->dev, (rt_err_t(*)(rt_device_t, rt_size_t))pd->old_rx_indicate); // 重新设置空当前串口回调函数 rt_kprintf("\r\n@note: enter modbus_rtu mode.\r\n"); } if (g_console_sem->value < 1) //做二值信号量使用 rt_sem_release(g_console_sem); //释放控制台 } } MSH_CMD_EXPORT(re_console, finsh console restore.); #endif ``` #### 效果演示 ### 其他说明 - 主机例程暂时没有添加 - modbus tcp暂时不支持 - 当前控制台转移功能支持自定义指令,当暂时不开放重载自定义识别功能 - 更多有趣、实用的功能,不妨大胆的发挥你的想象力,进行探索吧 ## 联系方式 & 感谢 - 维护:LHC324 - 主页:https://github.com/LHC324/lhc_modbus - 邮箱:[1606820017@qq.com](mailto:1606820017@qq.com) ## 许可 采用 Apache-2.0 开源协议,细节请阅读项目中的 LICENSE 文件内容。