对于Modbus TCP来讲与Modbus RTU和Modbus ASCII有比较大的区别,由于它是运行于以太网链路之上,是运行于TCP/IP协议之上的一种应用层协议。在协议栈的前两个版本中,Modbus TCP做为客户端时也存在一些局限性。咱们将对这些不足做必定更新。git
1、存在的不足github
在原有的协议栈中,咱们所封装的Modbus TCP客户端一个特定的客户端,即它只是一个客户端实例。在一般的应用中不会有什么问题,但在有些应用场合就会显现出它的局限性。数组
首先,做为一个特定的客户端,如果链接多个服务器目标时,修改服务器参数值的处理变的很是复杂,须要分辨是不一样的服务器,不一样的变量。当须要从不一样的网段操做数据时,咱们甚至须要标记不一样的网段。服务器
其次,做为一个特定的客户端,若是咱们操做的服务器参数类似时,哪怕来自于不一样的网段,咱们也须要仔细分辨或者传递额外的参数。由于同一客户端的解析函数是同一个。网络
最后,将多个Modbus TCP服务器通信都做为惟一的一个特定的服务器来处理,使得各部分混杂在一块儿,程序结构很不清晰,对象也不明确。函数
2、更新设计测试
考虑到前述的局限性,咱们将Modbus TCP客户端及其所访问的Modbus TCP服务器定义为通用的对象,而当咱们在具体应用中使用时,再将其特例化为特定的客户端和服务器对象。ui
首先咱们来考虑客户端,原则上咱们规划的每个客户端对象管理咱们设备上的一个IP网段的设备。那么在一个特定客户端下,咱们能够定义多达253个不一样的服务器。以下图所示:编码
从上图中咱们能够发现,咱们的目的就是让协议栈支持,多客户端和多服务器,而且在不一样客户端下能够访问同网段的多个服务器。接下来咱们还须要考虑服务器对象。客户端对服务器的操做无非两类:读服务器信息和写服务器信息。spa
对于读服务器信息来讲,客户端须要发送请求命令,等待服务器返回响应信息,而后客户端解析收到的信息并更新对应的参数值。由于返回的响应消息是没有对应的寄存器地址的,因此要想在解析的时候定位寄存器就必须知道发送的命令,为了便于分辨咱们将命令存放在服务器对象中。
而对于写服务器操做,不管写的要求来自于哪里,对于协议栈来讲确定是其它的数据处理进程发过来的,所接到要求后咱们须要记录是哪个客户端管理的哪个服务器的哪些参数。对于客户端咱们不须要分辨,由于每一个客户端都是独立的处理进程,可是对于服务器和参数咱们就须要分辨。每个客户端所管理的IP地址的最后一段为0到255,因此咱们能够依据来分辨服务器端。而在每个服务器节点中增长状态标志,用以记录请求状态,而全部服务器端组成链表。
3、编码实现
咱们已经设计了咱们的更新,接下来咱们就根据这一设计来实现它。咱们主要从如下几个方面来操做:第一,实现客户端对象类型和服务器对象类型;第二,客户端对象的实例化及服务器对象的实例化;第三,读服务器参数的客户端操做过程;第四,写服务器参的数客户端操做过程。接下来咱们将一一描述之。
3.1、定义对象类型
与在Modbus RTU和Modbus ASCII同样,在Modbus TCP协议栈的封装中,咱们也须要定义客户端对象和服务器对象,天然也免不了要定义这两种类型。
首先咱们来定义本地客户端的类型,其成员包括:一个uint32_t的写服务器标志数组;服务器数量字段;服务器顺序字段;本客户端所管理的服务器列表;4个数据更新函数指针。具体定义以下:
1 /* 定义本地TCP客户端对象类型 */ 2 typedef struct LocalTCPClientType{ 3 uint32_t transaction; //事务标识符 4 uint16_t cmdNumber; //读服务器命令的数量 5 uint16_t cmdOrder; //当前从站在从站列表中的位置 6 uint8_t (*pReadCommand)[12]; //读命令列表 7 ServerListHeadNode ServerHeadNode; //Server对象链表的头节点 8 UpdateCoilStatusType pUpdateCoilStatus; //更新线圈量函数 9 UpdateInputStatusType pUpdateInputStatus; //更新输入状态量函数 10 UpdateHoldingRegisterType pUpdateHoldingRegister; //更新保持寄存器量函数 11 UpdateInputResgisterType pUpdateInputResgister; //更新输入寄存器量函数 12 }TCPLocalClientType;
关于客户端对象类型,在前面的更新设计中已经讲的很清楚了,只有Server对象链表的头节点字段须要说明一下。该字段包括两个类容:第一,服务器链表的头节点指针,用来记录服务器对象列表。第二,记录链表的长度,即服务器节点的数量。具体以下图所示:
还须要定义服务器对象,此服务器对象只是便于客户端而用于表示真是的服务器。客户端的服务器列表中就是此对象。具体结构以下:
1 /* 定义被访问TCP服务器对象类型 */ 2 typedef struct AccessedTCPServerType{ 3 union { 4 uint32_t ipNumber; 5 uint8_t ipSegment[4]; 6 }ipAddress; //服务器的IP地址 7 uint32_t flagPresetServer; //写服务器请求标志 8 WritedCoilListHeadNode pWritedCoilHeadNode; //可写的线圈量列表 9 WritedRegisterListHeadNode pWritedRegisterHeadNode; //可写的保持寄存器列表 10 struct AccessedTCPServerType *pNextNode; //下一个TCP服务器节点 11 }TCPAccessedServerType;
关于服务器对象有三个字段须要说明一下。首先咱们来看一看“读命令列表(uint8_t (*pReadCommand)[12])”字段,它是12个字节,这是由Modbus TCP消息格式决定的。以下:
咱们看到协议标识符为0,是由于0就表示Modbus TCP。还有可写的线圈量列表头节点和可写的保持寄存器列表头节点。这两个字段用来表示对线圈和保持寄存器的列表即数量。
3.2、实例化对象
咱们定义了客户端即服务器对象类型,咱们在使用时就须要实例化这些对象。通常来讲一个IP网段咱们将其实例化为一个客户端对象。
TCPLocalClientType hgraClient;
/*初始化TCP客户端对象*/
InitializeTCPClientObject(&hgraClient,2,hgraServer,NULL,NULL,NULL,NULL);
而一个客户端对象会管理1到253个服务器对象,因此咱们能够将多个服务器对象实例组成数组,并将其赋予客户端管理。
TCPAccessedServerType hgraServer[]={{{192,168,0,1},0x00,0x00},{{192,168,1,1},0x00,0x00}};
因此,根据客户端和服务器实例化的条件,咱们须要先实例化服务器对象才能完整实例化客户端对象。在客户端的初始化中,咱们这里将4的数据处理函数指针初始化为NULL,有一个默认的处理函数会复制给它,该函数是上一版本的延续,在简单应用时简化操做。服务器的上一个发送的命令指针也被赋值为NULL,由于初始时尚未命令发送。
3.3、读服务器操做
读服务器操做原理上与之前的版本是同样的。按照必定的顺序给服务器发送命令再对收到的消息进行解析。咱们对客户端及其所管理的服务器进行了定义,将发送命令保存于服务器对象,将服务器列表保存于客户端对象,因此咱们须要对解析函数进行修改。
1 /*解析收到的服务器相应信息*/ 2 void ParsingServerRespondMessage(TCPLocalClientType *client,uint8_t *recievedMessage) 3 { 4 /*判断接收到的信息是否有相应的命令*/ 5 int cmdIndex=FindCommandForRecievedMessage(client,recievedMessage); 6 7 if((cmdIndex<0)) //没有对应的请求命令,事务号不相符 8 { 9 return; 10 } 11 12 if((recievedMessage[2]!=0x00)||(recievedMessage[3]!=0x00)) //不是Modbus TCP协议 13 { 14 return; 15 } 16 17 if(recievedMessage[7]>0x04) //功能码大于0x04则不是读命令返回 18 { 19 return; 20 } 21 22 uint16_t mLength=(recievedMessage[4]<<8)+recievedMessage[4]; 23 uint16_t dLength=(uint16_t)recievedMessage[8]; 24 if(mLength!=dLength+3) //数据长度不一致 25 { 26 return; 27 } 28 29 FunctionCode fuctionCode=(FunctionCode)recievedMessage[7]; 30 31 if(fuctionCode!=client->pReadCommand[cmdIndex][7]) 32 { 33 return; 34 } 35 36 uint16_t startAddress=(uint16_t)client->pReadCommand[cmdIndex][8]; 37 startAddress=(startAddress<<8)+(uint16_t)client->pReadCommand[cmdIndex][9]; 38 uint16_t quantity=(uint16_t)client->pReadCommand[cmdIndex][10]; 39 quantity=(quantity<<8)+(uint16_t)client->pReadCommand[cmdIndex][11]; 40 41 if(quantity*2!=dLength) //请求的数据长度与返回的数据长度不一致 42 { 43 return; 44 } 45 46 if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister)) 47 { 48 HandleServerRespond[fuctionCode-1](client,recievedMessage,startAddress,quantity); 49 } 50 }
解析函数的主要部分是在检查接收到的消息是不是合法的Modbus TCP消息。检查没问题则调用协议站解析。而最后调用的数据处理函数则是咱们须要在具体应用中编写。在前面客户端初始化时,回调函数咱们初始化为NULL,实际在协议占中有弱化的函数定义,须要针对具体的寄存器和变量地址实现操做。
3.4、写服务器操做
写服务器操做则是在其它进程请求后,咱们标识须要写的对象再统一处理。对具体哪一个服务器的写标识存于客户端实例。而该服务器的哪些变量须要写则记录在服务器实例中。
因此在进程检测到须要写一个服务器时则置位对应的位,即改变flagWriteServer中的对应位。而须要写该服务器的哪些变量则标记flagPresetCoil和flagPresetReg的对应位。修改这些标识都在其它请求更改的进程中实现,而具体的写操做则在本客户端进程中,检测到标志位的变化统一执行。
这部分不修改协议栈的代码,由于各服务器及各变量都只与具体对象相关联,因此在具体的应用中修改。
4、回归验证
借鉴前面Modbus ASCII和Modbus RTU的回归测试经验,咱们设计两个网段、每网段包括一个客户端及两个服务器的网络结构。但考虑到咱们只是功能性验证,因此咱们设计相对简单的服务器。因此咱们设计的网络为:协议栈创建2个客户端,每一个客户端管理同一网段的2个服务器,每一个服务器有8个线圈及2个保持寄存器。具体结构如图:
从上图咱们知道,该Modbus网关须要实现一个Modbus服务器用于和上位的通信;须要实现两个Modbus客户端用于和下位的通信。
在这个实验中,读操做没有什么须要说的,只须要发送命令解析返回消息便可。因此咱们中点描述一下为了方便操做,在须要写的连续段,咱们只要找到第一个请求写的位置后,就将后续连续可写数据一次性写入。
告之:源代码可上Github下载:https://github.com/foxclever/Modbus
欢迎关注: