实现Modbus TCP多网段客户端应用
对于Modbus TCP来说与Modbus RTU和Modbus ASCII有比较大的区别,因为它是运行于以太网链路之上,是运行于TCP/IP协议之上的一种应用层协议。在协议栈的前两个版本中,Modbus TCP作为客户端时也存在一些局限性。我们将对这些不足作一定更新。
1、存在的不足
在原有的协议栈中,我们所封装的Modbus TCP客户端一个特定的客户端,即它只是一个客户端实例。在通常的应用中不会有什么问题,但在有些应用场合就会显现出它的局限性。
首先,作为一个特定的客户端,若是连接多个服务器目标时,修改服务器参数值的处理变的非常复杂,需要分辨是不同的服务器,不同的变量。当需要从不同的网段操作数据时,我们甚至需要标记不同的网段。
其次,作为一个特定的客户端,如果我们操作的服务器参数相似时,哪怕来自于不同的网段,我们也需要仔细分辨或者传递额外的参数。因为同一客户端的解析函数是同一个。
最后,将多个Modbus TCP服务器通讯都作为唯一的一个特定的服务器来处理,使得各部分混杂在一起,程序结构很不清晰,对象也不明确。
2、更新设计
考虑到前述的局限性,我们将Modbus TCP客户端及其所访问的Modbus TCP服务器定义为通用的对象,而当我们在具体应用中使用时,再将其特例化为特定的客户端和服务器对象。
首先我们来考虑客户端,原则上我们规划的每一个客户端对象管理我们设备上的一个IP网段的设备。那么在一个特定客户端下,我们可以定义多达253个不同的服务器。如下图所示:
从上图中我们可以发现,我们的目的就是让协议栈支持,多客户端和多服务器,并且在不同客户端下可以访问同网段的多个服务器。接下来我们还需要考虑服务器对象。客户端对服务器的操作无非两类:读服务器信息和写服务器信息。
对于读服务器信息来说,客户端需要发送请求命令,等待服务器返回响应信息,然后客户端解析收到的信息并更新对应的参数值。因为返回的响应消息是没有对应的寄存器地址的,所以要想在解析的时候定位寄存器就必须知道发送的命令,为了便于分辨我们将命令存放在服务器对象中。
而对于写服务器操作,无论写的要求来自于哪里,对于协议栈来说肯定是其它的数据处理进程发过来的,所接到要求后我们需要记录是哪一个客户端管理的哪一个服务器的哪些参数。对于客户端我们不需要分辨,因为每个客户端都是独立的处理进程,但是对于服务器和参数我们就需要分辨。每一个客户端所管理的IP地址的最后一段为0到255,所以我们可以依据来分辨服务器端。而在每一个服务器节点中增加状态标志,用以记录请求状态,而所有服务器端组成链表。
3、编码实现
我们已经设计了我们的更新,接下来我们就根据这一设计来实现它。我们主要从以下几个方面来操作:第一,实现客户端对象类型和服务器对象类型;第二,客户端对象的实例化及服务器对象的实例化;第三,读服务器参数的客户端操作过程;第四,写服务器参的数客户端操作过程。接下来我们将一一描述之。
3.1、定义对象类型
与在Modbus RTU和Modbus ASCII一样,在Modbus TCP协议栈的封装中,我们也需要定义客户端对象和服务器对象,自然也免不了要定义这两种类型。
首先我们来定义本地客户端的类型,其成员包括:一个uint32_t的写服务器标志数组;服务器数量字段;服务器顺序字段;本客户端所管理的服务器列表;4个数据更新函数指针。具体定义如下:
/* 定义本地TCP客户端对象类型 */
typedef struct LocalTCPClientType{
uint32_t transaction; //事务标识符
uint16_t cmdNumber; //读服务器命令的数量
uint16_t cmdOrder; //当前从站在从站列表中的位置
uint8_t (*pReadCommand)[]; //读命令列表
ServerListHeadNode ServerHeadNode; //Server对象链表的头节点
UpdateCoilStatusType pUpdateCoilStatus; //更新线圈量函数
UpdateInputStatusType pUpdateInputStatus; //更新输入状态量函数
UpdateHoldingRegisterType pUpdateHoldingRegister; //更新保持寄存器量函数
UpdateInputResgisterType pUpdateInputResgister; //更新输入寄存器量函数
}TCPLocalClientType;
关于客户端对象类型,在前面的更新设计中已经讲的很清楚了,只有Server对象链表的头节点字段需要说明一下。该字段包括两个类容:第一,服务器链表的头节点指针,用来记录服务器对象列表。第二,记录链表的长度,即服务器节点的数量。具体如下图所示:
还需要定义服务器对象,此服务器对象只是便于客户端而用于表示真是的服务器。客户端的服务器列表中就是此对象。具体结构如下:
/* 定义被访问TCP服务器对象类型 */
typedef struct AccessedTCPServerType{
union {
uint32_t ipNumber;
uint8_t ipSegment[];
}ipAddress; //服务器的IP地址
uint32_t flagPresetServer; //写服务器请求标志
WritedCoilListHeadNode pWritedCoilHeadNode; //可写的线圈量列表
WritedRegisterListHeadNode pWritedRegisterHeadNode; //可写的保持寄存器列表
struct AccessedTCPServerType *pNextNode; //下一个TCP服务器节点
}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、读服务器操作
读服务器操作原理上与以前的版本是一样的。按照一定的顺序给服务器发送命令再对收到的消息进行解析。我们对客户端及其所管理的服务器进行了定义,将发送命令保存于服务器对象,将服务器列表保存于客户端对象,所以我们需要对解析函数进行修改。
/*解析收到的服务器相应信息*/
void ParsingServerRespondMessage(TCPLocalClientType *client,uint8_t *recievedMessage)
{
/*判断接收到的信息是否有相应的命令*/
int cmdIndex=FindCommandForRecievedMessage(client,recievedMessage); if((cmdIndex<)) //没有对应的请求命令,事务号不相符
{
return;
} if((recievedMessage[]!=0x00)||(recievedMessage[]!=0x00)) //不是Modbus TCP协议
{
return;
} if(recievedMessage[]>0x04) //功能码大于0x04则不是读命令返回
{
return;
} uint16_t mLength=(recievedMessage[]<<)+recievedMessage[];
uint16_t dLength=(uint16_t)recievedMessage[];
if(mLength!=dLength+) //数据长度不一致
{
return;
} FunctionCode fuctionCode=(FunctionCode)recievedMessage[]; if(fuctionCode!=client->pReadCommand[cmdIndex][])
{
return;
} uint16_t startAddress=(uint16_t)client->pReadCommand[cmdIndex][];
startAddress=(startAddress<<)+(uint16_t)client->pReadCommand[cmdIndex][];
uint16_t quantity=(uint16_t)client->pReadCommand[cmdIndex][];
quantity=(quantity<<)+(uint16_t)client->pReadCommand[cmdIndex][]; if(quantity*!=dLength) //请求的数据长度与返回的数据长度不一致
{
return;
} if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister))
{
HandleServerRespond[fuctionCode-](client,recievedMessage,startAddress,quantity);
}
}
解析函数的主要部分是在检查接收到的消息是否是合法的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
欢迎关注:
实现Modbus TCP多网段客户端应用的更多相关文章
- 初识Modbus TCP/IP-------------C#编写Modbus TCP客户端程序(一)
转自:http://blog.csdn.net/thebestleo/article/details/52269999 首先我要说明一下,本人新手一枚,本文仅为同样热爱学习的同学提供参考,有不 对的地 ...
- 初识Modbus TCP/IP-------------C#编写Modbus TCP客户端程序(二)
由于感觉上一次写的篇幅过长,所以新开一贴,继续介绍Modbus TCP/IP的初步认识, 书接上回 3).03(0x03)功能码--------读保持寄存器 请求与响应格式 这是一个请求读寄存器108 ...
- freemodbus modbus TCP 学习笔记
1.前言 使用modbus有些时间了,期间使用过modbus RTU也使用过modbus TCP,通过博文和大家分享一些MODBUS TCP的东西.在嵌入式中实现TCP就需要借助一个以太网协议 ...
- 基于STM32和W5500的Modbus TCP通讯
在最近的一个项目中需要实现Modbus TCP通讯,而选用的硬件平台则是STM32F103和W5500,软件平台则选用IAR EWAR6.4来实现. 1.移植千的准备工作 为了实现Modbus TCP ...
- 开放型Modbus/TCP 规范
修订版 1.0,1999 年3 月29 日Andy SwalesSchneider 电气公司aswales@modicon.com目录目录............................... ...
- LwIP之socket应用--WebServer和Modbus TCP
1. 引言 LwIP是嵌入式领域一个流行的以太网协议栈, LwIP开放源码,用C写成非常方便移植,并且支持socket接口,使用者可以集中精力处理应用功能. 本文就是LwIP socket使用的一个小 ...
- Modbus库开发笔记之四:Modbus TCP Client开发
这一次我们封装Modbus TCP Client应用.同样的我们也不是做具体的应用,而是实现TCP客户端的基本功能.我们将TCP客户端的功能封装为函数,以便在开发具体应用时调用. 对于TCP客户端我们 ...
- Modbus库开发笔记之三:Modbus TCP Server开发
在完成了前面的工作后,我们就可以实现有针对性的应用了,首先我们来实现Modbus TCP的服务器端应用.当然我们不是做具体的应用,而是对Modbus TCP的服务器端应用进行封装以供有需要时调用. 这 ...
- C# ModBus Tcp读写数据 与服务器进行通讯
前言 本文将使用一个NuGet公开的组件技术来实现一个ModBus TCP的客户端,方便的对Modbus tcp的服务器进行读写,这个服务器可以是电脑端C#设计的,也可以是PLC实现的,也可以是其他任 ...
随机推荐
- netty: 将传递数据格式转为String,并使用分隔符发送多条数据
自定义分割符,用:DelimiterBasedFrameDecoder类 ByteBuf转String,用StringDecoder类 参考代码: //设置连接符/分隔符,换行显示 ByteBuf b ...
- Dobbox
一.向本地仓库导入Dubbox依赖 1.1解压压缩包 1.2打开cmd窗口切到源码包路径 1.3输入命令行 1.4成功后展示如图 1.5输入命令行 1.6成功后如图 public class DoSo ...
- Linux Shell 如何获取参数
$# 是传给脚本的参数个数 $0 是脚本本身的名字 $1 是传递给该shell脚本的第一个参数 $2 是传递给该shell脚本的第二个参数 $@ 是传给脚本的所有参数的列表 $* 是以一个单字符串显示 ...
- C# 打开 EXE 文件
命名空间是using System.Diagnostics; 在编写程序时经常会使用到调用可执行程序的情况,本文将简单介绍C#调用exe的方法.在C#中,通过Process类来进行进程操作. Proc ...
- (尚011)Vue事件处理
test011.html <!DOCTYPE html><html lang="en"><head> <meta charset=&quo ...
- Qt智能指针QPointer, QSharedDataPointer ,QSharedPointer,QWeakPointer和QScopedPointer
QPointer (4.0) 已经过时,可以被QWeakPointer所替代,它不是线程安全的. QSharedDataPointer (4.0) -- 提供对数据的COPY-ON-WRITE以及浅拷 ...
- aix 10代oracle zabbix2.4.4 日志监控
同一类型的监控项,zabbix 2.4的客户端也支持日志监控,可是在参数个数上有问题,如果把所有参数都放满,监控项会提示too mant parameters,无法 生效取数据, 对于不同的正则式.m ...
- 码农,就要做有范儿的geek【网摘】
摘要: “我是个geek,圈子里都称呼我为xx,我周一到周五亲自写写代码,指导指导新人,周末参加圈子的活动,跟别人分享一下我的经验.至于未来嘛,还没想过,反正自己技术水平在这了,呵呵,扎克伯格当年不也 ...
- 2017.10.4 国庆清北 D4T1 财富
(其实这题是luogu P1901 发射站 原题,而且数据范围还比luogu小) 题目描述 LYK有n个小伙伴.每个小伙伴有一个身高hi. 这个游戏是这样的,LYK生活的环境是以身高为美的环境,因此在 ...
- Flask一种通用视图,增删改查RESTful API的设计
模型设计是后端开发的第一步.数据模型反映了各种对象之间的相互关系. from app import db class Role(db.Model): """角色" ...