查看其它库函数说明请点击此处跳转到SeanLib主页1. 本篇内容本篇讲解ModbusRTU库的用法单片机使用此库可作为主站或从站与远程设置进行通讯本库自动完成数据解析、打包和发送大大简化了用户代码。2. 使用说明要使用本库请将ModbusRTU.lib和ModbusRTU.h添加到项目工程中2.1 头文件介绍头文件中定义了一个RTU设备类型typedefstruct//定义一个Modbus RTU数据包结构体类型{unsignedcharIsMaster;//表示本设备是否是Modbus主站unsignedcharSite;//从站有效表示本站站号unsignedshortCoilCount;//从站有效存放允许主站访问的线圈点数unsignedshortRegCount;//从站有效存放允许主站访问的寄存器数量unsignedshortBufSize;//RTU自身使用的数据缓存区大小unsignedchar*Buffer;//RTU自身要使用的数据缓存区指针unsignedchar*Coils;//从站有效存放允许主站访问的线圈首地址每个变量表示一个线圈unsignedshort*Regs;//从站有效存放允许主站访问的寄存器首址每个变量表示一个寄存器//从站专用解析主站发送过来的数据并做出响应RTU_Status_t(*SlaveHandle)(unsignedchar*,unsignedchar);//主站专用读线圈函数参数从站站号、线圈地址、读取数量、存放线圈值的数组char类型1表示ONRTU_Status_t(*ReadCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用写一个线圈参数从站站号、线圈地址、线圈值char类型1表示ONRTU_Status_t(*WriteCoil)(unsignedchar,unsignedshort,unsignedchar);//主站专用写多个线圈参数从站站号、线圈地址、写入数量、要写入的线圈数组char类型1表示ONRTU_Status_t(*WriteCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用读寄存器 参数从站站号、起始地址、读取数量、存放寄存器的数组short类型RTU_Status_t(*ReadRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);//主站专用写一个寄存器参数从站站号、寄存器地址、要写入的数据RTU_Status_t(*WriteReg)(unsignedchar,unsignedshort,unsignedshort);//主站专用写多个寄存器参数从站站号、起始地址、写入数量、要写入的寄存器数组short类型RTU_Status_t(*WriteRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);}RTU_Device_t;结构体中的成员都是私有RTU设备创建之后就不要修改。Modbus通讯需要区分主站和从站本库分别提供了创建主站和从站设备的方法/******************************************************************************* * 功 能: 创建一个Modbus RTU主站设备 * 参 数: * BufSize : 指定为设备开辟的缓存区大小用于存储通讯中发送和接收使用的数据包 * 返回值: 创建成功返回设备指针否则返回空指针NULL *******************************************************************************/externRTU_Device_t*NewRtuMaster(unsignedshortBufSize);/******************************************************************************* * 功 能: 创建一个Modbus RTU从站设备 * 参 数: * Site : 本站站号 * BufSize : 指定为设备开辟的缓存区大小用于存储通讯中发送和接收使用的数据包 * Coils : 指定要与主站同步的线圈起始地址每个元素表示一个线圈1表示ON0表示OFF * CoilCount : 指定从站支持访问的线圈总数量 * Regs : 指定要与主站同步的寄存器起始地址 * RegCount : 指定从站支持访问的寄存器总数量 * 返回值: 创建成功返回设备指针否则返回空指针NULL *******************************************************************************/externRTU_Device_t*NewRtuSlave(unsignedcharSite,unsignedshortBufSize,unsignedchar*Coils,unsignedshortCoilCount,unsignedshort*Regs,unsignedshortRegCount);BufSize指定缓存区大小创建设备时会动态申请一块内存用于存放modbut通讯的数据帧大小计算方法为先确定一个命令最多可能读写的线圈数据M和寄存器数量N计算线圈帧最大大小Size_M 9 (M7)/8 字节(M7)/8需要向上取整计算寄存器帧最大大小Size_N 9 2N 字节取Size_M和Size_N的最大值作为BufSize即可。建议额外增加一些防止内存溢出比如一个写10个寄存器的帧大小为92*1029字节另外本库依赖两个外部数据接收和发送函数请在SeanLib.c中实现//1.数据发送函数Sean_Send//参数:// Dev: 调用该函数的设备函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(DevPLC){HAL_UART_Transmit(UART_PLC,Data,Len,30);}}//2.数据接收函数Sean_Recv对于RTU库仅主站需要从站不需要该函数//参数:// Dev: 调用该函数的设备可根据该指针判断要将数据发送到哪个通讯端口// Data: 接收到的数据的存放指针// Len: 最多允许接收的数据长度//返回值: 实际接收到的数据长度unsignedshortSean_Recv(void*Dev,unsignedchar*Data,unsignedshortMaxLen){uint16_tRecv_Len;HAL_UARTEx_ReceiveToIdle(UART_PLC,Data,MaxLen,Recv_Len,30);returnRecv_Len;}2.2 作为主站时的用法单片机做主站时可以主动发起通讯指令读取或写入从站设备的线圈或寄存器常用于连接如温控器、PLC、压力传感器之类的从站设备2.2.1 创建主站设备示例代码RTU_Device_t*Motor;//RTU设备初始化voidRTU_Init(void){MotorNewRtuDevice(0);//用站号0创建RTU设备表示本机是主站if(MotorNULL){printf(Creat RTU device failed!\r\n);while(1);}Motor-SiteSITE;//设置从站站号}MSH_INIT_EXPORT(2,RTU_Init,ModbusRTU Package initialization);2.2.2 编写外部函数作为从站时只需要实现Sean_Send即可请在SeanLib.c中实现该函数//1.数据发送函数//参数:// Dev: 调用该函数的设备函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(DevMotor){HAL_UART_Transmit(huart2,Data,Len,Len);}}//2.数据接收函数对于RTU库仅主站需要从站不需要该函数//参数:// Dev: 调用该函数的设备函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 接收到的数据的存放指针// Len: 最多允许接收的数据长度//返回值: 实际接收到的数据长度unsignedshortSean_Recv(void*Dev,unsignedchar*Data,unsignedshortMaxLen){unsignedshortLen0;if(DevMotor){HAL_UARTEx_ReceiveToIdle(huart2,Data,MaxLen,Len,10000);}returnLen;}2.2.3 读写从站寄存器或线圈调用下列方法可以直接操作从站的寄存器和线圈//主站专用读线圈函数参数从站站号、线圈地址、读取数量、存放线圈值的数组char类型1表示ONRTU_Status_t(*ReadCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用写一个线圈参数从站站号、线圈地址、线圈值char类型1表示ONRTU_Status_t(*WriteCoil)(unsignedchar,unsignedshort,unsignedchar);//主站专用写多个线圈参数从站站号、线圈地址、写入数量、要写入的线圈数组char类型1表示ONRTU_Status_t(*WriteCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用读寄存器 参数从站站号、起始地址、读取数量、存放寄存器的数组short类型RTU_Status_t(*ReadRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);//主站专用写一个寄存器参数从站站号、寄存器地址、要写入的数据RTU_Status_t(*WriteReg)(unsignedchar,unsignedshort,unsignedshort);//主站专用写多个寄存器参数从站站号、起始地址、写入数量、要写入的寄存器数组short类型RTU_Status_t(*WriteRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);2.3 作为从站时的用法单片机做从站时需要及时响应主站发送的通讯指令根据指令发送或改写本地的线圈或寄存器的值常用于连接如HMI、PC之类的主站设备。本地需要创建一个线圈和寄存器的数组如uint8_tCoils[16]{0};//定义允许Modbus访问的线圈组uint16_tRegs[10]{0};//定义允许Modbus访问的寄存器组工作流程串口接收数据–解析请求帧–生成响应帧–发送响应帧根据这个流程非常适合用DMA来接收和发送串口数据后面的例子均按DMA的方案进行介绍。2.3.1 创建从站设备voidRTU_Init(void){PLCNewRtuSlave(Config.Site,100,Coils,16,Regs,10);if(PLCNULL){Error_Handle(0,Creat rtu device failed!);}}MSH_INIT_EXPORT(1,RTU_Init,Creat modbus rtu slave);Config.Site为本机站号2.3.2 串口数据接收voidUSART_IT_Callback(UART_HandleTypeDef*huart){uint8_ttemp;BaseType_t TaskWokenpdFALSE;//使用之前必须初始化为Falseuint32_tSRhuart-Instance-SR;//读SR寄存器检查中断标志uint32_tCR1huart-Instance-CR1;//读CR1寄存器检查中断使能状态if(huartUART_PC){...}elseif(huartUART_HMI){...}elseif(huartUART_PLC){if((SRUSART_SR_IDLE)(CR1USART_CR1_IDLEIE))//发生了空闲中断说明数据接收完成{temp(uint8_t)huart-Instance-DR;//读一次DR清除中断标志xTaskNotifyFromISR(Task_Uart,EVENT_UART_PLC,eSetBits,TaskWoken);//发送任务通知}if((SRUSART_SR_TC)(CR1USART_CR1_TCIE))//发生了发送完成中断{//DMA会开启该中断因此需要在这里清除中断标志或直接关闭中断使能__HAL_UART_DISABLE_IT(huart,UART_IT_TC);}}portYIELD_FROM_ISR(TaskWoken);//如果TaskWoken为true进行一次上下文切换}2.3.3 编写串口数据处理任务//线程串口数据处理线程voidStartTaskUart(void*argument){uint8_tcmd_PLC[50];//定义PLC的接收缓存区uint8_tLen_PLC0,temp;uint8_tsite,func;EventBits_t EventBits;RTU_Status_t Ret;__HAL_UART_ENABLE_IT(UART_PLC,UART_IT_IDLE);//开启PLC串口的空闲中断HAL_UART_Receive_DMA(UART_PLC,cmd_PLC,sizeof(cmd_PLC));//启动DMA接收while(1){xTaskNotifyWait(//等待有串口数据到达0x00,//进入时不清除任何标志位EVENT_UART_ALL,//退出时清除相关标志位EventBits,portMAX_DELAY);if(EventBitsEVENT_UART_PC)//PC的数据接收完成执行命令解析处理{...}if(EventBitsEVENT_UART_HMI)//如果有HMI接收完成的事件{...}if(EventBitsEVENT_UART_PLC)//PLC的数据接收完成执行Modbus数据解析处理{Len_PLCsizeof(cmd_PLC)-__HAL_DMA_GET_COUNTER(hdma_usart3_rx);HAL_UART_DMAStop(UART_PLC);sitecmd_PLC[0];//从本包数据中取出站号funccmd_PLC[1];//从本包数据中取出功能码RetPLC-SlaveHandle(cmd_PLC,Len_PLC);//解析Modbus命令if(RetRTU_Status_OK){if(siteConfig.Site)//如果本包数据是发给本站的需要判断是否主站更改了本站的线圈和寄存器{if(func0x05||func0x06||func0x10)//如果主站是写操作则需要触发主任务进行处理{xTaskNotifyGive(Task_Main);//通知主任务线圈被更改了}}}HAL_UART_Receive_DMA(UART_PLC,cmd_PLC,sizeof(cmd_PLC));//再次启动DMA接收}}}SlaveHandle函数会自动识别站号、功能码根据功能码操作前面定义的线圈和寄存器数组Coils、Regs。只是需要注意在主站改写线圈或寄存器后用户代码记得进行处理即可。2.3.4 编写外部函数作为从站时只需要实现Sean_Send即可请在SeanLib.c中实现该函数//1.数据发送函数Sean_Send//参数:// Dev: 调用该函数的设备函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(DevPLC){UART_PLC.gStateHAL_UART_STATE_READY;//手动恢复串口的状态UART_PLC.hdmatx-StateHAL_DMA_STATE_READY;//手动恢复DMA的状态__HAL_UNLOCK(hdma_usart3_tx);//手动解锁DMAHAL_UART_Transmit_DMA(UART_PLC,Data,Len);//用DMA发送数据}}