
简介本资源是一套基于C#开发的倍福PLC通信实战源码面向工业自动化领域的新手开发者与有一定经验的工控程序员解决C#上位机通过ADS协议与TwinCAT PLC建立稳定数据交互的核心技术问题适用于设备监控、数据采集及HMI开发等典型工业场景。压缩包共61个文件含12个核心C#源码文件.cs、2个Visual Studio解决方案.sln及项目配置文件.csproj配套3个运行所需DLL、3个可执行程序.exe以及2份详实技术文档.docx涵盖WinCE平台ADS通信适配说明与整体通讯流程解析另有PLC程序文件.dfr与符号表文件.sym便于联合调试。资源大小仅1.51MB结构紧凑、注释完整已获1516人学习下载是快速掌握TwinCAT.Ads.dll调用机制、理解ADS地址映射与读写逻辑的高实用性参考范例。1. 项目概述与核心价值最近在做一个工业数据采集的项目客户现场用的是倍福Beckhoff的TwinCAT PLC。和之前玩过的西门子、三菱不太一样倍福这套系统主打的是基于PC的控制它的通信核心是ADS协议。网上找了一圈关于C#通过官方TwinCAT.Ads.dll库进行通信的完整、可运行的实例代码并不多很多资料要么是官方文档的片段要么是只讲理论缺少实操。所以我把自己调试通过的一个完整通信实例程序源码整理了出来并打包成了C#通过TwinCAT.Ads.dll库文件和倍福PLC通信实例程序源码.zip。这个项目不仅仅是一份代码更是一个从环境搭建、库引用、连接建立、数据读写到异常处理的完整工作流拆解。无论你是刚开始接触倍福上位机开发还是正在为某个具体的读写问题头疼这份源码和接下来的详细解析应该都能给你提供一个清晰的参考路径。2. 环境准备与库文件解析2.1 开发环境与工具链选择工欲善其事必先利其器。首先明确我们的战场环境。我使用的是Visual Studio 2022.NET Framework 4.7.2。选择这个版本是基于稳定性和兼容性的考虑很多工业现场的工控机系统可能还是Windows 7或Windows 10 LTSC.NET Framework 4.7.2在这些系统上拥有良好的支持。当然你也可以使用.NET Core或.NET 5/6/8但需要注意TwinCAT.Ads.dll是一个传统的.NET Framework程序集在跨平台的.NET Core/5中引用时需要确保项目支持Windows桌面开发并且可能需要处理一些细微的兼容性问题。对于工业上位机这种对稳定性要求极高的场景我倾向于使用久经考验的.NET Framework。接下来是核心——TwinCAT.Ads.dll库文件。这个DLL是倍福官方提供的用于ADS协议通信的.NET组件。重要提示不要从不明来源下载这个DLL。最可靠的方式是从你安装的TwinCAT开发环境中获取。通常路径在C:\TwinCAT\3.1\Components\AdsApi\.NET具体版本号可能不同。使用这个路径下的DLL能确保其版本与你的TwinCAT运行时环境匹配避免因版本不一致导致的诡异错误。2.2 理解ADS协议与TwinCAT系统架构在写代码之前有必要简单理解一下ADSAutomation Device Specification协议和TwinCAT系统。这能帮你理解后续那些“索引组”、“索引偏移”等参数到底是什么。你可以把TwinCAT系统想象成一个“软件PLC”。它运行在Windows系统上将PC的CPU时间片划分出一部分来实时执行PLC控制任务。而ADS协议就是这套系统中各个模块如PLC运行时、NC运动控制、甚至其他Windows应用程序之间进行数据交换的“通用语言”。ADS通信基于客户端-服务器模型。我们的C#上位机程序就是客户端AdsClient而TwinCAT PLC运行时就是服务器。通信需要两个关键地址AmsNetId 这是一个唯一标识符格式类似于192.168.1.100.1.1。它标识了网络中的目标TwinCAT系统。通常PLC设备的AmsNetId可以在TwinCAT开发环境的“System Manager”中查看或者通过路由表配置。AmsPort 这是一个端口号用于标识目标系统上的具体服务。对于标准的TwinCAT PLC运行时其端口号通常是851。理解了这个模型你就知道建立连接本质上就是我们的客户端程序拿着目标服务器的“门牌号”AmsNetId和“房间号”AmsPort去敲门建立对话通道。3. 核心通信类设计与封装3.1 AdsClientHelper 封装类详解直接裸用TwinCAT.Ads.dll提供的AdsClient类虽然可以但代码会显得冗余且不易维护。我的做法是将其封装成一个AdsClientHelper工具类集中管理连接、提供常用的读写方法、并统一异常处理。using System; using TwinCAT.Ads; namespace BeckhoffAdsDemo.Core { public class AdsClientHelper : IDisposable { private TcAdsClient _adsClient; private bool _isConnected false; private string _remoteNetId; private int _remotePort; // 单例模式根据需求可选 private static AdsClientHelper _instance; private static readonly object _lockObj new object(); public static AdsClientHelper Instance { get { if (_instance null) { lock (_lockObj) { if (_instance null) { _instance new AdsClientHelper(); } } } return _instance; } } private AdsClientHelper() { } /// summary /// 连接到目标PLC /// /summary /// param namenetId目标AMS NetId如 192.168.1.100.1.1/param /// param nameport目标端口PLC通常为851/param /// returns连接是否成功/returns public bool Connect(string netId, int port 851) { try { if (_isConnected _adsClient ! null) { Disconnect(); } _adsClient new TcAdsClient(); _adsClient.Connect(netId, port); _remoteNetId netId; _remotePort port; _isConnected true; Console.WriteLine($”[信息] 成功连接到 {netId}:{port}“); return true; } catch (AdsErrorException ex) { Console.WriteLine($”[连接错误] AdsError: {ex.ErrorCode}, Message: {ex.Message}“); // 这里可以更细致地解析错误码例如0x704表示目标端口未找到 } catch (Exception ex) { Console.WriteLine($”[连接异常] {ex.Message}“); } _isConnected false; return false; } // ... 其他方法见下文 } }封装要点解析TcAdsClient实例管理 将TcAdsClient作为私有成员生命周期由帮助类管理避免外部不当操作。连接状态维护 使用_isConnected标志位防止重复连接或未连接状态下的操作。异常分类处理 特别注意捕获AdsErrorException这是ADS协议返回的特定错误其ErrorCode属性对于排查通信问题至关重要。例如0x706可能表示连接被拒绝0x707表示目标设备未找到。单例模式 在大多数上位机应用中与同一台PLC的通信由一个客户端管理足矣。使用单例模式可以方便地在程序各处获取同一个连接实例。当然如果你的应用需要同时连接多台PLC则需要改造为多实例模式。3.2 基础数据读写方法实现连接建立后最核心的操作就是读写PLC变量。PLC变量在ADS协议中通过“索引组IndexGroup”和“索引偏移IndexOffset”来寻址。对于标准的PLC变量读写我们使用固定的索引组0xF020符号读写或0xF030按地址读写索引偏移则需要通过变量名或地址来获取。首先实现一个通过变量名读取任意类型数据的方法。这里用到了泛型使得代码更通用。/// summary /// 通过变量名读取数据 /// /summary /// typeparam nameT读取的数据类型如 bool, int, double, string 等/typeparam /// param namevariableNamePLC中定义的变量名如 “MAIN.bStart”/param /// param namevalue输出参数读取到的值/param /// returns读取是否成功/returns public bool ReadValueT(string variableName, out T value) { value default(T); if (!_isConnected || _adsClient null) { Console.WriteLine(”[错误] 未建立连接无法读取。“); return false; } try { // 第一步根据变量名获取该变量的句柄Handle int handle _adsClient.CreateVariableHandle(variableName); // 第二步使用句柄读取数据 value (T)_adsClient.ReadAny(handle, typeof(T)); // 第三步释放句柄重要避免资源泄漏 _adsClient.DeleteVariableHandle(handle); Console.WriteLine($”[读取] {variableName} {value}“); return true; } catch (AdsErrorException ex) { Console.WriteLine($”[读取错误] 变量 ‘{variableName}’ AdsError: {ex.ErrorCode}“); // 0x706: 符号未找到检查变量名拼写和作用域 } catch (Exception ex) { Console.WriteLine($”[读取异常] 变量 ‘{variableName}’: {ex.Message}“); } return false; }对应的写入方法/// summary /// 通过变量名写入数据 /// /summary /// typeparam nameT写入的数据类型/typeparam /// param namevariableNamePLC变量名/param /// param namevalue要写入的值/param /// returns写入是否成功/returns public bool WriteValueT(string variableName, T value) { if (!_isConnected || _adsClient null) { Console.WriteLine(”[错误] 未建立连接无法写入。“); return false; } try { int handle _adsClient.CreateVariableHandle(variableName); _adsClient.WriteAny(handle, value); _adsClient.DeleteVariableHandle(handle); Console.WriteLine($”[写入] {variableName} - {value}“); return true; } catch (AdsErrorException ex) { Console.WriteLine($”[写入错误] 变量 ‘{variableName}’ AdsError: {ex.ErrorCode}“); } catch (Exception ex) { Console.WriteLine($”[写入异常] 变量 ‘{variableName}’: {ex.Message}“); } return false; }关键点与避坑指南变量句柄Handle管理CreateVariableHandle和DeleteVariableHandle必须成对出现。每次读写操作都获取并释放句柄是一种简单安全的模式适用于不频繁操作的变量。如果你需要高频读写同一个变量例如每10ms读取一次则应该在程序初始化时获取一次句柄并缓存起来在程序退出时再释放这样可以大幅提升性能。变量名与作用域 变量名需要包含完整路径。例如在MAIN程序PRG中定义的bStart变量其全名应为MAIN.bStart。如果变量在全局变量列表中则直接写变量名即可。最准确的方式是在TwinCAT PLC开发环境中查看变量的“符号信息”。数据类型匹配ReadAny和WriteAny方法依赖于.NET类型与PLC数据类型的正确映射。例如PLC中的BOOL对应C#的boolINT对应shortDINT对应intLREAL对应doubleSTRING对应string。如果类型不匹配会导致读取错误或数据解析混乱。3.3 数组与结构体的读写工业数据中数组和结构体非常常见。TcAdsClient同样支持。读取数组// 读取一个 INT 数组 short[] intArray; int handle _adsClient.CreateVariableHandle(“MAIN.arrValues”); intArray (short[])_adsClient.ReadAny(handle, typeof(short[]), new int[] { 10 }); // 假设数组长度为10 _adsClient.DeleteVariableHandle(handle);ReadAny方法的第三个参数可以指定数组各维度的长度。写入数组short[] newArray new short[] {1, 2, 3, 4, 5}; _adsClient.WriteAny(handle, newArray);读写结构体结构体读写需要先在C#中定义一个与PLC结构体布局完全一致的类或结构并使用[StructLayout(LayoutKind.Sequential, Pack 1)]等特性确保内存对齐一致。假设PLC中有一个结构体ST_DataTYPE ST_Data : STRUCT iValue : INT; bStatus : BOOL; rSpeed : LREAL; END_STRUCT END_TYPE对应的C#类[StructLayout(LayoutKind.Sequential, Pack 1)] public class ST_Data { public short iValue; // INT 对应 short [MarshalAs(UnmanagedType.I1)] // BOOL 在C#中作为byte处理需要特殊标记 public bool bStatus; public double rSpeed; // LREAL 对应 double }读写操作// 读取结构体 ST_Data myData; int handle _adsClient.CreateVariableHandle(“MAIN.stMachineData”); myData (ST_Data)_adsClient.ReadAny(handle, typeof(ST_Data)); _adsClient.DeleteVariableHandle(handle); // 写入结构体 myData.iValue 100; myData.bStatus true; myData.rSpeed 1500.5; _adsClient.WriteAny(handle, myData);这里有个大坑PLC中的BOOL类型是1位但在内存中通常按字节对齐。在C#中bool默认是4字节。直接使用bool类型会导致整个结构体的内存布局错位。解决方案如上面代码所示使用[MarshalAs(UnmanagedType.I1)]特性并配合byte或sbyte类型来映射PLC的BOOL或者使用short等类型然后在代码中进行位判断。更稳妥的方法是对于包含BOOL的结构体在PLC端将其定义为BYTE类型在C#中用byte读取再通过位运算解析这样可以完全避免对齐问题。4. 实战构建一个简单的监控与控制系统4.1 项目UI与业务逻辑设计光有核心类还不够我们需要一个界面来演示和测试。我使用WinForms创建了一个简单的程序主要功能包括连接管理 输入AmsNetId和Port进行连接和断开操作。数据监控 定时读取几个关键的PLC变量如电机转速、温度、启动状态并显示在界面上。控制命令 提供按钮向PLC写入控制命令如启动、停止、设置目标值。在UI层如MainForm.cs我们调用封装好的AdsClientHelper。private AdsClientHelper _adsHelper; private System.Windows.Forms.Timer _readTimer; public MainForm() { InitializeComponent(); _adsHelper AdsClientHelper.Instance; // 获取单例 _readTimer new System.Windows.Forms.Timer(); _readTimer.Interval 500; // 500ms读取一次 _readTimer.Tick ReadTimer_Tick; } private void btnConnect_Click(object sender, EventArgs e) { string netId txtNetId.Text.Trim(); // 例如 “192.168.1.100.1.1” int port; if (!int.TryParse(txtPort.Text, out port)) port 851; bool success _adsHelper.Connect(netId, port); if (success) { btnConnect.Enabled false; btnDisconnect.Enabled true; _readTimer.Start(); // 连接成功后启动定时读取 } } private void ReadTimer_Tick(object sender, EventArgs e) { // 避免在UI线程进行耗时操作此处为演示简化处理。 // 实际项目中应考虑使用异步或后台线程。 bool isRunning; if (_adsHelper.ReadValue(“MAIN.bMachineRunning”, out isRunning)) { lblRunningStatus.Invoke((MethodInvoker)(() { lblRunningStatus.Text isRunning ? “运行中” : “停止”; lblRunningStatus.BackColor isRunning ? Color.LightGreen : Color.LightCoral; })); } double currentSpeed; if (_adsHelper.ReadValue(“MAIN.rCurrentSpeed”, out currentSpeed)) { lblSpeed.Invoke((MethodInvoker)(() { lblSpeed.Text currentSpeed.ToString(“F2”); })); } } private void btnStart_Click(object sender, EventArgs e) { _adsHelper.WriteValue(“MAIN.bStartCmd”, true); // 通常PLC程序里bStartCmd是一个上升沿触发所以写入true后可以再延时写入false或由PLC程序复位。 Task.Delay(100).ContinueWith(_ _adsHelper.WriteValue(“MAIN.bStartCmd”, false)); } private void btnSetSpeed_Click(object sender, EventArgs e) { if (double.TryParse(txtTargetSpeed.Text, out double targetSpeed)) { _adsHelper.WriteValue(“MAIN.rTargetSpeed”, targetSpeed); } }4.2 定时读取与性能优化上面的例子使用了一个System.Windows.Forms.Timer进行定时轮询。这在数据点少、更新频率要求不高如1秒1次的场景下是可行的。但它存在两个问题1) 定时器在UI线程触发如果读写操作耗时较长会卡住界面2) 轮询方式效率较低。优化方案1使用异步读写TcAdsClient的部分方法有异步版本如ReadAnyAsync。我们可以结合async/await来避免UI线程阻塞。private async Task ReadDataAsync() { while (_isReading) { try { int handle _adsClient.CreateVariableHandle(“MAIN.rCurrentSpeed”); object result await _adsClient.ReadAnyAsync(handle, typeof(double)); _adsClient.DeleteVariableHandle(handle); double speed (double)result; // 更新UI BeginInvoke((MethodInvoker)(() lblSpeed.Text speed.ToString(“F2”))); } catch { /* 处理异常 */ } await Task.Delay(200); // 异步等待 } }优化方案2使用ADS通知Notification这是倍福ADS协议的高级功能也是最佳实践。你可以为某个PLC变量注册一个通知回调当该变量的值发生变化时或按一定周期PLC运行时会主动通知你的上位机程序而不是由上位机不停地去问。这极大地减少了不必要的网络流量和PLC的CPU负载。private int _notificationHandle; private void SubscribeToVariable() { try { int variableHandle _adsClient.CreateVariableHandle(“MAIN.bAlarm”); // 添加通知当变量变化时触发循环时间200ms最大延迟时间500ms _adsClient.AddDeviceNotificationEx( variableHandle, new AdsStream(4), // 根据变量类型分配足够大的缓冲区 AdsTransMode.OnChange, // 变化时通知 200, // 循环时间ms 500, // 最大延迟ms OnDeviceNotificationChanged, // 回调函数 null // 用户对象 ); // 注意这里不需要DeleteVariableHandle通知会持有这个句柄。 } catch { } } // 通知回调函数 private void OnDeviceNotificationChanged(object sender, AdsNotificationExEventArgs e) { // e.DataStream 包含变化后的数据 bool alarmStatus e.DataStream.ReadBoolean(); // 读取BOOL值 BeginInvoke((MethodInvoker)(() { lblAlarmStatus.Text alarmStatus ? “报警” : “正常”; })); } // 程序退出时记得移除通知 private void Unsubscribe() { if (_notificationHandle ! 0) { _adsClient.DeleteDeviceNotification(_notificationHandle); } }使用通知机制是实现高效、实时监控的推荐方式。5. 部署、调试与故障排查实录5.1 项目部署与依赖项处理当你将程序开发完成后需要部署到现场的工控机上。除了你的程序本身还需要确保目标机器上具备以下环境.NET Framework 对应版本如4.7.2的运行库。TwinCAT Ads Router 这是ADS通信的基石。通常如果工控机上安装了TwinCAT运行时Runtime或开发环境XAE这个组件就已经存在了。如果没有你需要单独安装“TwinCAT XAR”Ads Router。可以从倍福官网下载。TwinCAT.Ads.dll 将你项目引用的这个DLL文件复制到你的程序输出目录exe所在文件夹。这是最简单可靠的方式可以避免因系统路径不同导致的“无法加载DLL”错误。部署检查清单[ ] 目标系统已安装所需.NET Framework。[ ] 目标系统已安装TwinCAT Ads Router可通过服务列表查看是否有“TcAdsServer”服务。[ ] 程序目录下包含TwinCAT.Ads.dll。[ ] 防火墙已放行ADS通信端口通常为48898、48899具体取决于TwinCAT版本和配置。[ ] 上位机与PLC的IP网络互通且AmsNetId配置正确。5.2 常见错误代码与解决方案速查表在实际调试中你会遇到各种AdsErrorException。下面是一个常见错误代码的速查表能帮你快速定位问题。错误代码 (十六进制)错误常量名 (参考)含义与可能原因排查步骤0x704ADSERR_DEVICE_NOTFOUND设备未找到。1. 检查AmsNetId和Port是否正确。2. 目标PLC的TwinCAT运行时是否已启动进入“运行”模式。3. 网络是否连通ping PLC IP。4. 本地路由表TwinCAT Router是否配置正确。0x706ADSERR_DEVICE_SYMBOLNOTFOUND符号变量未找到。1.检查变量名拼写和大小写这是最常见原因。2. 确认变量是否存在于当前激活的PLC程序中。3. 确认变量的作用域是全局变量还是程序内变量路径是否正确。4. PLC程序是否已成功下载并运行。0x707ADSERR_DEVICE_SYMBOLVERSIONINVALID符号版本无效。1. PLC程序在上位机连接后发生了在线修改并下载导致符号表版本变化。需要重新建立连接或重新获取变量句柄。0x708ADSERR_DEVICE_INVALIDOFFSET无效的偏移地址。1. 使用地址读写索引组0xF030时偏移地址计算错误。2. 变量地址在PLC内存中不存在。0x709ADSERR_DEVICE_SYMBOLNOTACTIVE符号未激活。变量在PLC程序中被声明但未被使用优化掉了或者处于未激活的程序组织单元POU中。0x70AADSERR_DEVICE_ACCESSDENIED访问被拒绝。1. 尝试写入一个只读变量如常量或输入变量。2. ADS通信权限不足检查TwinCAT路由配置。0x750ADSERR_CLIENT_PORTNOTOPEN客户端端口未打开。本地Ads Router服务TcAdsServer可能未启动。检查Windows服务。0x274CADSERR_DEVICE_INVALIDSIZE数据大小无效。1. 读写数据时指定的数据长度与PLC变量实际长度不符。2. 读写结构体或数组时C#与PLC的类型定义内存布局不一致。5.3 连接建立失败深度排查如果Connect方法一直失败可以按照以下流程进行系统性排查基础网络检查确保上位机和倍福控制器或运行TwinCAT的PC在同一个局域网段没有VLAN隔离。在命令行用ping PLC_IP测试物理连通性。如果ping不通检查网线、交换机、IP设置。本地路由配置关键在开发电脑或上位机上打开TwinCAT开发环境或“TwinCAT XAE Shell”。进入“Route”界面路由设置。点击“Add”添加一条路由。在“Address”中输入PLC的IP地址在“Ams Net Id”中输入PLC的AmsNetId如192.168.1.100.1.1。务必点击“Add Route”按钮并输入本地系统的用户名和密码。这一步是将路由信息写入本地系统的ADS路由器很多连接失败都是因为漏了这一步路由没有真正生效。远程PLC状态检查确认PLC的TwinCAT运行时状态。通常可以通过TwinCAT开发环境远程连接到PLC查看或者如果PLC有网页界面登录查看TwinCAT状态是否为“Run”。确认PLC的ADS服务端口默认851是开放的。可以在上位机使用telnet PLC_IP 851命令测试端口连通性需先开启Windows的Telnet客户端功能。防火墙与杀毒软件临时关闭上位机和PLC端的Windows防火墙测试是否能连接。如果可以则需要配置防火墙入站规则放行“TcAdsServer.exe”程序以及相关的端口48898/48899 UDP/TCP851 TCP。检查是否有第三方杀毒软件或工业安全软件拦截了ADS通信。5.4 调试技巧与心得使用TwinCAT System Manager进行“抓包” TwinCAT System Manager自带一个“ADS Monitor”工具。你可以在PLC端或上位机端启动它监控所有ADS通信报文。当你的程序读写变量时在这里能看到详细的请求和响应包括错误码。这是诊断通信问题最强大的工具。从简单变量开始 先尝试读写一个简单的BOOL或INT全局变量确保基础通信链路是通的再逐步测试复杂类型和程序内变量。日志记录 在你的AdsClientHelper中将所有的操作连接、断开、读写请求、错误都记录到文件或数据库而不仅仅是控制台输出。现场出现问题后日志是唯一的“黑匣子”。处理连接中断与重连 工业现场网络可能不稳定。你的程序需要具备断线重连机制。可以在定时读取的异常捕获中检测到连接断开然后尝试重新初始化TcAdsClient并连接。重连逻辑需要谨慎设计避免无限重试刷日志可以加入指数退避策略。资源释放 确保程序退出时调用Disconnect方法并实现IDisposable接口。对于使用通知Notification的情况务必在断开连接前删除所有通知否则可能导致资源泄漏。这个实例源码和详细的解析基本覆盖了C#与倍福PLC通信的绝大多数场景。从最基础的连接、读写到稍复杂的数组结构体、性能优化再到最后的部署调试我把实践中踩过的坑和总结的经验都融了进去。工业通信编程稳定性压倒一切每一个细节的疏忽都可能导致现场生产的停顿。希望这份材料能帮你把路走得更顺一些。如果在实际使用中遇到新的问题不妨回头看看ADS错误码和路由配置这两个往往是问题的根源。本文还有配套的精品资源点击获取