ARTICLE DETAIL

资讯详情

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

TwinCAT ADS协议详解:用C#从PLC高效读取数组数据

TwinCAT ADS协议详解:用C#从PLC高效读取数组数据 1. 读懂Sample01之前先弄明白ADS在干嘛1.1 一个协议撑起一台“软PLC”的通讯基座做自动化上位机开发的人早晚都会碰上倍福这套东西。不管是TwinCAT 2还是TwinCAT 3上位机要读写PLC里的变量基本都绕不开ADS协议。ADS全称Automation Device Specification是Beckhoff定义的一套设备间通讯协议规范核心作用就是让外部应用程序能够访问TwinCAT实时运行环境里的数据。这里有个背景要先理清楚。TwinCAT本质上是一套运行在Windows上的实时软件系统PLC程序并不是直接跑在Windows内核里的而是跑在一个独立的实时子系统里。PLC里用ST语言或FBD图定义的变量都存在于这个实时子系统的符号表中。普通进程想直接通过内存地址去拿这些数据是拿不到的必须由ADS路由做“翻译”和“转发”。所以你可以把ADS理解为一把钥匙上位机程序拿着它去敲TwinCAT实时内核的门然后按变量名把数据取出来。很多人会把ADS和S7通讯类比但有个关键差异一定要记住S7通讯通常面向寄存器地址而ADS允许直接通过符号名访问变量。也就是说上位机不需要关心PLC内部怎么分配地址只要变量名没错ADS运行时就会自动完成符号解析和内存定位。这就是Sample01里代码看着这么短、却能把PLC数组完整读回来的底层原因。1.2 为什么偏偏是.NET Framework而不是新版.NET现在.NET已经迭代到8、9了TwinCAT的ADS官方库也提供了对应版本。那为什么Sample01这类基础示例还是基于.NET Framework真实原因有两个。第一工厂里的上位机项目大多是历史工程常年跑在Windows 7或较老的Windows Server上系统里稳定运行着.NET Framework 4.x甚至3.5。迁移到.NET Core可能牵扯到第三方UI库、报表组件、老式COM接口调用这些组件在新框架下未必有替代品。所以最务实的做法就是把ADS通讯模块加到老框架项目里。第二Beckhoff官方示例库中Sample01这类入门示例最早就是基于.NET Framework 4.x写的官方DLLTwinCAT.Ads.dll在4.x下运行没有任何兼容问题文档和社区讨论也最齐全。你拿新版库去连同一个PLC核心逻辑其实差不多但Framework版本报错在网络上一搜一大片反而是旧版最稳。2. 环境准备引用库和AMSNetId两个最容易被绊倒的地方2.1 NuGet引入TwinCAT.ADS的两种方式把环境跑通第一步是引入ADS通讯库。主要有两种方式。第一种是NuGet包管理器。在Visual Studio里右键项目选择“管理NuGet程序包”搜索“TwinCAT.Ads”一般会看到两个结果一个是Beckhoff.TwinCAT.Ads对应老命名空间核心类是AdsClient另一个是TwinCAT.Ads新版命名空间Api和用法有差异。Sample01多数情况使用的是老命名空间下的DLL。我的建议是看一下你拿到的示例代码里using的是哪个命名空间就按那个来引。第二种方式是手动添加引用。很多车间电脑不允许连外网NuGet根本用不了这时候需要把DLL拷到本地。路径一般在TwinCAT安装目录下C:\TwinCAT\AdsApi\或者安装包解压后的ADSNET文件夹里。引用的时候有个细节容易踩坑老版本TwinCAT.Ads.dll同时存在x86和x64版本。如果你的上位机程序还需要访问西门子、三菱等其他PLC的通讯库而这些库经常是x86的为了整个进程统一你可能得把整个项目编译目标调成x86否则进程启动时会直接报BadImageFormatException。2.2 AMSNetId和端口号到底填什么这是Sample01里第一个让新手挠头的地方。连接代码通常长这样using TwinCAT.Ads; AdsClient client new AdsClient(); client.Connect(192.168.0.12.1.1, 851);“192.168.0.12.1.1”不是普通的IP地址它是AMSNetId一共6个字节写成点分形式就是6段数字。前4段习惯上跟PLC的IP地址保持一致后2段是系统代号常见的是1.1。查看方法很简单在TwinCAT XAE环境中双击SYSTEM节点在“AMS Router”页面查看路由表每个安装TwinCAT的电脑或运行中的PLC目标都会有自己的AMSNetId。端口号851对应PLC运行时PLC Runtime的标准端口。如果TwinCAT运行在同一台电脑上也就是本机连接AMSNetId一般填写本机ID比如127.0.0.1.1.1这种形式。有些特殊场景会改用801系统服务端口或自定义端口但Sample01最标准的组合就是目标AMSNetId加851。提示连接不上时不要急着改代码先用TwinCAT自带的Router诊断工具查看对方路由是否在线。很多时候问题出在路由表配置而不是端口号。3. Sample01逐行拆解连接、读数组、收尾3.1 先连上再说话AdsClient的ConnectSample01整体流程分四段创建客户端对象、建立连接、读取数据、清理资源。前两段的核心代码是class Program { static void Main(string[] args) { string amsNetId 192.168.0.12.1.1; int port 851; AdsClient client new AdsClient(); try { client.Connect(amsNetId, port); Console.WriteLine(Connected to {0}:{1}, amsNetId, port); // 读取操作 } catch (AdsErrorException ex) { Console.WriteLine(ADS error: ex.Message); } finally { client.Dispose(); } } }Connect方法接收两个参数AMSNetId和Port。如果目标是本机可以调用无参的client.Connect()ADS库会自动连接本机默认PLC端口。但示例代码里通常显式传参目的就是让初学者清楚自己连的是什么。还有一个细节值得注意AdsClient实现了IDisposable用完之后一定要释放。很多人第一次跑通就不管了结果程序多次连接之后本地路由表里攒了一堆僵尸连接后面再连就会间歇性失败。我写项目时坚持用try-finally或using这个习惯救了我不止一次。3.2 读取数组的三种写法与区别读取数组是重头戏。Sample01里最常见的写法是// 方式一ReadAny直接按类型读 short[] buffer new short[10]; int result client.ReadAny(MAIN.arrTest, typeof(short[]), buffer);这句话的含义是从PLC符号表中定位MAIN.arrTest这个变量按照short[]类型把它读取到buffer中。返回的result是ADS对本次请求的状态码0表示成功非0可以在错误码表中查到具体原因。第二种写法是先用变量句柄Handle再读uint handle client.CreateVariableHandle(MAIN.arrTest); try { short[] buffer new short[10]; client.ReadAny(handle, typeof(short[]), buffer); } finally { client.DeleteVariableHandle(handle); }为什么要多此一举用句柄因为ADS每次按符号名读取时都需要在符号表中做一次名称到地址的解析。如果程序高频读取同一个变量比如每秒几十次这个查找开销就很可观。句柄相当于提前把“名字到地址”的映射关系建立好之后每次读取都直接走地址省去了符号解析的时间。在要求实时性的上位机程序里句柄几乎成了标配。第三种方式适合数据量大的场景使用ADS通知Notification。数据变化时ADS会自动把新值推送到上位机而不是上位机每次发请求去拉。Sample01一般不会这么深入但如果你读数组只是为了刷新界面用轮询反而白白浪费带宽用Notification才是正路。通知机制在ADS库里对应AdsNotification以及client.Notification事件代码复杂一些但收益明显。3.3 收尾动作释放句柄和连接读数组这种看似简单的操作坑往往都在尾部。句柄创建了不删会持续占用路由资源客户端连接不断开下次连接可能报“连接已被占用”。官方示例写得很朴素但我在项目里都会牢记句柄用完就删客户端用完就Dispose顺序上先删句柄再断开连接。下面是一段比较完整的Sample01风格示例把读取和清理都放在一起using System; using TwinCAT.Ads; class Sample01 { static void Main() { string amsNetId 192.168.0.12.1.1; int port 851; AdsClient client new AdsClient(); try { client.Connect(amsNetId, port); uint handle client.CreateVariableHandle(MAIN.arrTest); try { short[] data new short[10]; int status client.ReadAny(handle, typeof(short[]), data); if (status 0) { foreach (short val in data) { Console.WriteLine(val); } } } finally { client.DeleteVariableHandle(handle); } } catch (AdsErrorException ex) { Console.WriteLine(ADS通信错误: ex.Message); } finally { client.Dispose(); } } }4. 数组读回来的那点事长度、类型、性能4.1 类型映射PLC的INT对应.NET的什么数组能否正确读回第一关就是类型映射要对。TwinCAT的PLC类型和.NET类型不是一一对应的尤其是整型长度差异最容易坑人PLC类型字节数.NET类型备注BOOL1bool数组读取时按字节排列BYTE / USINT1byteINT2short特别注意不是intDINT4intWORD2ushortREAL4floatLREAL8doubleSTRING可变string数组读场景较少这是非常容易踩的坑。如果你在PLC里定义的是INT也就是16位有符号整数在C#里却用int[]去接收ReadAny要么抛异常要么返回一堆错位的数据。乍一看毫无头绪本质上就是类型字节长度对不上。4.2 数组长度校验与半途返回比如PLC里定义arrTest : ARRAY[1..10] OF INT;那在.NET这边至少要准备short[10]的缓冲区。ReadAny会把你传入的buffer长度当作期望读取的长度。如果buffer长度小于实际数组长度它可能只读前几个元素也可能直接判定请求非法取决于ADS库的具体实现。更保险的做法是先用client.ReadAny(MAIN.arrTest, typeof(short), out short single)读取单个元素确认格式和变量名没问题调试时把返回的状态码打印出来ADS的错误码非常精细比瞎猜变量名高效得多。连不上时错误码往往是路由问题读不到时错误码往往是符号名或类型问题排查起来有的放矢。4.3 2000个元素的数组轮询速度如何优化数组越大单次读取的报文越长耗时也呈线性增长。如果只是每秒刷新一次界面2000个元素问题不大但如果上位机要做高速采集或参与控制一次全读就不划算了。我的做法通常是分块读取// 按偏移分块读取每块256个short short[] chunk new short[256]; for (int i 0; i 2000; i 256) { int remaining Math.Min(256, 2000 - i); client.ReadAny(handle, typeof(short[]), chunk, new int[] { i }, new int[] { remaining }); // 处理chunk }ADS库提供了带数组偏移和读取长度的重载这在协议层面对应“按索引访问数组段”的能力。对于几千个元素的数组分块读取比一次性拉全量数据更稳定也更容易做超时控制。如果PLC侧数组是允许动态变化长度的分块读还能顺带校验当前实际长度避免越界。5. 实测翻车记录从连接失败到数据错位5.1 连不上的三种原因和定位手段我实际遇到的连接失败主要集中在三种路由不通、端口错、防火墙拦截。路由不通最常见。PLC那侧必须把上位机的AMSNetId添加到路由表中操作位置在TwinCAT XAE的SYSTEM - AMS Router右键添加。很多PLC项目只在控制器侧配了到上位机的路由反向没配导致上位机连PLC时直接超时。定位手段很简单在TwinCAT的命令行里敲route命令查看路由表或者打开TC3的Router Diagnose工具看AMS路由状态是不是ACTIVE。端口错的典型场景是目标不是标准PLC运行时端口。如果PLC侧用的是801系统端口、852 NC端口或者用户自定义端口你用851去连当然失败。检查目标端口即可确认。防火墙拦截比较隐蔽因为ADS报文依赖TCP/UDP动态端口分配即使AMSNetId明确Windows防火墙还是可能拦。最快的测试办法是目标电脑上临时禁用防火墙如果马上能连上说明问题就在这。之后在防火墙里放行TwinCAT相关进程即可主要涉及TcSysService.exe、Router.exe等。5.2 “有效执照”报错、端口被占用的处理不少刚接触倍福的人会在网络上看到“ADS无法找到有效执照”的报错这通常不是代码问题而是TwinCAT授权没有正确部署。PLC运行ADS服务是需要许可证授权的LIC文件要安装到目标电脑且授权绑定的System ID必须与当前电脑一致。文件放在C:\TwinCAT\3.1\License目录下然后用系统托盘里的TwinCAT License Client激活。评估模式下TwinCAT有试用期到期后ADS服务会停止响应代码层面表现为连接超时或没有数据。还有一种情况是端口被占用。本机运行多个TwinCAT实例或者自己开发的服务占了851端口底层Socket会直接报错。用netstat -ano查一下851被谁占用把多余实例关掉就能解决。5.3 数据错位和缓存刷新问题数据错位往往不是通讯错而是类型映射错。结构体布局不一致、字段偏移看错都会导致数组读回来之后前几个元素对后面全乱。这里有个很实用的调试技巧在PLC里固定给数组赋值已知序列比如arrTest[1] : 100; arrTest[2] : 200; ...上位机读回来后逐项对照。数值一旦对不上很快就能判断出是类型长度不对还是符号名指向了错误的变量。至于缓存刷新ADS本身每次读写都是实时访问TwinCAT运行时并不存在协议层的缓存滞后。如果你连续读同一个数组发现数值老是旧的多半是自己在上位机内存里缓存了一份没有真正更新的结果。修改这个误区只需要检查程序逻辑里是不是自己把数据存到了静态变量里。6. 从Sample01延伸出去项目里的实用小习惯6.1 把ADS通讯封装成一个小类Sample01演示的是控制台里一把梭的写法但真实上位机项目里我通常不会让业务代码直接操作AdsClient。一般会封装成一个通讯服务类把连接参数、重连逻辑、日志都放进去。基本骨架长这样public class AdsService : IDisposable { private AdsClient _client; private readonly string _amsNetId; private readonly int _port; public AdsService(string amsNetId, int port) { _amsNetId amsNetId; _port port; } public bool Connect() { try { _client new AdsClient(); _client.Connect(_amsNetId, _port); return true; } catch { return false; } } public T[] ReadArrayT(string symbolName, int length) { T[] buffer new T[length]; _client.ReadAny(symbolName, typeof(T[]), buffer); return buffer; } public void Dispose() { _client?.Dispose(); } }这样调度层、界面层只关心业务数据ADS的细节被隔离在服务类里。以后换了PLC的IP只要改配置文件不用动任何业务代码。项目越大这个习惯越值钱。6.2 轮询、通知、手写心跳的取舍最后说一点我自己的取舍经验。项目里如果只是做数据采集、展示我倾向于用Notification用一次连接订阅多个变量减少通讯量和CPU占用。如果是和PLC做握手式的任务交互比如上位机发指令后等PLC返回完成标志我倾向于轮询加超时因为逻辑最直观出问题也好排查。还有一个细节长时间运行的上位机程序我会每30秒发一次心跳读取比如读一个系统时间变量。这样既能及时发现连接断开也能让路由保持活跃状态避免被操作系统或防火墙误判为无活动连接而切断。这个经验不在Sample01的官方注释里但在我跑过的项目里确实避免了好几次莫名其妙的掉线问题。如果让我总结这次从官方示例学到的东西其实就一句话先把最基础的连接和读取跑通再谈优化。Sample01就是把这条路铺平的最小工程理解它再去看Notification、结构体读写、跨网段路由这些高级内容都会顺手很多。
返回列表