简介:基于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)是端口851,NC轴系统是端口501,HMI是端口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 端口号怎么确定
同一个控制器上跑着多个服务,端口号就是区分服务的标识。最常碰到的几个端口号是:
| 端口号 | 服务 | 典型用途 |
|---|---|---|
| 10000 | TwinCAT HMI / PLC Runtime旧版本 | 老项目里常见 |
| 851 | PLC Runtime(TwinCAT 3) | 读取PLC变量 |
| 501 | NC轴系统 | 读写轴位置、速度 |
| 410 | 注册表访问 | 比较少用 |
| 900 | I/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( length=2, # 字节长度,BOOL对应1,INT对应2 st_cycles=100, # 采样周期,单位是100微秒 max_delay=500 ) 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_cycles=100相当于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状态切到Run,ADS访问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里定义的是DINT(32位整数),上位机用PLCTYPE_INT(16位)去读,读出来的数值就是莫名其妙的。
排查办法:先看PLC里变量的声明类型,再对照ADS库的PLC数据类型映射表。pyads里常见的映射关系如下:
| PLC类型 | pyads常量 | Python类型 |
|---|---|---|
| BOOL | PLCTYPE_BOOL | bool |
| BYTE | PLCTYPE_BYTE | int |
| INT | PLCTYPE_INT | int |
| DINT | PLCTYPE_DINT | int |
| REAL | PLCTYPE_REAL | float |
| LREAL | PLCTYPE_LREAL | float |
| STRING | PLCTYPE_STRING | str |
5.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, port=851): 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_type=pyads.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_ms=10): attrib = pyads.NotificationAttrib( length=2, st_cycles=cycle_ms * 10, max_delay=500 ) 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的通讯需求,直接把这段思路搬过去用,应该能省不少折腾的时间。
本文还有配套的精品资源,点击获取