SeanLib系列函数库-ModbusRTU
查看其它库函数说明,请点击此处跳转到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表示ON)RTU_Status_t(*ReadCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用,写一个线圈,参数:从站站号、线圈地址、线圈值(char类型,1表示ON)RTU_Status_t(*WriteCoil)(unsignedchar,unsignedshort,unsignedchar);//主站专用,写多个线圈,参数:从站站号、线圈地址、写入数量、要写入的线圈数组(char类型,1表示ON)RTU_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表示ON,0表示OFF * CoilCount : 指定从站支持访问的线圈总数量 * Regs : 指定要与主站同步的寄存器起始地址 * RegCount : 指定从站支持访问的寄存器总数量 * 返回值: 创建成功返回设备指针,否则返回空指针NULL *******************************************************************************/externRTU_Device_t*NewRtuSlave(unsignedcharSite,unsignedshortBufSize,unsignedchar*Coils,unsignedshortCoilCount,unsignedshort*Regs,unsignedshortRegCount);BufSize:指定缓存区大小,创建设备时会动态申请一块内存,用于存放modbut通讯的数据帧,大小计算方法为:
- 先确定一个命令最多可能读写的线圈数据M和寄存器数量N
- 计算线圈帧最大大小Size_M = 9 + (M+7)/8 字节,(M+7)/8需要向上取整
- 计算寄存器帧最大大小Size_N = 9 + 2N 字节
- 取Size_M和Size_N的最大值作为BufSize即可。建议额外增加一些,防止内存溢出
比如,一个写10个寄存器的帧大小为:9+2*10=29字节
另外,本库依赖两个外部数据接收和发送函数,请在SeanLib.c中实现:
//1.数据发送函数:Sean_Send//参数:// Dev: 调用该函数的设备,函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(Dev==PLC){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){Motor=NewRtuDevice(0);//用站号0创建RTU设备,表示本机是主站if(Motor==NULL){printf("Creat RTU device failed!\r\n");while(1);}Motor->Site=SITE;//设置从站站号}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(Dev==Motor){HAL_UART_Transmit(&huart2,Data,Len,Len);}}//2.数据接收函数:(对于RTU库仅主站需要,从站不需要该函数)//参数:// Dev: 调用该函数的设备,函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 接收到的数据的存放指针// Len: 最多允许接收的数据长度//返回值: 实际接收到的数据长度unsignedshortSean_Recv(void*Dev,unsignedchar*Data,unsignedshortMaxLen){unsignedshortLen=0;if(Dev==Motor){HAL_UARTEx_ReceiveToIdle(&huart2,Data,MaxLen,&Len,10000);}returnLen;}2.2.3 读写从站寄存器或线圈
调用下列方法可以直接操作从站的寄存器和线圈:
//主站专用,读线圈函数,参数:从站站号、线圈地址、读取数量、存放线圈值的数组(char类型,1表示ON)RTU_Status_t(*ReadCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用,写一个线圈,参数:从站站号、线圈地址、线圈值(char类型,1表示ON)RTU_Status_t(*WriteCoil)(unsignedchar,unsignedshort,unsignedchar);//主站专用,写多个线圈,参数:从站站号、线圈地址、写入数量、要写入的线圈数组(char类型,1表示ON)RTU_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){PLC=NewRtuSlave(Config.Site,100,Coils,16,Regs,10);if(PLC==NULL){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 TaskWoken=pdFALSE;//使用之前必须初始化为Falseuint32_tSR=huart->Instance->SR;//读SR寄存器,检查中断标志uint32_tCR1=huart->Instance->CR1;//读CR1寄存器,检查中断使能状态if(huart==&UART_PC){...}elseif(huart==&UART_HMI){...}elseif(huart==&UART_PLC){if((SR&USART_SR_IDLE)&&(CR1&USART_CR1_IDLEIE))//发生了空闲中断,说明数据接收完成{temp=(uint8_t)huart->Instance->DR;//读一次DR,清除中断标志xTaskNotifyFromISR(Task_Uart,EVENT_UART_PLC,eSetBits,&TaskWoken);//发送任务通知}if((SR&USART_SR_TC)&&(CR1&USART_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_PLC=0,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(EventBits&EVENT_UART_PC)//PC的数据接收完成,执行命令解析处理{...}if(EventBits&EVENT_UART_HMI)//如果有HMI接收完成的事件{...}if(EventBits&EVENT_UART_PLC)//PLC的数据接收完成,执行Modbus数据解析处理{Len_PLC=sizeof(cmd_PLC)-__HAL_DMA_GET_COUNTER(&hdma_usart3_rx);HAL_UART_DMAStop(&UART_PLC);site=cmd_PLC[0];//从本包数据中取出站号func=cmd_PLC[1];//从本包数据中取出功能码Ret=PLC->SlaveHandle(cmd_PLC,Len_PLC);//解析Modbus命令if(Ret==RTU_Status_OK){if(site==Config.Site)//如果本包数据是发给本站的,需要判断是否主站更改了本站的线圈和寄存器{if(func==0x05||func==0x06||func==0x10)//如果主站是写操作,则需要触发主任务进行处理{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(Dev==PLC){UART_PLC.gState=HAL_UART_STATE_READY;//手动恢复串口的状态UART_PLC.hdmatx->State=HAL_DMA_STATE_READY;//手动恢复DMA的状态__HAL_UNLOCK(&hdma_usart3_tx);//手动解锁DMAHAL_UART_Transmit_DMA(&UART_PLC,Data,Len);//用DMA发送数据}}