简介:本资源是一套面向工业自动化开发者的TwinCAT3与C# ADS通信实战例程,适用于具备基础C#编程能力和PLC通信概念的工程师及高校自动化专业学习者,旨在解决上位机与TwinCAT3 PLC间高效、实时数据交互的核心问题。压缩包共110个文件,含15个C#源码(.cs)、3个Visual Studio解决方案(.sln)与项目文件(.csproj)、8个依赖DLL、5个可执行程序(.exe)及配套配置文件(.config)、说明文档(.txt/.md)和调试资源(.pdb/.xml),整体2.25MB,结构完整,开箱即用。已有386人学习下载,覆盖从环境搭建、ADS连接建立、符号变量读写到实时通知订阅的全流程实现。读者可直接运行示例工程,深入理解AdsClient初始化、ReadBySymbol/WriteBySymbol调用、AddDeviceNotification回调机制等关键API用法,并参考异常处理与资源释放规范,快速掌握工业场景下C#与TwinCAT3协同开发的核心实践路径。
1. TwinCAT3与C# ADS通讯不是“连上就行”,而是要打通实时控制链路的底层握手
很多做上位机开发的工程师拿到TwinCAT3项目后,第一反应是“用C#写个界面读PLC数据”,结果卡在ADS端口不通、符号找不到、结构体解析错乱、甚至ADS库加载失败报WinError 1114。这根本不是C#语法或UI刷新的问题——而是没理解ADS协议本质:它不是HTTP那样的无状态请求,而是基于Windows服务+本地路由表+符号映射的确定性通信通道。本例程聚焦真实产线场景中最常卡住的三个断点:TwinCAT3运行时环境未就绪导致ADS地址无效、C#端未正确绑定到ADS服务器的NetID与端口、结构化数据(如ST数组、UDT嵌套)在.NET侧反序列化时字节对齐错位。适合已装好TwinCAT3但调试ADS连接失败的自动化工程师、C#上位机开发者,以及需要把PLC变量实时驱动WPF图表或数据库写入的集成人员。文中所有命令、配置项、代码片段均经TwinCAT3 Build 4024+VS2022实测,不依赖任何第三方NuGet包,仅使用Beckhoff官方ADS DLL。
2. 搭建ADS通信基础环境:从TwinCAT3服务启动到C#项目引用验证
ADS通信的前提是TwinCAT3系统处于可被外部访问的状态,而非仅本地模拟。很多开发者误以为“TwinCAT3启动了”就等于ADS就绪,实际必须确认三件事:RT实时内核是否加载、ADS服务器是否注册为Windows服务、本地路由表是否包含有效NetID。以下步骤缺一不可。
2.1 确认TwinCAT3运行时服务状态与NetID分配
TwinCAT3的ADS服务由TcAdsServer64.exe提供,它必须作为Windows服务运行且状态为“正在运行”。打开PowerShell(管理员权限),执行:
Get-Service | Where-Object {$_.Name -like "*TcAds*"} | Select-Object Name, Status, StartType正常输出应为:
Name Status StartType ---- ------ --------- TcAdsServer64 Running Automatic若状态非Running,手动启动:
Start-Service TcAdsServer64接着检查本地NetID。TwinCAT3默认将本机NetID设为127.0.0.1.1.1,但该值可能被重置。打开TwinCAT System Manager(TcSysManager.exe),点击菜单栏System → Route Configuration,查看“Local NetId”字段。若为空或非标准格式(如192.168.0.100.1.1),需手动填写并点击“Apply”。注意:此NetID必须与C#代码中AdsClient.Connect()的第一个参数完全一致,包括点分十进制格式和末尾.1.1。
提示:NetID不是IP地址,而是Beckhoff定义的6字节标识符,格式为
A.B.C.D.E.F,其中A~D对应IPv4地址,E=1表示主站,F=1表示主站端口。修改后需重启TcAdsServer64服务生效。
2.2 验证ADS端口可达性与路由表条目
ADS默认使用TCP端口851(TwinCAT3)和48898(TwinCAT2兼容)。使用telnet测试端口连通性(需启用Windows Telnet客户端):
telnet 127.0.0.1 851若屏幕变黑且光标闪烁,说明端口开放;若提示“无法打开到主机的连接”,则TcAdsServer64未监听该端口。此时检查TwinCAT3是否处于Config Mode(配置模式):右下角系统托盘图标应为绿色齿轮状,而非红色停止图标。若为红色,右键图标选择“Switch to Config Mode”。
再查路由表。在TcSysManager中,System → Route Configuration页面下方列表即为ADS路由表。确保存在一条Target NetId为127.0.0.1.1.1、Target IP Address为127.0.0.1、State为Active的条目。若缺失,点击“Add Route”,输入Target NetId(同Local NetId)、Target IP(127.0.0.1)、Target AMS Port(851),勾选“Local route”,点击OK。该操作会写入注册表HKEY_LOCAL_MACHINE\SOFTWARE\Beckhoff\TwinCAT3\AdsRouter\Routes,是ADS通信的寻址依据。
2.3 C#项目引用ADS DLL并初始化客户端实例
新建.NET 6.0或.NET Framework 4.7.2以上控制台项目(推荐.NET 6.0以避免WinError 1114)。不要通过NuGet安装Beckhoff.TwinCAT.Ads——该包版本混乱且与TwinCAT3 Build 4024不兼容。正确做法是直接引用TwinCAT3安装目录下的原生DLL:
C:\TwinCAT\Functions\ADS\AdsLib.dll(x64)C:\TwinCAT\Functions\ADS\TcAdsDll.dll(x64)
在Visual Studio中,右键项目→“添加引用”→“浏览”→定位到上述路径,勾选两个DLL。关键设置:在解决方案资源管理器中,选中这两个引用,将“复制到输出目录”属性设为“始终复制”,“生成操作”设为“无”。
然后编写初始化代码:
using System; using TwinCAT.Ads; class Program { static void Main() { // 创建ADS客户端实例 using AdsClient client = new AdsClient(); try { // 连接参数:本地NetID、AMS端口(851)、超时(ms) client.Connect("127.0.0.1.1.1", 851, 5000); Console.WriteLine($"ADS连接成功,本地NetID: {client.GetLocalAddress().NetId}"); Console.WriteLine($"远程设备NetID: {client.GetRemoteAddress().NetId}"); } catch (Exception ex) { Console.WriteLine($"ADS连接失败: {ex.Message}"); // 常见错误码解析 if (ex is AdsErrorCodeException adsEx) { Console.WriteLine($"ADS错误码: 0x{adsEx.ErrorCode:X8}"); switch (adsEx.ErrorCode) { case 0x00000005: Console.WriteLine("错误原因:目标设备未运行或NetID错误"); break; case 0x00000007: Console.WriteLine("错误原因:端口851被防火墙拦截"); break; case 0x0000001F: Console.WriteLine("错误原因:TcAdsServer64服务未启动"); break; } } } } }注意:
AdsClient必须用using声明,否则未释放的ADS句柄会导致后续连接失败。Connect()方法第二个参数是AMS端口,TwinCAT3固定为851;第三个参数是超时毫秒数,建议设为3000~5000,过短易因系统负载波动误判失败。
3. 实现变量读写与结构体解析:从简单INT到嵌套UDT的完整映射
ADS通信的核心价值在于读写PLC变量,但直接读取原始字节易出错。本节演示如何安全读取单个INT、数组、以及TwinCAT3中定义的复杂UDT(User Defined Type),重点解决字节序、内存对齐、字符串编码三大陷阱。
3.1 读取基本类型变量:以PLC中声明的iCounter : INT为例
在TwinCAT3 PLC项目中,声明一个全局变量:
PROGRAM MAIN VAR iCounter : INT := 0; END_VAR编译下载后,在C#中读取:
// 获取变量符号句柄(Symbol Handle) uint handle = client.CreateSymbolHandle("MAIN.iCounter"); try { // 读取INT值(2字节) short value = client.ReadSymbol<short>(handle); Console.WriteLine($"iCounter = {value}"); // 写入新值 client.WriteSymbol<short>(handle, (short)(value + 1)); } finally { client.DeleteSymbolHandle(handle); // 必须释放句柄 }关键点:
CreateSymbolHandle()返回uint句柄,是ADS内部索引,比字符串路径查询快10倍以上;ReadSymbol<T>()泛型方法自动处理字节转换,T必须与PLC变量类型严格匹配(INT→short,DINT→int,REAL→float);DeleteSymbolHandle()必须调用,否则句柄泄漏导致TwinCAT3内存溢出。
3.2 读取结构化数据:解析PLC中的UDT(User Defined Type)
假设PLC中定义UDT:
TYPE ST_Motor : STRUCT bEnable : BOOL; fSpeed : REAL; sStatus : STRING(20); aPosition : ARRAY[0..2] OF LREAL; END_STRUCT END_TYPE GLOBAL gMotor : ST_Motor;在C#中需定义完全匹配的结构体,并用[StructLayout(LayoutKind.Sequential, Pack = 1)]强制内存对齐:
using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ST_Motor { [MarshalAs(UnmanagedType.U1)] public bool bEnable; // BOOL占1字节 public float fSpeed; // REAL占4字节 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)] // STRING(20)含结尾\0,共21字节 public string sStatus; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] public double[] aPosition; // LREAL数组,每个8字节 } // 读取整个UDT uint udtHandle = client.CreateSymbolHandle("gMotor"); try { ST_Motor motor = client.ReadSymbol<ST_Motor>(udtHandle); Console.WriteLine($"Enable: {motor.bEnable}, Speed: {motor.fSpeed}"); Console.WriteLine($"Status: {motor.sStatus.TrimEnd('\0')}"); Console.WriteLine($"Position: [{string.Join(", ", motor.aPosition)}]"); } finally { client.DeleteSymbolHandle(udtHandle); }提示:
Pack = 1是关键!TwinCAT3默认按1字节对齐,若C#结构体未指定Pack,.NET可能按4或8字节对齐,导致fSpeed读取错位。SizeConst = 21因STRING(20)在ADS中存储为20字符+1字节空终止符。
3.3 批量读写提升效率:使用ADS Read/Write Multiple API
单次读写一个变量延迟约0.5ms,高频采集时需批量操作。ADS提供AdsSyncReadWriteReqEx2底层接口,但C# SDK封装为ReadMultiple()和WriteMultiple():
// 定义要读取的变量路径和类型 var reads = new[] { new AdsVariableReadRequest("MAIN.iCounter", typeof(short)), new AdsVariableReadRequest("MAIN.fTemp", typeof(float)), new AdsVariableReadRequest("gMotor.bEnable", typeof(bool)) }; // 批量读取 var results = client.ReadMultiple(reads); foreach (var result in results) { Console.WriteLine($"{result.SymbolPath} = {result.Value}"); }该方法一次网络往返完成多个变量读取,比循环调用ReadSymbol快3~5倍。注意:所有变量必须在同一ADS设备上(即同一NetID),且总数据长度不超过4096字节(ADS协议限制)。
4. 解决WinError 1114 DLL初始化失败与UI线程卡顿问题
OSERROR: [WinError 1114] 动态链接库(DLL)初始化例程失败是ADS开发中最棘手的报错之一,表面看是DLL加载失败,实则根因在.NET运行时与TwinCAT3 ADS DLL的线程模型冲突。而C#上位机UI卡顿,则源于ADS同步调用阻塞主线程。二者需协同解决。
4.1 根治WinError 1114:强制x64平台与正确的DLL加载顺序
该错误90%由以下三种情况触发:
- 项目平台目标设为
Any CPU,而TwinCAT3 ADS DLL仅支持x64; AdsLib.dll和TcAdsDll.dll未按顺序加载(TcAdsDll.dll依赖AdsLib.dll);- .NET运行时版本与TwinCAT3 Build 4024不兼容(如.NET 5.0+需额外配置)。
解决方案:
- 在项目属性中,将“平台目标”明确设为
x64(而非Any CPU); - 在
Program.cs顶部添加静态构造函数,确保DLL按序加载:
static class AdsLoader { static AdsLoader() { // 先加载AdsLib.dll(被TcAdsDll.dll依赖) var adsLibPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "TwinCAT", "Functions", "ADS", "AdsLib.dll"); if (!File.Exists(adsLibPath)) throw new FileNotFoundException("AdsLib.dll not found"); LoadLibrary(adsLibPath); // 再加载TcAdsDll.dll var tcAdsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "TwinCAT", "Functions", "ADS", "TcAdsDll.dll"); if (!File.Exists(tcAdsPath)) throw new FileNotFoundException("TcAdsDll.dll not found"); LoadLibrary(tcAdsPath); } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr LoadLibrary(string lpFileName); }- 对于.NET 6.0+项目,在
.csproj文件中添加:
<PropertyGroup> <RuntimeIdentifier>win-x64</RuntimeIdentifier> <PublishTrimmed>false</PublishTrimmed> </PropertyGroup>注意:
LoadLibrary调用必须在AdsClient实例化前完成,否则SDK内部加载会失败。PublishTrimmed=false防止.NET发布时移除ADS DLL依赖。
4.2 消除UI卡顿:用Task.Run解耦ADS调用与WPF/WinForms线程
C#上位机常见问题是“循环读取PLC数据导致界面冻结”。根本原因是ReadSymbol()是同步阻塞调用,若在UI线程执行,会挂起整个消息泵。正确做法是将ADS操作移至后台线程,并用Dispatcher.Invoke安全更新UI:
// WPF示例:每100ms读取一次iCounter并更新TextBlock private async void StartReading() { while (isReading) { try { // 在后台线程执行ADS读取 var value = await Task.Run(() => { return client.ReadSymbol<short>(counterHandle); }); // 安全线程切换回UI线程更新控件 Dispatcher.Invoke(() => { txtCounter.Text = value.ToString(); }); } catch (Exception ex) { Dispatcher.Invoke(() => MessageBox.Show($"读取失败: {ex.Message}")); } await Task.Delay(100); // 控制采样频率 } }对于高频采集(如1kHz),建议使用System.Threading.Channels构建生产者-消费者队列,ADS线程只负责采集并写入Channel,UI线程按需消费最新值,避免Task.Delay精度不足。
5. 实战调试技巧:快速定位ADS连接中断与符号查找失败
当ADS连接突然中断或CreateSymbolHandle返回0时,不能仅靠异常信息判断。需结合TwinCAT3日志、Windows事件查看器及ADS诊断工具三层排查,以下为一线工程师验证有效的速查清单。
5.1 启用TwinCAT3 ADS详细日志并定位错误源头
TwinCAT3内置ADS日志开关,无需重启服务即可开启。打开注册表编辑器,导航至:HKEY_LOCAL_MACHINE\SOFTWARE\Beckhoff\TwinCAT3\AdsRouter
新建DWORD值:
- 名称:
LogLevel,值:3(0=关闭,1=错误,2=警告,3=详细) - 名称:
LogToFile,值:1 - 名称:
LogFilePath,值:C:\TwinCAT\Logs\AdsRouter.log(确保目录存在)
重启TcAdsServer64服务后,日志将记录每次连接、符号查找、读写请求的完整流程。典型错误日志:
[2024-05-20 14:22:31] ERROR: Symbol 'MAIN.iCounter' not found in symbol table [2024-05-20 14:22:35] WARN: Connection from 127.0.0.1:54321 closed due to timeout若出现Symbol not found,说明PLC项目未编译下载,或变量名拼写错误(区分大小写!)。
5.2 使用AdsQuery工具验证符号路径与数据类型
Beckhoff官方提供AdsQuery.exe(位于C:\TwinCAT\Bin\AdsQuery.exe),是诊断符号问题的终极工具。运行后输入:
AdsQuery -n "127.0.0.1.1.1" -p 851 -s "MAIN.iCounter"输出示例:
Symbol: MAIN.iCounter IndexGroup: 0xF000 IndexOffset: 0x00001234 Size: 2 bytes Type: INT若返回Symbol not found,则问题在PLC端;若返回类型与C#中ReadSymbol<T>的T不匹配(如PLC为DINT但C#用short),则必报AdsErrorCodeException。
5.3 ADS连接状态监控表:关键指标与阈值参考
| 监控项 | 正常范围 | 异常表现 | 应对措施 |
|---|---|---|---|
| 连接建立时间 | < 100ms | > 500ms | 检查防火墙、TcAdsServer64服务状态 |
| 单次ReadSymbol耗时 | < 2ms(局域网) | > 10ms | 检查PLC负载、网络抖动、变量是否在优化块中 |
| 符号句柄创建失败率 | 0% | > 1% | 清理TwinCAT3符号表缓存(重启TcXaeShell) |
| ADS路由表条目数 | 1~5条 | > 20条 | 删除冗余路由,避免NetID冲突 |
提示:在TwinCAT3 System Manager中,System → Diagnostics → ADS Router Statistics可实时查看连接数、请求吞吐量、错误计数。若“Failed Requests”持续增长,优先检查PLC程序是否崩溃或进入Stop状态。
本文还有配套的精品资源,点击获取