干这行的都知道,倍福(Beckhoff)的PLC在非标自动化、高端设备制造里出场率很高,而上位机跟TwinCAT交换数据,最直接的一条路就是走ADS(Automation Device Specification)通讯。很多第一次接触倍福的工程师,看完官方文档都是一头雾水:一堆DLL、一堆端口号、还有AMS NetId这种概念,根本不知道从哪下手。这篇文章要聊的Sample01,是Beckhoff官方示例里最简单的一个入门Demo,核心就一件事:从.NET上位机程序里,把PLC侧的一组数组数据读出来,显示到界面上。
Sample01本身代码量不大,但它把ADS通讯的标准流程给你串明白了:怎么建立连接、怎么通过符号名或地址读取数据、读出来的类型怎么和PLC对齐。搞懂了它,后面玩Notification订阅、写PLC数据、调参数都不是难事。适合刚接触倍福上位机开发的工程师,也适合那些已经在用OPC UA或者Modbus、想换个更底层更灵活的通讯方式的同学参考。
1. 项目概述与ADS方案选型
1.1 为什么选择ADS通讯方案
先说说为什么我们最终选ADS,而不是用OPC UA或者直接用Modbus TCP。ADS是Beckhoff自己定义的通讯协议,运行在TCP/IP之上,但它比OPC UA更“贴肉”。举个例子,你用OPC UA读PLC数组,需要先在服务器端配好节点映射,客户端再用UA Client去订阅,链路长,中间多了一层抽象。ADS则直接通过TwinCAT的Router机制访问变量表,只要你知道变量名字或者内存地址,就能直接读,几乎没有额外开销。
在工程现场,时间就是成本。ADS通讯的开发周期很短,尤其你只是把数据从PLC拽回上位机的时候,它比OPC UA那套配置流程要快得多。但ADS也不是没有短板,它对网络环境的依赖比较强,跨网段访问时需要配置TwinCAT Router的静态路由表,这也是后面要重点讲的坑之一。
Sample01选择的通讯方案正是基于标准ADS连接。它不涉及复杂的安全认证、不依赖第三方中间件,直接在.NET进程里引用官方的TwinCAT.Ads.dll,就能完成从连接到读取的全部过程。对于做设备调试、数据采集这类周期性很强的工控场景,这种轻量方案反而最可靠。
1.2 Sample01示例的核心价值
Sample01作为官方示例,价值不在于它有多少行代码,而在于它展示了ADS通讯最标准的代码骨架。我接触过的不少项目,包括后来做的设备数据采集系统、MES对接中间件,核心流程基本就是从这个骨架上长出来的。
你可以把Sample01理解成一个“模板工程”。它告诉你三件事:第一,TcAdsClient这个核心类怎么创建和释放;第二,连接PLC时端口号该填什么(比如TwinCAT 3的PLC运行系统默认是851);第三,读取数组时,API层面应该调用ReadAny还是ReadSymbol,参数该怎么给。
把这些搞明白了,你再看官方文档里的其他复杂示例(比如Notification、读写任意内存区域),会发现只是在这个基础上加了更多API调用而已。这也是我为什么建议新手不要一上来就啃那本几百页的ADS手册,先把Sample01跑通,再顺着它去扩展。
2. 环境准备与项目搭建
2.1 .NET Framework版本怎么选
官方示例最早是基于.NET Framework 3.5写的,后来也提供了.NET 4.x的版本。这里有个实际工程问题:很多工控电脑还是Windows 7或Windows 10老环境,装.NET Framework 3.5偶尔会报错(比如热词里那个错误代码0x80d03805),所以我的建议是直接用.NET Framework 4.7.2或4.8。
为什么推荐4.x而不是3.5?一方面,4.8在Windows 10/11上是可选功能,开启一下就行,而且与老代码基本兼容;另一方面,TwinCAT.Ads这个库本身从早期版本到新版迭代很多,新版本对.NET 4.x支持更好,API也更完整(比如后来增加的ReadSymbol系列方法)。
如果你遇到的是在Win10上装.NET Framework 3.5失败的情况,不用死磕,直接在Visual Studio里把目标框架切到4.7.2,然后重新编译就行。老项目升到4.x一般改动很小,Sample01这种示例更是不用改逻辑,只换框架版本就能跑起来。
2.2 TwinCAT ADS库的引入方式
引入ADS库有两种方式,我实际项目里都用过。
第一种,如果你电脑上装了TwinCAT 3(XAE开发环境),那么这个DLL就在安装目录下,具体路径一般是C:\TwinCAT\AdsApi\TwinCAT.Ads.dll,直接在Visual Studio里添加引用就能用。这种方式的好处是版本一定和你的TwinCAT开发环境匹配,不会出现连不上PLC的问题。
第二种,通过NuGet包管理器安装TwinCAT.Ads包。这种方式更适合搞独立上位机开发的场景,因为你可能只需要ADS通讯功能,不想为了一个DLL把整个TwinCAT安装包都装到上位机电脑上。NuGet会帮你处理依赖,版本控制更灵活。我后来做交付给客户的采集程序,就是打包了这个NuGet包,客户电脑只要装了对应的.NET运行时就行,不需要装TwinCAT全家桶。
注意一点:ADS库分32位和64位,如果你的上位机跑的是64位程序,就去选用x64的平台目标;如果PLC侧有老旧的第三方驱动或老版本TwinCAT,有时候32位反而更稳。我踩过坑,用64位连接不上,换成x86编译立马通了,这个后面在问题排查部分细说。
2.3 一个最小可运行项目的搭建流程
为了让小白也顺利跑通,我把步骤写到可以直接照抄的级别:
- 打开Visual Studio,新建一个控制台应用程序(.NET Framework,4.7.2)。
- 项目名称随意,比如
AdsSample01。 - 右键“引用”,如果走NuGet就搜索
TwinCAT.Ads,如果是本机装了TwinCAT则直接添加DLL引用。 - 确认项目平台目标:如果PLC和上位机都在同一操作系统下,建议直接选
x64,省事。 - 在
Main方法里写基本连接代码,先连上再读数据。 - 编译运行。如果一切正常,你会在控制台窗口里看到从PLC读到的数组内容从0到9依次打出来(前提是你PLC侧的程序里确实定义了对应变量)。
这个工程模板,我后来一直保存着,每次做新项目就直接复制一份改改,特别省时间。
3. 核心代码解析:读取PLC数组
3.1 建立ADS连接
ADS连接的核心类是TcAdsClient。很多第一次接触的人会把它类比成Socket,其实不太准确,它内部封装了TwinCAT Router的握手过程。最基本的使用就两行:
TcAdsClient adsClient = new TcAdsClient(); adsClient.Connect(851);这里的851是端口号,对应TwinCAT 3的PLC运行系统。如果你用的是TwinCAT 2或者TwinCAT 3里有多个PLC任务(比如分配了别的端口),这个数字会不一样。判断方法很简单:在TwinCAT的系统管理器里看“PLC”项的属性,里面就有AMS Port号。
还有一点很多人困惑:本地连接和远程连接传的参数不一样。本地连接(也就是上位机软件和PLC跑在同一台工控机里)直接传端口号就行;远程连接则要多传一个AMS NetId,比如:
adsClient.Connect("192.168.1.20.1.1", 851);这个AMS NetId是PLC的实时内核在TwinCAT Router里注册的标识,前四段通常是PLC网卡的IP地址,后两段是设备标识号。记不住没关系,在TwinCAT系统管理器右上角能看到当前系统的AMS NetId,直接复制。
3.2 读取数组的核心实现
连接建立之后,读取PLC数组最推荐的写法和最稳妥的写法略有差异。先说推荐写法,用ReadSymbol按变量名读取:
static void Main(string[] args) { TcAdsClient adsClient = new TcAdsClient(); try { adsClient.Connect(851); // 读取PLC中的数组,假设PLC侧声明的变量是 arrData: ARRAY[0..9] OF DINT int[] plcArray = (int[])adsClient.ReadSymbol("MAIN.arrData", typeof(int[])); for (int i = 0; i < plcArray.Length; i++) { Console.WriteLine($"arrData[{i}] = {plcArray[i]}"); } } catch (Exception ex) { Console.WriteLine("读取失败: " + ex.Message); } finally { adsClient.Dispose(); } }ReadSymbol方法的核心在于第二个参数typeof(int[]),它告诉ADS库你要把PLC侧的数据当成什么类型来反序列化。PLC侧如果是DINT数组,对应到.NET就是int[];如果是INT数组,对应short[]。
另一种更偏底层的方式是用ReadAny:
int[] plcArray = (int[])adsClient.ReadAny( 0x000000F0, // IndexGroup,通常是数据区域 myOffset, // IndexOffset,偏移地址 typeof(int[]), new int[] { 10 } // 告诉ADS要读10个元素 );这种方式需要你事先知道变量在PLC内存中的地址,维护成本高,一旦PLC里的变量顺序调整了,地址就对不上了。所以我个人建议,除非你是在做老TwinCAT 2项目或者必须走地址访问,否则优先用ReadSymbol,可读性好、不容易错。
还有第三种方式是ReadArray,这是比较新的API,它的好处是直接返回强类型数组,省掉一次拆箱操作:
int[] plcArray = adsClient.ReadArray<int>("MAIN.arrData", 0, 10);这个泛型版本的性能比ReadSymbol好一些,因为少了object装箱拆箱的开销,在频繁读取的场合会体现出来。Sample01里的逻辑其实就涵盖了这三种思路的雏形,你在官方源码里可能看到的是其中一种,但我在工程里都试过,我自己现在的代码习惯是用ReadArray。
3.3 数据映射与类型转换
ADS通讯里,类型映射是最容易踩坑的地方。因为PLC侧的DINT不是C#里的INT(C#的int是32位,PLC的INT其实是16位),如果不注意,读出来的数据会莫名其妙翻倍或截断。下面这个对应关系表,建议直接收藏:
| PLC数据类型 | 存储大小 | .NET对应类型 | 备注 |
|---|---|---|---|
| BOOL | 1字节 | bool(或byte) | 读BOOL数组时用bool[] |
| BYTE | 1字节 | byte | 很少单独用 |
| INT | 2字节 | short | 注意不是int! |
| DINT | 4字节 | int | 最常用 |
| REAL | 4字节 | float | 注意不是double |
| LREAL | 8字节 | double | 和C#的double对应 |
| STRING | 80字节(默认) | string | 长度按PLC声明为准 |
| ARRAY OF DINT | 4*N字节 | int[] | 数组类型要匹配元素类型 |
我遇到过不止一次:PLC里定义的是ARRAY[0..9] OF INT(16位),上位机这里按int[]去读,结果数组里每个数看起来都变成了原来的两倍还加了一堆乱七八糟的值。这类问题的根源都是类型宽度不匹配,C#的int是32位,而PLC的INT是16位,ADS库按你传入的类型去切字节流,宽度不匹配数据自然错乱。
所以读数组前,一定先回PLC程序里看一眼变量类型。搞定类型,再提性能。数组元素越多,读取时长线性增加,但如果只是几十个元素的数组,完全不用操心。真正的坑在于数组跨越内存块边界时的读法,这个放到问题排查部分讲。
4. 常见问题与排查技巧实录
4.1 连接失败怎么排查
连接失败是Sample01最常见的问题。通常分两种情况:一种是连本机都失败,提示找不到目标路由或超时;另一种是远程连接失败,上位机连工控机上的PLC失败。
本地连接失败,九成是端口号不对或者TwinCAT系统没跑起来。先在工控机上打开TwinCAT系统管理器,确认PLC的AMS端口是多少,再看看系统状态是不是Run。很多新手建了PLC项目只是激活了配置,但没点“运行”,那PLC Runtime根本不在,ADS自然连不上。
远程连接失败,先ping一下IP通不通,然后看TwinCAT Router配置。远程访问需要在开发机上把PLC的AMS NetId添加到静态路由表里,这一步常被遗漏。添加方式:在Windows托盘区域找到TwinCAT Router图标,右键打开Router配置,添加一个静态路由,填PLC的AMS NetId和IP。添加完要重启Router进程生效。
另外还有个老生常谈:Windows防火墙。TwinCAT的ADS服务默认监听48898端口(广播跟路由相关还会用到很多高位端口),你可以在防火墙里放通TwinCAT相关进程。如果你在客户现场遇到“上午能连通、下午连不上”的情况,很多时候就是防火墙策略或者笔记本用了无线网卡换了IP导致。
4.2 数据读不对:类型不匹配与数组越界
读出来的数据不对,优先检查类型映射表。本章前面那张表,照着核对一遍。比如PLC的REAL数组,你如果用double[]去读,表面看数值挺像那么回事,实际上小数点后的精度和大小都可能是错的,因为32位浮点的位模式被硬解释成了64位浮点。
还有数组越界的问题。ReadSymbol在编译期无法检查数组长度,它在运行时是按变量的真实长度去读的。但如果你是用ReadAny或者ReadArray指定了长度,务必确保你给的长度不超过PLC数组声明的大小。一旦超出,ADS会返回一个错误状态码,在C#里会抛出异常。真正麻烦的是长度不够,比如数组有20个元素,你只读了10个,程序不报错,但数据不完整,容易引发业务逻辑上难以定位的Bug。
我实际写过一段很蠢的代码,把PLC一个声明长度为100的DINT数组,循环读了1000次,每次都从偏移加,结果大部分数据都是错的。因为跨过数组末尾之后,ADS会给你读相邻变量的内存垃圾。后来学聪明了,直接改读整个数组一次,再在NET侧处理。所以,数组读取要及时摆脱“循环+偏移”思维,优先整块读。
4.3 性能优化与工程落地建议
Sample01在简化场景下怎么读都行,但实际生产环境里,如果你要在大屏、看板或者数据采集服务里高频读取PLC数据,就必须注意下面几点。
第一,连接对象复用。不要每次读周期都new一个TcAdsClient再Connect,连接握手很耗费时间。正确做法是在服务启动时建立连接,整个生命周期复用同一个客户端实例,用完后统一Dispose。我测试过,多线程场景下频繁Connect/Dispose的损耗大概是你读取本身的几十倍,纯属自找麻烦。
第二,高频读取时改用ADS Notification。如果你只需要在PLC数据变化时获得通知,而不是自己掐着每秒50毫秒去轮询,那就要用AddDeviceNotification。这个属于Sample01的进阶玩法,但一旦你的采集频率超过100Hz,Notification方案的优势就非常明显,因为它在数据没变化时不消耗通讯带宽。
第三,用数组批量读代替逐个读。如果PLC里有一百个变量,不要循环调用ReadSymbol一百次,这是大多数人在代码评审阶段都会犯的错。而是应该把相关变量打包到一个结构体里(Structured类型),或者用数组方式一次读回。ADS是实时的工业协议,通讯的物理开销比数据处理开销大得多,批量操作比逐个操作能快上一个数量级。
第四,注意.NET任务的线程亲和性。ADS客户端不是线程安全的,如果多个线程同时读写同一个TcAdsClient实例,会出现随机异常。我用SemaphoreSlim或者lock做串行化,宁可牺牲一点并行度,也不能让连接状态不可控。
最后的落地建议:写一个单独的ADS通讯服务类,把连接、读取、类型转换、断开封装起来,再暴露事件或者回调给上层业务。不要像Sample01那样把通讯逻辑全写在Main函数里——那是教学演示,不是工程结构。我接手过好几个出问题的项目,最后定位完都是因为通讯代码和业务逻辑揉在一起,改一个参数要动一堆地方。
结尾的几句大白话
我自己从跑通Sample01到做正式项目,有一个很深的感触:ADS通讯的官方示例代码短,但文档里藏着很多“弦外之音”,没人带着梳理一遍,自己很容易卡在那些看似简单的细节上。比如AMS NetId的配置、端口号的选择、类型映射表,每一样都是“看起来容易、实际坑人”的典型。这篇笔记基本把我日常写倍福上位机时高频用到的东西都过了一遍,希望你也少走几步弯路。
如果你现在正打算读PLC里的数组数据,拿Sample01这个示例先跑通,把连接方式、读法、类型映射都验证一遍,再去往业务层扩展,这是最快也最稳的路子。后续熟练了,可以继续研究Notification怎么做、怎么把结构体整块读写、怎么把这套ADS通讯封装成稳定的服务组件。这些我都挨个趟过,后面有机会再拆开讲讲。