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中的符号名完全一致。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类型 | 字节数 |
|---|---|---|
| BOOL | bool | 1 |
| INT | short | 2 |
| DINT | int | 4 |
| REAL | float | 4 |
| LREAL | double | 8 |
| STRING(80) | string | 81 |
对于结构体或数组,可以使用 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通信不是一步到位的:先通过NetId找到设备,再通过端口号选中PLC运行时服务,最后通过句柄把变量"锁定"下来。三个环节任何一个出错,读写都会失败。在实际MES集成项目中,建议把NetId、端口号、变量路径都做成配置文件,便于在不同产线之间切换,同时配合连接状态监控和句柄自动重建机制,保证长时间运行的稳定性。