# OpenModbusFor i.MX RT **Repository Path**: hudiekaxp/open-modbus-for-imxrt ## Basic Information - **Project Name**: OpenModbusFor i.MX RT - **Description**: OpenModbusFor i.MX RT - **Primary Language**: C - **License**: BSD-3-Clause - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-04-16 - **Last Updated**: 2025-02-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # OpenModbus RTU for i.MX RT 使用说明 ## 1. 简介 OpenModbus是一款开源Modbus RTU软件,采用非阻塞函数调用,无需使用RTOS,支持主/从机模式,并支持多个主从机在同一个设备运行。OpenModbus RTU主要支持NXP Kinetis系列,For RT版本则支持NXP i.MX RT系列MCU,本项目致力于简化用户的操作,用户甚至可以几乎不了解Modbus RTU即可完成Slave设备的开发。 项目主要包含9个文件: | 文件名 | 描述 | | ------------------ | ------------------------------------------------------------ | | Modbus.c | OpenModbus核心文件,主要用处理各种功能码,用户无需修改 | | Modbus.h | OpenModbus核心头文件,主要用处理各种功能码,用户无需修改 | | Modbus_Porting.c | OpenModbus移植文件,根据不同MCU平台需要进行修改,底层调用SDK | | Modbus_Porting.h | OpenModbus移植头文件,根据不同MCU平台需要进行修改,底层调用SDK | | ModbusSlaveApp.c | OpenModbus从站示例代码文件 | | ModbusSlaveApp.h | OpenModbus从站示例代码头文件 | | ModbusMasterApp.c | OpenModbus主站示例代码文件 | | ModbusMasterApp.h | OpenModbus主站示例代码头文件 | | ModbusUserConfig.h | OpenModbus用户配置头文件 | 本项目测试环境: | 所需资源 | 描述 | | ------------------- | ------------------------- | | MIMXRT1010-EVK | x1 | | Micro USB | x1 | | NXP MCUxpresso SDK | 2.10.0 | | IAR EW for Arm | 9.10.2 | | MCULink driver | Virtual COM and CMSIS DAP | ## 2. 使用说明 ### 2.1 配置Modbus工作模式 首先根据需求定义在ModbusUserConfig.h中配置Modbus: ```c #ifndef __MODBUS_USERCONFIG_H__ #define __MODBUS_USERCONFIG_H__ #define MODBUS_MASTER_USED //MASTER总开关,如果屏蔽掉则不支持主站功能 #define MODBUS_SLAVE_USED //SLAVE总开关,如果屏蔽掉则不支持从站功能 #ifdef MODBUS_SLAVE_USED //如果开启从站功能 #define SLAVE_PORT0 0 //设置从站端口号 #define MODBUS_SLAVE_NUMBER 1 //设置从站个数,如果需要两路UART做从站,需要修改此参数为2 #define MODBUS_SLAVE0_ADDRESS 51 //设置从站地址 #endif #ifdef MODBUS_MASTER_USED //如果开启主站功能 #define MASTER_PORT0 0 //设置主站端口号 #define MODBUS_MASTER_NUMBER 1 //设置主站个数,如果需要两路UART做主站,需要修改此参数为2 #define SLAVE_READ_NUM 5 //设置需要访问的从站输入设备个数 #define SLAVE_WRITE_NUM 5 //设置需要访问的从站输出设备个数 #endif ``` ### 2.2 修改Modbus_Porting.c For i.MX RT版本OpenModbus已经移植好2路UART(1 for Master, 1 for Slave)到Modbus_Porting.c,下面列出了其所用到的资源情况: | Device | Mode | PIT | Interrupt | UART Pin | | ------ | ------------------------ | -------- | --------- | --------------------------- | | UART | Polling TX, Interrupt RX | Yes, Ch0 | UART RX | TX, RX, RTS(Only for RS485) | | | Polling TX, DMA RX | Yes, Ch0 | UART IDLE | TX, RX, RTS(Only for RS485) | | | DMA TX, Interrupt RX | Yes, Ch0 | UART RX | TX, RX, RTS(Only for RS485) | | | DMA TX, DMA RX | Yes, Ch0 | UART IDLE | TX, RX, RTS(Only for RS485) | 如果这两路UART已经足够用户使用,几乎是不需要做修改的。但用户硬件设计时可能会使用不同的UART或者Pin,这时就需要修改UART相关的宏来切换UART。下面以Master为例,如果硬件将LPUART2连接到Modbus Master, 则可以将下列代码中的LPUART1改为LPUART2,注意如果使用了DMA,MASTER0_TX_DMA_REQUEST,MASTER0_RX_DMA_REQUEST这两个宏也要进行相应的修改。 ```c #define MASTER0_UART_BAUDRATE 115200 #define MASTER0_UART_Rx_IRQ LPUART1_IRQn #define MASTER0_Rx_ISR LPUART1_IRQHandler #define MASTER0_UART LPUART1 #define MASTER0_TX_DMA_REQUEST kDmaRequestMuxLPUART1Tx #define MASTER0_RX_DMA_REQUEST kDmaRequestMuxLPUART1Rx #define MASTER0_TX_DMA_CHANNEL 0U #define MASTER0_RX_DMA_CHANNEL 1U ``` 默认情况下,UART通过DMA发送和接收数据,相关宏在Modbus_Porting.h中:MASTER_UART_TX_DMA, MASTER_UART_RX_DMA都设置为1时表示开启DMA传输 ![image-20220418160844546](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418160844546.png) 这样可以保证更低的MCU loading,可以去处理更多的其他外设的中断。同时,也可以开启更多的串口,而且几乎不需要考虑串口之间中断接收的优先级问题。这里使用了i.MX RT芯片内置的UART接收空闲中断,并设置接收空闲4Char产生中断,也就是说当帧间距大于4Char时如果RX始终未IDLE状态,Modbus协议规定的应该时3.5Char。这里有个风险就是如果总线上的帧间距3.5~4Char之间,其实是满足Modbus规范的,使用本方案就会出现问题。有两种解决方案: 1. 设置MASTER_UART_RX_DMA为0。这样就会使用UART RX中断接收数据,每接收一个数据就会进一次中断(不考虑FIFO的情况下),中断服务程序中会通过PIT定时器来判断3.5Char的时间 2. 设置IDLE char为2字节,这样当接收空闲2Char产生中断。这样做的兼容性更高,对端设备如果大于2Char,小于3.5Char,虽然是不满足Modbus规范的,但始终还是能连上。 ``` void Slave0_UART_Configuration(uint32_t buadrate) { …… config.rxIdleType = kLPUART_IdleTypeStopBit; config.rxIdleConfig = kLPUART_IdleCharacter2; …… } ``` ### 2.3 修改Pin MUX Project工程中提供了.mex文件,该文件是NXP MCUxpresso Config tools的配置文件,安装该工具后双击即可打开来配置时钟或者引脚,这里主要用它来配置管脚: ![image-20220418162044519](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418162044519.png) 用户可以根据硬件设计选择UART的TX, RX, 如果使用RS485还需要使用RTS Pin连接收发器的使能端,i.MX RT Series是支持通过该pin自动切换收发的 ![image-20220418163150491](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418163150491.png) 当设置好管脚后,点击上面绿色的更新源代码按钮,UART Pin设置就完成了 ![image-20220418163259372](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418163259372.png) ### 2.4 更多串口 如果用户需要更多的串口,请参考OpenModbus RTU移植指南(尚未完成)。 ## 3. 应用说明 ### 3.1 功能码 OpenModbus目前所支持的功能码列表如下: | 功能码 | | | ------ | ------------ | | 0x01 | 读线圈 | | 0x02 | 读离散量输入 | | 0x03 | 读保持寄存器 | | 0x04 | 读输入寄存器 | | 0x05 | 写单个线圈 | | 0x06 | 写单个寄存器 | | 0x0F | 写多个线圈 | | 0x10 | 写多个寄存器 | ### 3.2 Slave OpenModbus做从站是非常简单的,用户甚至不需要知道各个功能码的具体意义,它已经封装好了各个功能码的操作,并将通道的数据对应到数组g_u8SlaveBuf中。 举例说明: 如果做输入设备,用户仅需要将数据写入g_u8SlaveBuf中,主站即可通过0x3,0x4功能码读取到对应偏移的数据。 ```c void ModbusNet1SlaveAPP() { g_u8SlaveBuf[0] = 0x12; g_u8SlaveBuf[1] = 0x34; } ``` ### 3.3 Master OpenModbus已经提供了一个通过状态机访问从站的示例代码,用户只需要根据从站编辑两个数组即可将从站获取的数据放到数组g_u8MasterBuf: ```c typedef struct { uint16_t Offset; //用户数据区偏移 uint16_t Number; //寄存器个数 uint16_t RegisterAddress; //寄存器地址 uint8_t SlaveAddress; //从站地址 uint32_t TimeOut; //超时时间 uint8_t Function; //功能码 } MODBUS_SLAVELISTtyp; const MODBUS_SLAVELISTtyp g_SlaveWriteList[SLAVE_WRITE_NUM] = { {0, 1, 0, 51, 1000, 6}, {2, 1, 1, 51, 1000, 6}, {4, 1, 2, 51, 1000, 6}, {6, 1, 3, 51, 1000, 6}, {8, 1, 4, 51, 1000, 6} }; const MODBUS_SLAVELISTtyp g_SlaveReadList[SLAVE_READ_NUM] = { {10, 1, 0, 52, 1000, 3}, {12, 1, 1, 52, 1000, 3}, {14, 1, 2, 52, 1000, 3}, {16, 1, 3, 52, 1000, 3}, {18, 1, 4, 52, 1000, 3} }; ``` | 变量名 | 描述 | | --------------- | ------------------------------------------------------------ | | offset | 表示该从站数据在g_u8MasterBuf的偏移量,用户在使用的时候需要注意不要将不同的从站设备有覆盖 | | Number | 表示该从站访问的寄存器个数 | | RegisterAddress | 表示该从站访问的寄存器起始地址 | | SlaveAddress | 表示该从站地址 | | TimeOut | 表示等待该从站的超时时间 | | Function | 表示访问该从站的功能码 | ### 3.4异常处理 OpenModbus在主站模式下,定义了部分异常: ```c #define MODBUS_ERROR_OK 0 #define MODBUS_ERROR_MEMORY 1 #define MODBUS_ERROR_OPENFAILD 2 #define MODBUS_ERROR_REGISTER_ADDRESS_ODD 3 #define MODBUS_ERROR_TIMEOUT_LIMIT 4 #define MODBUS_ERROR_SLAVEADDR 5 #define MODBUS_ERROR_REGISTER_ADDRESS_OVERFLOW 6 #define MODBUS_ERROR_NUMBER 7 #define MODBUS_ERROR_FUNCTION 8 #define MODBUS_ERROR_TIMEOUT 9 #define MODBUS_ERROR_PROTOCOL 10 ``` 而实际代码仅处理了3个异常 MODBUS_ERROR_MEMORY:内存访问出错 MODBUS_ERROR_TIMEOUT_LIMIT:从站设备超时未应答 MODBUS_ERROR_FUNCTION:未支持功能码 具体错误记录在g_MProcess[port].Error变量 ## 4. 测试验证 ### 4.1 Slave测试 由于EVK仅提供了1个虚拟串口,默认主从代码都使用LPUART1,测试从站代码时可以在ModbusUserConfig.h中先屏蔽Master的总开关(MODBUS_MASTER_USED),编译下载工程,PC端打开Modbus Poll软件模拟主站,并根据下图设置: ![image-20220418175216838](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418175216838.png) ![image-20220418175244859](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418175244859.png) 点击Connect ![image-20220418175312292](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418175312292.png) ![image-20220418175341950](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418175341950.png) 连接成功后,可以看到ModbusSlaveApp.c中写入的0x1234 ![image-20220418183348331](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418183348331.png) 使用功能码0x02获取离散量数据,可以看到如下结果,这个值正好对应0x3412 ![image-20220418230541630](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418230541630.png) ### 4.2 Master测试 由于EVK仅提供了1个虚拟串口,默认主从代码都使用LPUART1,测试主代码时可以在ModbusUserConfig.h中先屏蔽Slave的总开关(MODBUS_SLAVE_USED),编译并下载工程,PC端打开Modbus Slave软件模拟从站,设置从站为52,功能码为0x03(根据g_SlaveReadList设置): ![image-20220418173921595](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418173921595.png) 并设置寄存器0~4如下: ![image-20220418174037094](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418174037094.png) 点击Connect,并设置如下: ![image-20220418174126102](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418174126102.png) IAR环境中通过Live Watch就可以看到: ![image-20220418174349405](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220418174349405.png) g_u8MasterBuf偏移10~19对应的功能码3,读入的值与Modbus Slave设置的值一致 ```c const MODBUS_SLAVELISTtyp g_SlaveReadList[SLAVE_READ_NUM] = { {10, 1, 0, 52, 1000, 3}, {12, 1, 1, 52, 1000, 3}, {14, 1, 2, 52, 1000, 3}, {16, 1, 3, 52, 1000, 3}, {18, 1, 4, 52, 1000, 3} }; ``` ## 5. 底层Driver修改 5.1 SDK修改点: OpenModbus底层驱动基于NXP MCUxpresso SDK 2.10.0修改而来,其中修改了SDK driver部分函数: 1. 修改fsl_edma.c->void EDMA_CreateHandle()函数,禁止eDMA完成中断 ![image-20220419000655438](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220419000655438.png) 2. 修改fsl_edma.c->void EDMA_Init()函数,禁止清零ERQ,这样做是因为如果使能多个DMA,这里清零代码会让之前使能的DMA通道关闭 ![image-20220419000829065](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220419000829065.png) 3. 修改fsl_lpuart.c->LPUART_Init()函数对应RTS功能 ![image-20220419000543170](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220419000543170.png) 4. 修改fsl_lpuart_edma.c->LPUART_SendEDMACallback(), 屏蔽开启UART TC中断 ![image-20220419000446006](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220419000446006.png) 重新定义UART ISR函数 SDK默认需要使能回调函数处理接收数据,OpenModbus重新自定义了UART中断服务程序入口(注意ISR中有几处寄存器操作并未调用SDK): ```c #define SLAVE0_Rx_ISR LPUART1_IRQHandler //中断服务程序示例 void SLAVE0_Rx_ISR() { #if SLAVE_UART_RX_DMA #else uint8_t len = 0, i = 0; static uint32_t last_time = 0; uint32_t current_time = 0; #endif uint8_t data = 0; uint32_t lpuart_status = 0; lpuart_status = LPUART_GetStatusFlags(SLAVE0_UART); //处理异常标志及清理FIFO if((kLPUART_RxOverrunFlag|kLPUART_NoiseErrorFlag|kLPUART_FramingErrorFlag|kLPUART_ParityErrorFlag) & lpuart_status) { data = LPUART_ReadByte(SLAVE0_UART); SLAVE0_UART->FIFO |= (LPUART_FIFO_TXFLUSH_MASK | LPUART_FIFO_RXFLUSH_MASK); SLAVE0_UART->STAT |= LPUART_STAT_OR_MASK | LPUART_STAT_FE_MASK | LPUART_STAT_PF_MASK | LPUART_STAT_NF_MASK; return; } //DMA 方式接收 #if SLAVE_UART_RX_DMA /* If new data arrived. */ if ((kLPUART_IdleLineFlag) & lpuart_status) { //清理中断标志 LPUART_ClearStatusFlags(SLAVE0_UART, kLPUART_IdleLineFlag); //获取当前DMA传输个数 g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount = SLAVE0_UART_GetRingBufferLengthDMA(); //拷贝数据 memcpy(&g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.byData[0], (void *)&g_SLAVE0_UART_BUFFER[0], g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount); if(ModbusSlaveReceiveInt((unsigned char*)g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.byData, g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount, SLAVE_PORT0)) { ; } if(g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount >= MODBUS_BUFF_SIZE) { g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount = 0; } //重新开始接收下一帧数据 LPUART_TransferAbortReceiveEDMA(SLAVE0_UART, &g_SLAVE0_UART_lpuartDmaHandle); memset((void *)&g_SLAVE0_UART_BUFFER[0], 0, sizeof(g_SLAVE0_UART_BUFFER)); /* Receive cmd*/ g_SLAVE0_UART_receiveXfer.data = &g_SLAVE0_UART_BUFFER[0]; g_SLAVE0_UART_receiveXfer.dataSize = MODBUS_BUFF_SIZE; LPUART_ReceiveEDMA(SLAVE0_UART, &g_SLAVE0_UART_lpuartDmaHandle, &g_SLAVE0_UART_receiveXfer); } //中断接收 #else /* If new data arrived. */ if ((kLPUART_RxDataRegFullFlag) & lpuart_status) { current_time = g_ModbusSlavePort[SLAVE_PORT0].Timer0_Value_Get(); #if TIMER_MINUS == 1 //如果定时器Counter是递减 if ((last_time - current_time) > g_ModbusSlavePort[SLAVE_PORT0].Timer0_Wait3_5char()) #else if ((current_time - last_time) > g_ModbusSlavePort[SLAVE_PORT0].Timer0_Wait3_5char()) #endif { g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount = 0; } /* Get the size that can be stored into buffer for this interrupt. */ #if defined(FSL_FEATURE_LPUART_HAS_FIFO) && FSL_FEATURE_LPUART_HAS_FIFO len = ((uint8_t)((SLAVE0_UART->WATER & LPUART_WATER_RXCOUNT_MASK) >> LPUART_WATER_RXCOUNT_SHIFT)); #else len = 1; #endif for(i = 0; i< len; i++) { data = LPUART_ReadByte(SLAVE0_UART); g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.byData[g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount++] = data; } if(ModbusSlaveReceiveInt((unsigned char*)g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.byData, g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount, SLAVE_PORT0)) { ; } if(g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount >= MODBUS_BUFF_SIZE) { g_ModbusSlavePort[SLAVE_PORT0].s_RxBuf.uCount = 0; } last_time = g_ModbusSlavePort[SLAVE_PORT0].Timer0_Value_Get(); } #endif /* Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F Store immediate overlapping exception return operation might vector to incorrect interrupt */ //#if defined __CORTEX_M && (__CORTEX_M == 4U) __DSB(); //#endif } ``` ## 6. 项目地址 https://gitee.com/hudiekaxp/open-modbus-for-imxrt.git 有问题可以联系我的个人微信 ![image-20220419003619764](https://gitee.com/hudiekaxp/WeChatImage/raw/master/image-20220419003619764.png)