虚拟设备实现Modbus串行链路与Modbus/TCPIP的互连
【摘要】Modbus作为第一个用于工业现场的总线协议,应用极其广泛,他包括基于串口的Modbus协议和基于以太网的Modbus/TCPIP协议。文章描述了通过虚拟设备实现了这两种通讯方式的互联并给出了实现细节,提出了虚拟技术具有广泛的应用领域。
【关键词】虚拟串口;TDI驱动;Modbus
Virtual device make the connection between Modbus serial port and Modbus/TCPIP
AbstractModbus is applied widely as the first bus treaty used in industry.It includes Modbus treaty based on serial port and Modbus/TCP treaty based on ethernet network. The essay realizes the connection of the two kinds of communication and give the realization in details, put forward that the technology of virtual will be used widely
Key words:virtual serial portwaitforsingleobject函数TDI driveModbus
引言
近年来,工业现场总线与工业以太网发展迅速,他们之间的之间的竞争也十分激烈。Modbus/TCP以太网协议由Schneider公司发布,是将Modbus现场总线协议与以太网TCP/IP协议结合而成,使得信息从一个网络传输到另一个网络而不需改变通讯协议成为了可能。IANA委员会给施耐德电气公司分配了已为大家熟知的TCP 502端口,以专为Modbus协议保留,可见,Modbus/TCPIP协议现在已经成为Internet标准。
虚拟技术就是把物理资源转变为逻辑上可以管理的资源,以打破物理结构之间的壁垒,Modbus/TCPIP以太网协议是以网卡为对外接口,Modbus现场总线协议是以串口与外界通讯,由于硬件的限制,双方是无法通讯的。但通过虚拟技术,使得他们之间的通讯成为了可能,他的好处是不需要通过添加网关这样的硬件。本文通过生成一个虚拟串口设备实现他们之间的互联。
1、虚拟设备实现互连原理
虚拟设备是如何实现Modbus串行链路与Modbus/TCPIP互联的呢,它采用的是欺上瞒下的手段,对上层应用程序,注册成一个标准的串口设备,那么只要用户打开这个虚拟设备,就可以发送所有操作串口的API过来,在收到这些请求之后,因为并非去操作真实串口设备,
所以对大部分请求直接返回成功即可。但其中有些请求却是需要去处理的,如创建、读写这些请求,以及串口操作控制码中的超时设置等。通过I/O的写请求包获取到应用程序发往串口的数据,虚拟设备做的工作就是把数据发往真实的网卡。它的实现是通过先打开协议驱动设备(TCP/IP协议驱动,也称为TDI传输器),通过TDI驱动接口构建I/O请求包(IRP)然后发给协议驱动,协议驱动负责为发送的网络数据添加协议头(TCP/IP协议的TCP头、IP),最后把封装好的IP包传给NIC设备驱动进行发送。Modbus协议的网络端在收到数据后进行解析,最终获得Modbus串口端发送的数据。在处理读请求时与写请求类似,获得网络数据后,把数据拷入串口读请求包中后返回。另外,对于Modbus这种主从协议需要处理超时,如在网络上移除从站后,在规定时间内没有从站应答返回,主站应取消对其的读请求,让读请求以失败返回。所以在处理超时请求时,保存这个超时时间,在读请求中启动定时器,在时间到时进行读操作,如果没有数据到来则失败返回。
2、程序实现
(1)在正式创建虚拟设备之前,需要准备一些工具,就像应用程序使用开发包SDK一样,内核编程使用”Windows Drive Kit”,简称WDKWDK自带所有需要的头文件、库、C/C++语言及
汇编语言的编译器与连接器。另外需安装一个虚拟机,因为在本机上直接加载一个内核模块是非常不明智的。如果模块中有错误,很容易导致操作系统蓝屏,这是工作文件可能还没有保存,导致代码丢失。
(2)首先要解决的是Modbus串行应用程序如何与虚拟串口设备交互的问题,先通过IoCreateDevice函数创建一个设备,每个驱动设备都有一个应用程序可访问的符号链接名(也称为Dos设备名)和其他驱动程序可访问的设备对象名,对于串口设备有一个核态使用的设备名,形如”\Device\Serial(n)”(n对应串口号),对于符号链接名,可通过函数IoCreateSymbolicLink创建一个名为”\DosDevices\COMn”(n对应串口号)的设备,这样应用程序就可以通过CreateFile函数对这个串口设备进行打开了。
(3)接下来是虚拟串口设备与TCP/IP协议驱动进行交互,是通过协议驱动所创建的设备对象名字”\Device\Tcp””\Device\Udp”,因为本例中是与Modbus/TCPIP交互,所以应绑定”\Device\Tcp”设备名。通过这个名字调用内核函数ZwCreateFile对其进行打开操作。对任何设备的打开都可以获得一个文件对象,这个对象表述的是对每个设备的一次打开,其中会保存一些用于这次打开以及之后使用的相关信息。对”\Device\Tcp”设备第一次打开操作时
通过对pEa结构的EaName域赋值TdiTransportAddress,返回一个代表传输地址的文件对象,第二次打开操作通过对pEa结构的EaName域赋值TdiConnectionContext,返回一个代表连接端点的文件对象(如果是面向无连接的UDP操作,则不需要这个对象),通过向下层提交 TDI_ASSOCIATIOM_ADDRESS IOCTL码的方式在已打开的连接和已打开的传输地址之间建立联系。接下来就可以进行连接操作(客户端)或者等待接受对方的连接(服务端)
(4)当作为Modbus主站(客户端)时,需通过TDI函数向对方发起一个连接,首先需要知道对方的远程IP地址和端口号,因为Modbus端口号已经被固定分配,所以这里端口号可以采用硬编码的方式,为了动态获得远程IP地址,可以在应用程序中将其写入注册表,在此时读出即可。一般来说,在对TDI传输器进行任何操作之前,都需要通过TdiBuildInternalDeviceControlIrp函数创建一个IRP(I/O请求包),把操作类型参数设置成需要操作的类型,如连接操作则设置为TDI_CONNECT值。接下来把从注册表读出的远程IP地址和端口号赋值给TDI_CONNECTION_INFORMATION结构中的TA_IP_ADDRESS中的对应变量,在TdiBuildConnect函数中代入之前需创建好的IRPTDI_CONNECTION_INFORMATION结构,把IRP设置成连接请求的IRP。最终通过IoCallDriver函数将其下发到底层设备。这个连接IRP绑定了一个事件,如果返回状态为pendi
ng值,则调用KeWaitForSingleObject函数在这个绑定的事件上等待,直到连接IRP完成,事件置位,此时判断IoStatus.Status的值,如为零则表示连接成功。
PIRP pIrp = TdiBuildInternalDeviceControlIrp (TDI_CONNECT, pTcpDevObj, pConnFileObj, &Event, &IoStatus);
if (NULL==pIrp)
{status = STATUS_INSUFFICIENT_RESOURCES; break;}
TA_IP_ADDRESSRmtIPAddr={1,
{TDI_ADDRESS_LENGTH_IP, TDI_ADDRESS_TYPE_IP,{RmtPort, RmtAddr}}}
TDI_CONNECTION_INFORMATION
RmtNode = {0, 0, 0, 0, sizeof(RmtIPAddr), &RmtIPAddr};
TdiBuildConnect(pIrp, pTcpDevObj,
pConnFileObj, NULL, NULL, NULL, &RmtNode, 0) ;
status = IoCallDriver(pTcpDevObj, pIrp);
if (STATUS_PENDING==status)
KeWaitForSingleObject(&Event, Executive,
KernelMode, FALSE, 0);
对于读请求可以有两种方式处理,一种是当串口读请求到来时先返回pending值,并通过TDI函数创建一个操作类型值为TDI_RECEIVEIRP进行下发。另一种方式是申请一个环状缓冲区,向TDI传输器注册一个面向端端连接的读回调例程,当有数据到达传输层时,传输层将调用注册的回调函数,参数中携带收到数据缓冲区的指针与长度,在回调函数把数据拷贝到我们已经申请号的环状缓冲区中,当串口读请求到来时只需在定时器时间到时查看环状缓冲区中是否有可读数据,如有则填充完成IRP返回,如缓冲区为空则返回失败。对于写请求与断开连接操作都与连接请求的处理方式类似。
(5)当作为Modbus从站(服务端)时,需监听到来的连接并判断是否要与其进行端端连接,为了监听连接需注册一个连接回调函数。当下层驱动调用时,从参数中取得对方的地址信息,其中有两个参数是需要填充返回的,一个是连接上下文信息(CONNECTION_CONTEXT *ppConnCtx),一个是接收IRP指针(PIRP *ppIrp),从回调函数参数中取得对方的地址信息为CONNECTION_CONTEXT结构赋值,然后调用TdiBuildAccept函数,其参数包括一个已创建好的用于处理接受连接请求的IRP,一个已创建并绑定到已打开的传输地址文件对象上的连接端点文件对象,接受连接请求IRP的完成回调函数,一个包含对方地址信息的TDI_CONNECTION_INFORMATION结构。之后设置好连接请求IRP的下层堆栈就可以返回给下层驱动去处理了。如接收IRP返回成功,说明端端连接已建立,就可以在这个连接端点的文件对象上进行收发数据了。连接回调函数实现代码如下:
NTSTATUSTDIClnEventConnect
( PVOID pEventCtx, LONG lnRmtAddr, PVOID
pRmtClnTA, LONG lnUserData, PVOID
pUserData, LONG lnOptions, PVOID pOptions,
CONNECTION_CONTEXT * ppConnCtx,
PIRP * ppIrp)
{pTDIClnConn
pLclConn=(pTDIClnConn)pEventCtx;
pTDIClientExtensionpDevExt=
pLclConn->pDevExt;
PTDI_CONNECTION_INFORMATIONpClientConn = pLclConn->pClientConnInfo;
PTA_IP_ADDRESSpClientConnInfo=