3. ADS通信基础与变量读写

在TwinCAT生产数据采集系统中,上位机(如C#编写的MES客户端)与PLC之间的数据交换几乎都通过ADS(Automation Device Specification)协议完成。ADS是Beckhoff定义的一套独立于底层总线的通信规范,它既可以在同一台工控机的进程之间通信,也可以跨网络访问远端TwinCAT控制器。理解ADS通信,核心要抓住三个概念:ADS设备(Device)、ADS端口(Port)和变量句柄(Handle)。这三者构成了从"找到目标"到"定位变量"再到"持续读写"的完整链条。

一、ADS设备:通信的目标对象

ADS设备指的是一个可被ADS访问的通信端点。在TwinCAT体系中,最常见的ADS设备就是TwinCAT运行时(TwinCAT 2的TC2、TwinCAT 3的TC3),它对外暴露一个AMS NetId作为唯一地址标识。AMS NetId通常由6段字节组成,例如本机回环地址为 127.0.0.1.1.1,而一台实际控制器的NetId可能是 192.168.1.10.1.1,其中前四段对应IP地址,后两段标识该主机上的ADS路由器实例。

在代码中,ADS设备通过 AmsAddress 或 AmsNetId 加端口号来描述。使用TwinCAT.Ads库时,典型写法是:

using TwinCAT.Ads;

// 方式一:直接指定NetId和端口
AmsAddress address = new AmsAddress("192.168.1.10.1.1", 851);
TcAdsClient client = new TcAdsClient();
client.Connect(address);

// 方式二:本机连接,使用默认端口
TcAdsClient localClient = new TcAdsClient();
localClient.Connect(851);

这里的 851 就是ADS端口号,它决定了我们访问的是TwinCAT运行时中的哪一个服务。

二、ADS端口:区分不同的服务通道

同一台ADS设备上可以同时运行多个ADS服务,端口号就是用来区分这些服务的逻辑通道。TwinCAT中常见的端口号有:

  • 851:TwinCAT 3 PLC运行时(Runtime)的标准端口,用于访问PLC变量、调用PLC功能块。
  • 852:TwinCAT 3 PLC的第二个运行时实例(如多PLC项目时)。
  • 801:TwinCAT 2 PLC运行时。
  • 10000:TwinCAT系统服务(System Service),用于读取系统状态、重启等。
  • 10001:TwinCAT实时服务器(Real-Time Server)。

端口号不是随意指定的,它由TwinCAT系统在启动时分配并注册到ADS路由器中。上位机连接时必须使用正确的端口,否则会返回 AdsErrorCode.DeviceInvalidPort 或类似错误。在实际项目中,如果PLC程序运行在第二个运行时实例,端口就要写成852,这一点在MES集成配置中经常被忽略。

三、变量句柄:高效读写的钥匙

ADS协议支持两种变量访问方式:按名称访问和按句柄访问。按名称访问每次都要把变量路径字符串发送给PLC,由PLC解析后再返回数据,效率较低;按句柄访问则先通过一次"获取句柄"操作,让PLC返回一个整数句柄(Handle),后续读写只需携带这个句柄即可,大幅减少通信开销。

获取句柄使用 ReadWrite 或 CreateVariableHandle 方法。以TwinCAT.Ads库为例:

// 获取变量句柄
int handle = client.CreateVariableHandle("MAIN.nProductionCount");

// 使用句柄读取变量
int count = (int)client.ReadAny(handle, typeof(int));

// 使用句柄写入变量
client.WriteAny(handle, 12345);

// 释放句柄
client.DeleteVariableHandle(handle);

句柄是一个由PLC运行时分配的整数,它只在当前连接有效。如果连接断开或PLC重启,句柄会失效,必须重新获取。因此在实际工程中,通常会在连接建立后统一获取所有需要采集的变量句柄,并保存在一个字典中,连接断开时清空,重连后重新获取。

句柄的典型生命周期:连接建立 → 获取句柄 → 循环读写 → 连接断开 → 句柄失效。不要在每次读写时都重新获取句柄,否则会频繁触发PLC内部的符号解析,影响实时性。

四、变量路径的写法

获取句柄时传入的变量路径必须与PLC中的符号名完全一致。TwinCAT 3中,变量路径通常由实例路径 + 变量名组成,例如:

  • MAIN.nProductionCount:主程序MAIN中的整型变量。
  • MAIN.fbMachine.bRunning:功能块实例fbMachine中的布尔变量。
  • GVL_Data.stRecipe.nBatchNo:全局变量列表GVL_Data中的结构体成员。

路径区分大小写,且不能包含空格。如果变量在PLC中被声明为 VAR_GLOBAL 且位于某个GVL中,路径必须带上GVL名称。对于数组元素,可以写成 MAIN.arrValues[0],但更推荐获取整个数组的句柄后一次性读取,减少通信次数。

五、读写操作与数据类型映射

使用句柄读写时,需要指定数据类型。TwinCAT.Ads库提供了 ReadAny 和 WriteAny 方法,支持.NET基础类型与PLC类型的自动映射。常见对应关系如下表:

PLC类型.NET类型字节数
BOOLbool1
INTshort2
DINTint4
REALfloat4
LREALdouble8
STRING(80)string81

对于结构体或数组,可以使用 ReadAny 配合自定义的 struct 或 class,但需要保证内存布局与PLC一致(注意对齐方式)。更稳妥的方式是使用 AdsBinaryReader 逐字段读取,或者使用TwinCAT.Ads库提供的 ReadValue 泛型方法。

六、一个完整的读写示例

下面是一个采集生产计数并写入配方编号的简化示例,展示了设备、端口、句柄三者的配合:

TcAdsClient client = new TcAdsClient();
client.Connect("192.168.1.10.1.1", 851);  // 设备+端口

int hCount = client.CreateVariableHandle("MAIN.nProductionCount");
int hRecipe = client.CreateVariableHandle("MAIN.nRecipeNo");

// 读取
int count = (int)client.ReadAny(hCount, typeof(int));
Console.WriteLine($"当前产量:{count}");

// 写入
client.WriteAny(hRecipe, 2024);

// 清理
client.DeleteVariableHandle(hCount);
client.DeleteVariableHandle(hRecipe);
client.Disconnect();

运行后,如果连接正常,控制台会输出当前产量值,同时PLC中的 nRecipeNo 被改为2024。如果出现异常,可以通过 AdsErrorCode 判断原因:DeviceInvalidNetId 表示NetId错误,DeviceInvalidPort 表示端口不对,DeviceSymbolNotFound 表示变量路径写错。

ADS通信三要素:设备 → 端口 → 句柄 上位机 / MES客户端 TcAdsClient Connect(NetId, Port) CreateVariableHandle() ReadAny / WriteAny DeleteVariableHandle() ADS设备 AMS NetId 192.168.1.10.1.1 本机回环:127.0.0.1.1 标识TwinCAT运行时实例 TwinCAT PLC运行时 端口 851 MAIN.nProductionCount MAIN.nRecipeNo GVL_Data.stRecipe 符号表 + 句柄分配器 AMS路由 端口寻址 变量句柄(Handle)机制 ① 获取句柄 CreateVariableHandle ② 返回整数句柄 如 handle = 5 ③ 循环读写 ReadAny / WriteAny 句柄仅在当前连接有效;断开或PLC重启后需重新获取 设备定位目标 → 端口选择服务 → 句柄锁定变量,三步完成一次高效ADS读写

从图中可以清晰看到,ADS通信不是一步到位的:先通过NetId找到设备,再通过端口号选中PLC运行时服务,最后通过句柄把变量"锁定"下来。三个环节任何一个出错,读写都会失败。在实际MES集成项目中,建议把NetId、端口号、变量路径都做成配置文件,便于在不同产线之间切换,同时配合连接状态监控和句柄自动重建机制,保证长时间运行的稳定性。