倍福ADS协议讲解及 Notification模式通讯应用案例

ADS设备概念

先看几个关键术语的缩写以明白他们的来由,方便后续理解

  • TwinCAT(The Windows Control and Automation Technology)
  • ADS(Automation Device Specification)
  • AMS(Automation Message Specification)

在TwinCAT系统的架构模型中,独立的软件模块(例如TwinCAT PLC,TwinCAT HMI,TwinCAT Scope)是被视为一个个相互独立的硬件设备,对于每一个Task来说,它都至少拥有一个软件模块(这个软件模块可以是“服务器”也可以是“客户端”,Task的使命就是运行这个软件模块。从现实世界来看,可以把Task 1当作一个操作员A,操作员A的职责就是运行一个硬件PLC实体,Task 2就对应操作员B,操作员B的职责就是运行一个硬件示波器实体【TwinCAT Scope】),从软件角度来看,此系统中所说的“服务器”是用来执行工作任务的设备,它的这种行为模式很像硬件设备的行为,基于这个原因,我们可以用一套软件来实现一个硬件设备的功能,在概念上把它称为“虚拟”硬件设备(译注:例如我们可以在电脑上编写一套示波器程序,通过USB接口采集数据,在电脑屏幕上绘出波形,这个时候,我们所开发的这套示波器程序它的作用和一个实物示波器是一样的,但是它是“虚拟的”,它是依赖电脑硬件的,在同样的电脑硬件上,我们还可以开发一个PLC程序,这个PLC程序可以运行梯形图等PLC编程语言执行逻辑操作,它的行为就和一个硬件PLC一样,但它也是虚拟的,它的存在和运行也依赖于电脑硬件,通过分时共享硬件,或者分配不同的物理核心来执行上述的两套程序,从功能上而言,我们就得到了一台PLC和一台示波器)。所谓的“客户端”也是一套程序,这套程序用来向“服务器”请求数据或服务(译注:例如一个HMI可以向一个PLC请求它的IO数据以显示出来,这里的HMI就是客户端,PLC就是服务器)

因此,在TwinCAT系统中,“服务器”与“服务器”之间,“服务器”与“客户端”之间的数据交互都是通过一个一致的ADS接口来实现(译注:这里所说的服务器和客户端都是运行在同一个PC上的不同软件模块,这些软件模块都拥有一个规则一致的数据交互接口,这个接口就是ADS接口,在软件层面上而言,本质就是一个“中间层”,软件术语也叫中间件)

每一个软件模块都有自己的一个ADS接口,各个软件模块的ADS接口通过ADS router来交换信息(这种架构犹如多台PC,每个PC都有网口,各个PC的网口通过连到同一个交换机上来彼此交互数据)

每一个TwinCAT设备都有一个TwinCAT message router,通过这个router,TwinCAT系统中的“server”和“client”可以交换数据,下图展示的是从ADS的角度来剖析一个TwinCAT设备的图解。Multi-core CPU这个大灰色框就是一个TwinCAT device,其中的Core1里面运行了User HMI软件模块且提供了ADS接口,它就是一个ADS device。
在这里插入图片描述

思考问题

TwinCAT device和ADS device的区别

装有TwinCAT runtime运行时的PC或者装有TwinCAT Engineering开发环境的PC(本质也是安装有runtime运行时)被视为TwinCAT device;具备了ADS接口的软件模块被称为ADS device。

ADS device中有ADS router吗

没有,ADS device是具备ADS接口规范的软件模块,它不需要维护ADS router,ADS router的维护是由TwinCAT device负责的,这就类似于PC本身是不需要维护路由表的,路由表的维护由路由器本身负责,PC只需要具备和能和路由器交互数据的接口即可,这个接口规范是双方都认可且遵循的。

ADS设备识别

ADS设备的标识符只有两个,通过这两个标识来表示一个唯一的ADS设备,类似ADS device的身份证

  • PortNr
  • NetId

其中,PortNr是预定义好的,在同一个TwinCAT device中,第一个PLC对应的PortNr是851,第二个PLC对应的PortNr是852,一个TCP/IP server对应的PortNr是10201,一个Database server对应的PortNr是21372…针对一个TwinCAT device而言,NetId就是指这个TwinCAT device的AMS NetId,所有在这个TwinCAT device中新建出来的ADS device都共享这一个NetId,例如上面举例说明的两个PLC的情形,把他们视为ADS device的时候,他们的属性就是这样的

PLC1:PortNr=851,NetId=5.19.218.60.1.1;

PLC2:PortNr=852,NetId=5.19.218.60.1.1;

在这里插入图片描述

上图是使用编程电脑连接倍福CX5020控制器,并在项目中新建两个PLC时的PortNr分配情况,CX5020是一个TwinCAT device,PLC1和PLC2是一个ADS device

下图展示了CX5020控制器的AMS NetId=5.19.218.60.1.1

在这里插入图片描述

有些IO模块是可以通过ADS协议进行访问的,例如EL6751 CANopen Master模块,这个模块是可以通过AoE(ADS over EtherCAT)进行访问的,那么它就是一个ADS设备,因为它实现了ADS接口,基于此,该模块必定有PortNr和NetId两个属性,下图列出了一个实际的例子,如何查看具备ADS接口的IO模块的PortNr和NetId

在这里插入图片描述

上图示出了EL6751模块的ADS身份信息,它的PortNr是1012,NetId是5.19.218.60.2.1,当需要使用ADS的功能块对EL6751进行操作的时候,就需要这两个信息进行定位.

思考问题

任何ADS device的PortNr都是固定的吗

不是,ADS device的PortNr概念是和计算机中的应用程序的Port类似,鉴于ADS device的概念最初是为了定义符合自动化规范的软件模块而引入,所以有必要从计算机的视角上对其进行审视,因为倍福开发的是基于PC的控制器,所以避免不了要考虑和PC的兼容以及一些PC术语的借鉴,在计算机的体系中,每一个应用程序(application program)为了和外界(一般使用互联网,采用TCP/IP协议群)进行信息交互,都会由系统分配一个端口号来作为唯一的信息交换口,而且这个端口号相对于同一台PC上的其他程序而言是唯一的,这样从计算机网口进来的数据,可以根据报文中的端口号准确地送达到对应的app上。

在计算机中,操作系统所提供的一些基础服务(本质上都是应用程序,这一点Linux就做的非常好,Linux的系统架构很明晰,方便学习,Windows因为是封闭系统,这部分不够透明)的端口号一般都是预定义的,他们分配在0-1023之间(总的可用端口号范围是0-65535),例如 HTTP服务器端口号是80,FTP服务器端口号是21…而用户的应用程序分配的端口号多在49152~65535之间,在1024-49151之间的端口号也被注册了一些,这个范围内的端口号并不是严格不能被用户程序使用,例如ADS用到的端口号就是48898(用于TCP连接传输数据,例如开发电脑下载PLC程序到控制器上)和48899(用于UDP广播发现设备例如scan操作)

Scenario: ADS connection through a firewall 1:

ADS device也是一个个的application program,从这个角度上看,为了交互数据,他们也必须具备唯一的端口号以供ADS router这个server来准确把数据传递给自己,所以衍生出了PortNr的概念,只不过像TwinCAT PLC和TwinCAT NC这样的基础服务程序一般都固定分配了对应的端口号,而像TwinCAT HMI这种用户自己实现的client可能有好几个,他们的端口就需要动态地申请,就没办法规定一个确定的端口号(但是TwinCAT HMI server的端口号一般都是固定的,例如19800).

总之,TwinCAT HMI client是ADS device但是它们的端口号并不固定,这和你在Windows上运行一个特定的程序,这个程序申请了一个特定的端口号用来通讯是一样的道理。

AMS NetId和控制器的IP地址有关系吗

没有必然的联系,只不过一般AMS(Automation Message Specification自动化设备规范)NetId都是在IP地址之后加了.1.1两个后缀,所以类比IP地址来看,AMSNetId是一个网络层当中的概念。对于一个运行有TwinCAT runtime的控制器而言,AMS NetId是对于ADS router而言的,因为在一个使用ADS router做数据交换的网络中,所有的ADS device并不一定都在控制器上以软件模块的形式存在,也可能需要两个控制器当中的app进行信息交换,例如控制器A中的PLC1将采集上来的传感器数据简单运算后发给控制器B中的PLC1进行更复杂的运算然后发给控制器B的NC模块进行axis1轴的运动控制,那么控制器A和控制器B的数据交换使用ADS router进行,这个时候就需要控制器A和控制器B具备不同的AMS NetId来作为网络层上的分辨标识,它的作用就类似于IP地址的作用,IP地址也是网络层上区分不同PC的一个身份标识。

更改AMS NetId并不影响IP,同样地更改IP也不影响AMS NetId,因为他们二者所服务的对象是不同的,AMS NetId是给ADS router这个server使用的,而IP是给TCP/IP server使用的。

在计算机的世界中,IP是用来在网络层上区分计算机的一个身份标识,而port是用来区分同一计算机上不同进程的标识(简单理解进程就是应用程序,但是一个应用程序可能有多个进程,分别掌管不同的功能),在TwinCAT的体系中,AMSNetId是用来区分不同的TwinCAT device,PortNr是用来区分同一个TwinCAT device上不同的ADS device(ADS device扮演的角色类似于进程)。

ADS设备访问

对于用户而言,及时取得所需的数据是最重要的,这里的及时意味着不能耽误生产的节拍,在自动化的行业里面,产线是有严格的生产节拍的,每一个处理步骤必须在规定的时间内完成,既不堵塞上游,也不耽误下游,各个站按部就班,各司其职保证生产顺利连贯。

ADS设备支持三种类型的访问方法,异步访问,通知,同步访问。

异步访问

异步访问的流程如下

1、ADS client向ADS server请求数据

2、在请求的数据未返回给client之前,client继续自己的工作

3、server处理请求后以call-back函数的形式返回给client

call-back即回调函数,何为回调函数?client调用server的API叫做call,server把处理结果通过调用client的API返回给client叫做call-back。举个简单的例子帮助理解,你家里有事需要向领导请假,你有领导的电话,这是领导告诉你的,于是你打电话给领导,说“领导你好,我是小王,今年5月1日家里拆迁了,需要我回家数钱,因此希望在5月1日请假1天,望领导批准”,领导正在忙其他的事情,说我稍后回你,于是你挂断了电话,领导有你的电话,这是你告诉领导的,过了一会儿领导给你打电话说“祝贺你,小王,记得买台点钞机,不要累坏身体,5月2日准时上班”,你听到以后很高兴,5月1日回家点钞去了…在这个过程中,领导就是“服务器”,你就是“客户端”,你通过服务器提供的API(领导的电话号码)向其请求1天的假期,服务器收到你的请求,通过你的API(你的电话号码)把批假1天的消息反馈给你,于是你愉快地去数钱了,领导给你打电话批假这个过程就是回调,即调用你的接口返回你需要的数据,体会一下 call和call-back这个命名的内涵。需要注意的是,领导不一定会忙到什么时候,所以回你电话的时间是不确定的,你不能及时收到领导的反馈,这就是异步的内涵。

通知

通知(Notification)访问的流程如下

1、ADS client在ADS server上注册一条ADS request

2、ADS server通过回调函数的形式不断响应这个请求,直到ADS client取消它的request

通知访问的模式非常类似于软件概念中的“发布/订阅模式”,类似于我们生活中常见的公众号一样,只要我们订阅了公众号,一旦公众号的作者有更新,那我们就能收到新的内容提醒。还以上面的请假为例,一旦公司变大,领导不可能总去接听电话处理事务,于是新上了一套系统,这套系统就是银蝶(哈哈…),你想请领导批假,于是你写了一个请求发送给了领导,领导的银蝶系统会把你的请求弹在他的手机中央,并以红色鲜明标识出来,每一分钟自动亮屏加振动一次…,领导批你假了,同样领导的批假也会发到你的手机上,自动亮屏加振动,直到你点击进去,点击“已阅”按钮,此时这个系统消停了。这样的设计避免了领导忘记批假时,你不断打电话询问他导致他接听不到别的重要电话的情况发生,你不用过多地占用领导的手机(就是API),节省了通话资源。

同步访问

同步访问的流程如下

1、ADS client向ADS server请求数据

2、在请求的数据未返回给client之前,client就一直等待返回数据,不干别的活儿

3、server处理请求后以call-back函数的形式返回给client,client接着继续往下干活

思考问题

如何在非倍福的设备上使用ADS

去倍福的官方网站上下载一个TC1000的安装包,类似下图,安装到第三方的PC上使用即可。


如果开发电脑上安装了TwinCAT XAE的开发环境,那么也会同时安装ADS的库

ADS应用实例

这里我们以安装有TwinCAT XAE开发环境的电脑为例,连接一个倍福的PLC,在此开发电脑上使用C++编写一个控制台应用程序,通过ADS协议以通知的方式读取PLC中的临时变量PLCVar。

下面示出的是C++控制台程序的代码

#include <iostream>
#include <conio.h>
#include < iomanip>
#include <windows.h>
#include <winbase.h>

//包含TwinCAT 提供的库文件
#include "C:\TwinCAT\AdsApi\TcAdsDll\Include\TcAdsDef.h"
#include "C:\TwinCAT\AdsApi\TcAdsDll\Include\TcAdsAPI.h"

//声明一个回调函数,此回调函数用于处理server返回的数据
void _stdcall Callback(AmsAddr*, AdsNotificationHeader*, ULONG);

int main()
{
    long nErr,nPort,LocalnPort;//nErr用来存放错误代码,nPort用来存放ADS server的通讯端口,LocalnPort用来存放本地的ADS通讯端口
    AmsAddr Addr;//用来保存 ADS server的AMS地址信息,该信息包含了NetId和port,是一个结构体
    AmsAddr LocalAddr;//用来保存 ADS client的AMS地址信息,该信息包含了NetId和port,是一个结构体
    PAmsAddr pLocalAddr = &LocalAddr;//指向ADS client的AMS地址信息的指针
    PAmsAddr pAddr = &Addr;
    ULONG hNotification, hUser;//句柄变量
    AdsNotificationAttrib adsNotificationAttrib;
    char szVar[] = { "MAIN.PLCVar" };//ADS server中待读取的变量的名称

    //设定ADS server的地址和端口信息
    Addr.netId.b[0] = 5;
    Addr.netId.b[1] = 19;
    Addr.netId.b[2] = 218;
    Addr.netId.b[3] = 60;
    Addr.netId.b[4] = 1;
    Addr.netId.b[5] = 1;
    Addr.port = 851;

    //向本地的ADS router申请端口,建立TCP连接
    LocalnPort = AdsPortOpen();

    //打印本地ADS client的端口信息
    std::cout << "Local ADS port is:" << LocalnPort << std::endl;
    nErr= AdsGetLocalAddress(pLocalAddr);
    if (nErr) std::cout << "Error:GetLocalAmsAddr:" << nErr << std::endl;

    //打印本地ADS client的NetId信息
    std::cout << "The LocalAMSNetId is:";
    std::cout << (int)pLocalAddr->netId.b[0] << "." << (int)pLocalAddr->netId.b[1] << "." << (int)pLocalAddr->netId.b[2] << "."
        << (int)pLocalAddr->netId.b[3] << "." << (int)pLocalAddr->netId.b[4] << "." << (int)pLocalAddr->netId.b[5] << std::endl;

    //设定通知的参数
    adsNotificationAttrib.cbLength = 1;//client所请求的变量长度为1个byte
    adsNotificationAttrib.nTransMode = ADSTRANS_SERVERONCHA;//传输模式为值变动就传输
    adsNotificationAttrib.nMaxDelay = 0;//回调函数的调用周期,单位为100ns
    adsNotificationAttrib.nCycleTime = 10000000; //变量值检查周期,server每隔1秒检查一次变量值是否有更新

    //获取ADS server中MAIN.PLCVar变量的句柄,这里ADS server实际上就是远程PLC,把获取到的MAIN.PLCVar变量的句柄值存放在hUser中
    nErr = AdsSyncReadWriteReq(pAddr,ADSIGRP_SYM_HNDBYNAME,0x0,sizeof(hUser),&hUser,sizeof(szVar),&szVar);
    std::cout << "hUser is:" << hUser << std::endl;
    if (nErr)
        std::cerr << "Error:AdsSyncReadWriteReq:" << nErr << std::endl;
    
    //向ADS server中添加通知,adsNotificationAttrib是通知的参数,hUser是需要传递给Callback函数的变量句柄,
    nErr = AdsSyncAddDeviceNotificationReq(pAddr,ADSIGRP_SYM_VALBYHND,hUser,&adsNotificationAttrib,Callback,hUser,&hNotification);
    if (nErr)
        std::cerr << "Error:AdsSyncAddDeviceNotificationReq:" << nErr << std::endl;
    std::cout << "Notification:" << hNotification << std::endl;
    std::cout.flush();

    //保留屏幕
    _getch();

    //删除通知
    nErr = AdsSyncDelDeviceNotificationReq(pAddr,hNotification);
    if (nErr)
        std::cerr << "Error:AdsSyncDelNotificationReq:" << nErr << std::endl;
    
    //释放句柄
    nErr = AdsSyncWriteReq(pAddr,ADSIGRP_SYM_RELEASEHND,0,sizeof(hUser),&hUser);
    if (nErr)
        std::cerr << "Error:AdsPortClose" << nErr << std::endl;
}

void _stdcall Callback(AmsAddr* pAddr, AdsNotificationHeader* pNotification, ULONG hUser)
{
    int nIndex;
    static ULONG nCount = 0;
    SYSTEMTIME SystemTime, LocalTime;
    FILETIME FileTime;
    LARGE_INTEGER LargeInterger;
    TIME_ZONE_INFORMATION TimeZoneInformation;

    std::cout << ++nCount << "Call\n"<<std::endl;

    //打印变量的值和通知的值
    std::cout << "Value:" << *(ULONG*)pNotification->data << std::endl;
    std::cout << "Notification:" << pNotification->hNotification << std::endl;

    //把时间戳的格式转换成系统时间的格式
    LargeInterger.QuadPart = pNotification->nTimeStamp;
    FileTime.dwLowDateTime = (DWORD)LargeInterger.LowPart;
    FileTime.dwHighDateTime = (DWORD)LargeInterger.HighPart;
    FileTimeToSystemTime(&FileTime,&SystemTime);

    //将时间转换到当前时区
    GetTimeZoneInformation(&TimeZoneInformation);
    SystemTimeToTzSpecificLocalTime(&TimeZoneInformation,&SystemTime,&LocalTime);

    //打印时间戳
    std::cout << LocalTime.wYear << "-" << LocalTime.wMonth << "-" << LocalTime.wDay << " " << LocalTime.wHour << ":" << LocalTime.wMinute << ":" << LocalTime.wSecond << ":"
        << LocalTime.wMilliseconds << std::endl;
    
    //打印buffer的大小
    std::cout << "SampleSize:" << pNotification->cbSampleSize << std::endl;

    //打印MAIN.PLCVar变量的句柄
    std::cout << "hUser:" << hUser << std::endl;

    //打印ADS server的ADS地址信息
    std::cout << "ServerNetId:";
    for (nIndex = 0;nIndex < 5;nIndex++) 
    {
        std::cout << (int)pAddr->netId.b[nIndex] << ".";
    }
    std::cout << (int)pAddr->netId.b[5] << std::endl;

    std::cout << "nPort" << pAddr->port << std::endl;
    std::cout.flush();

远程PLC中的代码很简单,此处仅列出需要注意设置的部分,为了可以通过ADS访问临时变量,需要在PLC中启用生成TMC文件功能

在这里插入图片描述

仅需在PLC中声明一个PLCVar的变量即可,不做任何操作,运行PLC的时候,对其进行在线修改即可

在这里插入图片描述

测试结果截图如下,在PLC中在线修改PLCVar的值,同时观察cmd的窗口即可观察到变量的变化,值只要变动,cmd窗口的值就会更新

在这里插入图片描述
把值更新一下
在这里插入图片描述

  • 6
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值