ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

倍福PLC ADS通讯实战:Python pyads库实现上位机与TwinCAT高效数据交互

倍福PLC ADS通讯实战:Python pyads库实现上位机与TwinCAT高效数据交互 简介基于ADS通讯的倍福PLC示例项目面向需要在上位机与倍福控制器之间实现高效数据交换的C开发人员。该项目完整演示了在Windows环境下调用TcAdsDll库建立ADS客户端、连接PLC服务器、按句柄或地址读写变量的核心流程并覆盖连接建立、异常处理等工程中常用环节。压缩包共31个文件以h与cpp源码、lib与dll动态链接库为主辅以sln工程文件、资源文件及可直接运行的exe程序整体体积仅491KB结构简洁便于快速定位代码与配置。目前已有2181人学习下载适合具备基础C知识、希望快速掌握ADS协议落地方法的工业自动化开发者。通过对照示例可理解ADS路由机制与TwinCAT变量映射方式同时获得一套能直接编译运行的最小通信框架为后续集成到实际控制项目提供可复用的起点。 倍福PLC的ADS通讯说实话是很多做上位机、做非标设备的人绕不过去的一道坎。项目只要一上倍福的系统TwinCAT作为软PLC和运动控制器你总得和它交换数据——要么是MES要采集数据要么是视觉系统要触发定位要么是机器人要跟PLC握手。走Modbus TCP当然也能凑合但一旦涉及实时性要求高、变量多、或者需要读取控制器内部状态的时候ADS协议才是真正干这个活儿的家伙。我最初接触ADS是因为一个视觉定位项目上位机需要用C#跟控制器做数据交互和触发信号那时候被AMS NetId和端口号折磨得够呛。后来用Python做原型验证发现pyads这个库特别好使一套下来整个通讯逻辑就通了。这篇博文就把我实际踩过的坑和能直接抄作业的步骤都写出来适合做过PLC但没接触过ADS通讯的工程师也适合刚入行做上位机开发的朋友。1. ADS通讯的核心价值与工程场景1.1 ADS到底是什么ADS全称Automation Device Specification是倍福TwinCAT系统中用于组件之间通讯的协议。它的底层基于TCP/IP但报文结构是倍福自定义的。和Modbus那种走寄存器地址的方式不一样ADS直接面向变量名和句柄来操作数据。拿Modbus对比一下就明白了Modbus是PLC工程师在程序里手动Map寄存器地址通讯的另一端得照着地址表来读写地址表一变上位机就得跟着改。而ADS是直接以变量名为单位操作代码里写read_symbol(Main.counter)就能把PLC里的计数器值读回来类型、长度这些信息可以由PLC里的符号信息推导出来。省去了映射地址的环节开发效率高了一截出问题的概率也小一些。1.2 什么时候必须用ADS不是所有场景都要用ADS。如果你只是想在触摸屏和PLC之间传几个数据ADS反而杀鸡用牛刀了。ADS真正发挥价值的地方是这么几类上位机软件直接访问PLC全局变量像MES系统采集数台倍福设备的数据每台设备变量几百个用ADS比用Modbus轮询效率高很多。控制器之间的实时数据交互比如两台TwinCAT控制器需要同步数据用ADS添加路由就可以实现高速通讯。访问TwinCAT实时任务内部数据有些变量不通过IO映射呈现比如NC轴的位置、状态字通过ADS就能直接读取。Scope数据采集和调试TwinCAT的ScopeView本质上就是基于ADS采集数据的。我自己的经验是凡是数据量超过几十个点位、或者需要频繁读写的项目直接用ADS能省掉一半的通讯调试时间。2. 准备工作ADS通讯的环境搭建与连接2.1 TwinCAT版本选择和ADS库获取目前市面上常见的TwinCAT版本是TwinCAT 2和TwinCAT 3。TwinCAT 3是在Visual Studio基础上的新架构用ADS通讯方面两个版本差别不大端口号、协议格式基本一致。如果你的PLC是CX系列嵌入式控制器比如CX5120、CX5140或者用普通工控机装了TwinCAT的运行时在另一台电脑做上位机需要确认两件事一是PLC和上位机之间网络能通二是上位机上要能解析ADS协议。解析ADS协议有两种方式一种是自己用Socket发包能看懂官方文档的报文格式就行另一种是直接用现成的库。Windows下用C#或VB.NET可以直接引用倍福官方的TwinCAT.Ads.dll这个DLL在安装TwinCAT时会自动注册到GAC里。Python环境下最常用的是pyads这个开源库底层封装了ADS协议的各种命令。2.2 上位机和PLC的网络连接倍福PLC和电脑连接最常遇到的情况有两种直接网线连接电脑网口直接和PLC的Ethernet口对接这种场景最简单只需设好IP地址在同一网段。通过交换机组成局域网多台设备和PLC都接在同一个交换机上上位机通过IP地址区分不同的PLC节点。连接好之后在电脑命令行里ping一下PLC的IP地址能通就说明物理链路没问题。这里有个小细节如果你用的是CX系列它通常有两个网口一个用于和EtherCAT总线通讯另一个是普通以太网口。连接上位机时建议用标有Ethernet的端口不要接EtherCAT口否则可能通讯时断时续。2.3 确认AMS NetId和端口号ADS通讯有两个核心参数缺一不可AMS NetId相当于设备在ADS网络中的唯一标识格式类似IP地址但一共6个字节通常写成192.168.1.10.1.1这种样子。最后一个网段不是随便填的而是标识设备内的路由编号。ADS端口号决定访问设备内的哪个服务。比如PLC运行时PLC Runtime是端口851NC轴系统是端口501HMI是端口10000。获取PLC的AMS NetId的实战方法打开TwinCAT的System Manager或TwinCAT 3界面在System节点下能看到AMS NetId。如果是在远程电脑上也可以通过点击Choose Target在弹出的对话框里搜索局域网内的设备搜索到的设备会显示它的AMS NetId和IP地址。2.4 安装pyads库Python环境下用pyads是最省事的方式pip install pyads安装完成之后可以用一段简单的代码测试通讯import pyads # 连接到本机的TwinCAT运行时 plc pyads.Connection(127.0.0.1.1.1, 851) plc.open() print(plc.read_symbol(Main.counter)) plc.close()实测下来pyads在Windows和Linux上都表现稳定如果需要在树莓派或者边缘网关这类设备上做数据采集同样可行。3. ADS通讯的核心机制路由、端口与变量访问3.1 AMS路由和NetId的关系很多新手一开始不理解为什么ADS通讯要配置路由。ADS的报文在网络中传输时目的地不是IP地址而是AMS NetId。设备收到报文之后通过路由表决定是本地处理还是转发到别的设备。用生活化的类比来说IP地址相当于门牌号能让你找到楼AMS NetId相当于房间里的人名你得知道找谁。如果你要访问的PLC的NetId没有加到本机的路由表里报文就发不过去。Windows下配置ADS路由有两个方法一是安装TwinCAT后通过TwinCAT Router服务自动绑定二是用Static Routes界面手动添加。如果你只是做上位机开发不想装完整的TwinCAT环境只装一个TwinCAT ADS Router倍福官方的独立路由器安装包就够了。在Linux环境下用pyads则需要在启动时提供本机自己的AMS NetId然后和PLC的NetId建立连接pyads会自动完成路由器注册import pyads pyads.open_port() pyads.local_add_route(192.168.1.10.1.1, 192.168.1.10) plc pyads.Connection(192.168.1.10.1.1, 851) plc.open()3.2 端口号怎么确定同一个控制器上跑着多个服务端口号就是区分服务的标识。最常碰到的几个端口号是端口号服务典型用途10000TwinCAT HMI / PLC Runtime旧版本老项目里常见851PLC RuntimeTwinCAT 3读取PLC变量501NC轴系统读写轴位置、速度410注册表访问比较少用900I/O系统直接访问IO我实际项目里绝大多数场景只需要和851通讯。如果你的控制器上PLC采用了非默认实例名端口号可能要做偏移但多数情况按默认来就行。3.3 符号名访问和句柄机制ADS访问PLC变量有两种作风按符号名SymbolicName读取直接传变量名字符串方便直观适合数量不太大的场景。按句柄Handle读取先通过变量名字符串拿到一个句柄之后用句柄来读值效率更高。pyads的read_symbol和write_symbol就对应符号名方式。如果变量非常多、循环次数很高建议考虑用句柄方式import pyads plc pyads.Connection(127.0.0.1.1.1, 851) plc.open() # 获取句柄 handle plc.get_handle(Main.velocity) # 用句柄读值 value plc.read_by_name(Main.velocity, pyads.PLCTYPE_INT) # 或者直接读句柄 value plc.read(handle, pyads.PLCTYPE_INT) plc.close()用句柄的优点是解析变量名的操作只需做一次循环读写时省掉了重复的字符串匹配在几十毫秒周期的数据采集场景里优势明显。4. 实操用Python实现ADS读写倍福PLC变量4.1 读取PLC全局变量假设PLC里有一个全局变量Main.temperature数据类型是REAL我们要在上位机里读出它import pyads PLC_AMS_NET_ID 192.168.1.10.1.1 PLC_IP 192.168.1.10 PLC_PORT 851 plc pyads.Connection(PLC_AMS_NET_ID, PLC_PORT, PLC_IP) plc.open() try: temp plc.read_symbol(Main.temperature) print(f当前温度: {temp:.2f}) finally: plc.close()Connection的第一个参数是PLC的AMS NetId第三个IP参数是可选的用于路由自动配置。如果是本机TwinCAT第三个参数可以省略。有几点要特别注意如果PLC变量是REAL类型对应Python的float。如果PLC变量是INT对应Python的int如果是BOOL对应bool。变量名里大小写要严格匹配PLC里是Main.temperature你就不能写成Main.Temperature。4.2 写入PLC变量写入操作更直接write_symbol一行就行plc.write_symbol(Main.setpoint, 100.0)但工程上真正的控制命令并没有这么简单因为直接写变量不是线程安全的如果PLC侧同时也在改这个变量会产生并发问题。更可靠的做法是在PLC里定义功能块用ADS去置位触发命令# 触发PLC里的启动命令 plc.write_symbol(Main.cmdStart, True)写入BOOL类型时有一个坑如果PLC侧变量定义的是BOOL写True没问题但如果定义的是BYTE你写True可能被解释成1类型不匹配在某些情况下不会报错但结果不可控。建议严格匹配PLC侧的数据类型。4.3 订阅通知实现实时数据监控有的场景只需要周期读但有的场景要求数据变化时上位机能立刻知道比如报警变量从False变成True。这时候用轮询不仅延迟大还浪费CPU。ADS协议支持Notification机制PLC侧变量变化时主动推送到上位机。pyads里用add_device_notification实现import pyads import time plc pyads.Connection(192.168.1.10.1.1, 851) plc.open() def callback(notification, data): print(data) attr pyads.NotificationAttrib( length2, # 字节长度BOOL对应1INT对应2 st_cycles100, # 采样周期单位是100微秒 max_delay500 ) notif_handle plc.add_device_notification( Main.bAlarm, attr, callback ) time.sleep(10) plc.del_device_notification(notif_handle) plc.close()这里的st_cycles单位是100微秒所以st_cycles100相当于10ms的周期。注意回调函数里不能做重量级操作否则会影响通知的处理线程最好把数据放到队列里由另一个线程做数据入库或者界面刷新。4.4 读取数组和结构体PLC里定义数组或者结构体时ADS读取需要多带点东西。如果你定义的是ARRAY[1..10] OF REAL在pyads里读取import struct data plc.read_symbol(Main.arrData) # 返回的可能是字节串需要按类型解析 values struct.unpack(10f, data)这里有个小坑pyads的read_symbol对于有些类型会返回Python对象但对数组类型常常直接返回原始字节串。用struct.unpack按小端格式解析就对了。遇到结构体同理用ctypes构造结构体后传给read函数这是实际项目中常用的做法。4.5 批量读写优化如果一次需要读上百个变量一个一个read_symbol效率并不差但如果周期要压到几毫秒最好用read_listing这类批量接口或者自己组包发送多变量读取请求。ADS协议本身就支持一条报文里读取多个变量。pyads里没有直接提供批量读的接口但你可以用双缓冲的思路先获取所有变量的句柄再循环用句柄读取handles {} for name in var_list: handles[name] plc.get_handle(name) while True: data {} for name, handle in handles.items(): data[name] plc.read(handle, pyads.PLCTYPE_REAL) # do something with data实测下来几十个变量循环读在10ms周期下一点问题没有CPU占用很低。5. 常见问题与排查技巧实录5.1 通讯连不上的排查步骤ADS通讯做不出来九成是网络、路由、端口三者中的某一个出了问题。我的排查顺序是固定的症状排查方向解决手段pyads报ADSError: No route to target上位机路由表里没加PLC在TwinCAT路由器里添加静态路由或用pyads.local_add_route能通但读不到变量端口号错误确认PLC实例端口是否是851尝试用TwinCAT的Choose Target看能否连接变量读取返回超时PLC程序处于停止状态把PLC状态切到RunADS访问PLC变量需要PLC运行偶发断线网络不稳定或路由表冲突检查交换机端口、网线固定IP地址避免两台设备相同AMS NetId我最常踩的坑是在CX系列上第一次烧录程序时AMS NetId是默认的192.168.1.10.1.1后来改了PLC的IP地址但忘了AMS NetId也变了结果上位机代码里还用老的NetId导致一直连不上。以后凡是换IP或换设备第一时间核对AMS NetId。5.2 变量类型不匹配的坑ADS通讯底层是按字节传输的类型不匹配时不会像高级语言那样直接报错而是读出垃圾数据。比如PLC里定义的是DINT32位整数上位机用PLCTYPE_INT16位去读读出来的数值就是莫名其妙的。排查办法先看PLC里变量的声明类型再对照ADS库的PLC数据类型映射表。pyads里常见的映射关系如下PLC类型pyads常量Python类型BOOLPLCTYPE_BOOLboolBYTEPLCTYPE_BYTEintINTPLCTYPE_INTintDINTPLCTYPE_DINTintREALPLCTYPE_REALfloatLREALPLCTYPE_LREALfloatSTRINGPLCTYPE_STRINGstr5.3 路由表在Linux下配置不生效怎么办在Windows上装TwinCAT时路由很容易自动配置。但Linux上要手动加路由偶尔会遇到加了路由仍然不通的情况。这时要考虑本机AMS NetId是否冲突以及防火墙是否拦了48898端口ADS协议默认的通讯端口。确认方法很简单用netstat -an | grep 48898看端口是否在监听用tcpdump抓包看有没有ADS请求发出。之前我遇到过一次Linux板和PLC不在同一网段导致路由表配了但实际发包到网关去了改成直连网线后问题立刻消失。5.4 高速读写时的性能优化如果循环周期压得很低比如1ms每个周期都read_symbol字符串匹配的开销会被放大。我的优化顺序是用句柄替代符号名省掉字符串解析。把数据打包成结构体用一次ADS读取读取多个变量。必要时直接走ADS的SumRead命令一条报文同时读多个变量。上位机侧把通信逻辑放独立进程与UI解耦避免UI阻塞影响通讯周期。实际项目里做到第1步和第2步大部分场景就够用了。只有非常极限的同步场景才需要研究SumRead。5.5 踩过的一个大坑写的变量被PLC侧覆盖有好几次上位机写入了一个设定值PLC里也确认写入成功了但过一会儿又被改回去了。原因是PLC程序里有其它的逻辑在维护这个变量比如自动模式下的内部计算值不断刷新设定值。ADS写入只是改变了变量的值没有改变PLC程序的执行逻辑。所以ADS写变量之前一定要确认这个变量不会被PLC里的其他代码覆盖。最稳妥的做法是在PLC侧专门定义一个结构体或者数组区域作为“上位机指令区”只由ADS写入PLC程序从这块区域读取指令去执行不反向修改。这样数据流清晰也便于排查问题。6. 从例子到工程我的ADS通讯通用模板经过几个项目的反复打磨我总结了一套通用模板用ADS通讯时基本就是往里套import threading import queue import pyads class AdsClient: def __init__(self, net_id, ip, port851): self.plc pyads.Connection(net_id, port, ip) self.rx_queue queue.Queue() self._running False def connect(self): self.plc.open() self._running True def read(self, symbol, plc_typepyads.PLCTYPE_REAL): return self.plc.read_symbol(symbol, plc_type) def write(self, symbol, value): self.plc.write_symbol(symbol, value) def subscribe(self, symbol, callback, cycle_ms10): attrib pyads.NotificationAttrib( length2, st_cyclescycle_ms * 10, max_delay500 ) return self.plc.add_device_notification(symbol, attrib, callback) def close(self): self._running False self.plc.close()用的时候初始化一次业务逻辑里直接调read和write这样代码清晰后续维护也方便。如果你的上位机是多线程的注意pyads的Connection对象不是线程安全的建议加锁或者把所有ADS操作放在一个线程里串行执行。我在实际项目中就遇到过两个线程同时read_symbol导致程序崩溃的情况加锁后就好了。7. 我的一些补充经验写了这么多年倍福的项目我最后的体会是ADS通讯本身不难难的是调试时的反复排查。有时候通讯不好用不是ADS协议的问题而是网络结构、路由配置或者变量定义上的问题。所以动手之前先把网络拓扑画清楚把PLC变量名和类型列一个清单把AMS NetId和端口号确认好一次通讯调试通常不会超过半小时。如果大家第一次用ADS是在TwinCAT 3环境下建议先在TwinCAT里用自带的TestClient或者TcXaeShell连接到本机随便读一个变量验证环境是否正常。然后跑通pyads的简单读写再上复杂的通知和结构体。步子太大容易踩坑我见过不少人在一开始就想着把所有变量一次性读全结果被各种类型问题搞到崩溃实在没必要。另外一个小技巧如果你同时接了两台倍福PLC想在一个上位机里分别访问不用开两条TCP连接——ADS路由器是支持多目标的你在pyads.Connection里分别指定不同PLC的NetId就行它们共用一个路由服务。这个特性在做多机协作项目时特别有用。做工业通讯这行说白了就是个“耐心活”。ADS通讯的文档倍福官方写得很全但零散得很这篇例子算是把最常用的一环串起来了。以后大家再遇到倍福PLC的通讯需求直接把这段思路搬过去用应该能省不少折腾的时间。本文还有配套的精品资源点击获取
返回列表