TwinCAT ADS .NET方法调用:把PLC函数变成RPC接口

发布时间:2026/10/14 7:41:49
TwinCAT ADS .NET方法调用:把PLC函数变成RPC接口 1. Sample16 到底在解决什么问题熟悉倍福 TwinCAT 的朋友都知道ADSAutomation Device Specification是倍福自己的一套通讯协议本质上就是 PC 端与 PLC 运行时之间的“数据通道”。平时我们聊 ADS 通讯大多数场景是在读写 PLC 变量——比如把某个REAL或者INT从 PLC 里读出来或者往某个变量里写一个新值。这种操作在 Sample01、Sample02 之类的例子里讲得已经很透了无非是ReadSymbol/WriteSymbol或者按地址读写。但实际项目里有一个特别常见的需求是“按变量读写”解决不了的你不想去操作 PLC 内部的一个变量而是希望直接触发 PLC 里的某段逻辑执行。比如你做了一个上位机程序界面上有个“一键启动配方”的按钮点击之后希望 PLC 里跑一段完整的配方加载流程。如果你通过置位一个BOOL变量来触发就必须在 PLC 侧额外写一段“等待变量置位然后执行”的梯形图或 ST 代码来回沟通很繁琐而且要等扫描周期实时性不够直观。Sample16 提供的正是一种更直接的方式——从 .NET 端把 PLC 里的“方法”当成本地函数来调用。PLC 侧不需要额外写触发逻辑只需要定义好带参数的 Function 或者 Function Block上位机直接传参调用拿到返回值。相当于把 PLC 的一部分功能“RPC 化”了。这篇文章我会把官方 Sample16 的核心逻辑重新梳理一遍不是照抄官方注释而是把每一步背后“为什么这么做”讲透包括我在真实项目里踩过的坑。适合已经有 ADS 基础、但还没深入用过方法调用的朋友如果你连 ADS 通讯的变量读写都还没跑通建议先去把 Sample01 跑一遍再回来看这篇。2. 核心思路拆解为什么方法调用和变量读写不一样要理解 Sample16 的代码关键是要先建立一个新的心智模型。变量读写的模型是“地址 类型”。你告诉 ADS 客户端“我要读地址 0x1234 的一个 4 字节浮点数”ADS 就去那个内存地址把数据给你抓回来。这本质上是个非常简单的内存访问动作和 PLC 程序本身的逻辑没有任何关系。方法调用则完全不是这个套路。方法调用意味着你在触发 PLC 里的程序执行流要有“调用约定”。就像你在 C 语言里调用一个函数编译器会规定参数怎么压栈、返回值放哪个寄存器ADS 方法调用也有自己的一套约定。先记住一个结论PLC 里的方法在 ADS 通讯层看来就是一个特殊的符号Symbol。你在上位机代码里构建一个AdsStream或者 byte 数组按照“参数列表 返回值”的格式把数据填进去然后发起一次带方法名 ID 的写入PLC 端收到之后解析这个流执行方法再把返回值写回来。这整个过程ADS 客户端是不需要重新连接的还是走同一个 ADS 端口只是把方法名转换成了一个特殊的 ID。官方 Sample16 里核心做的事用三句话就能概括获取到 PLC 方法的句柄Handle这个句柄其实就是一个数值 ID代表“我要调用的那个方法”。构建一个输入输出参数的二进制数据流。用ReadAny/WriteAny或者更底层的Read/Write把这坨参数流发过去再收回来。要注意的是这里“方法”分两种Function函数和Function Block功能块。两者在调用方式上有区别。Function 是无状态的输入参数传进去、返回值吐出来完事。Function Block 是有实例的内部有状态变量调用前你需要在 PLC 里声明这个 FB 的实例并且通过 ADS 获取到这个实例的符号路径。Sample16 里着重讲的是 Function 的调用FB 的调用思路类似但多了一步“实例定位”。3. 环境准备与项目初始化先把代码跑起来的准备工作列一遍这部分看起来琐碎但每一步都容易出问题。3.1 硬件与软件版本要求我这边测试用的环境是TwinCAT 3.1.4024 及以上版本推荐 4024 之后的版本符号路径相关 API 更稳定.NET Framework 4.6.1 或 .NET Core 3.1 / .NET 6倍福 ADS 类库官方Beckhoff.TwinCAT.AdsNuGet 包一个可运行的 TwinCAT 项目并且项目里启用了 AMS 路由如果你是离线环境或者公司内网没有 NuGet 源可以去倍福官网下载TwinCAT ADS NuGet Package的离线文件手动引用 DLL。我习惯直接引用 DLL 而不是用 NuGet因为这样出问题时更容易控制版本。注意TwinCAT.Ads.dll和TwinCAT.Ads.AdsRouter.dll要放到同一个输出目录下否则运行时会在 Router 那一步抛DllNotFoundException。3.2 TwinCAT 项目侧的准备光有上位机代码还不够。Sample16 能在 PLC 侧调用方法有个前提PLC 项目里必须声明了可被调用的方法。在 TwinCAT 的 PLC 工程里你可以新建一个FUNCTION或者某个 FB 的方法Method。这里有个细节——我说的 FB 的 Method 和 FB 本身不一样FB 的方法是在 FB 类型内部定义的。比如你定义了一个FB_TemperatureController它内部有一个CalculatePID方法这个方法的全名路径是TwinCAT_Project.TemperatureController.CalculatePID这样。而要在上位机里调用它你需要一点“额外操作”获取 FB 实例的路径。但 Sample16 用的是最朴素的 Function。比如我建立一个全局函数FUNCTION F_Add : REAL VAR_INPUT a : REAL; b : REAL; END_VAR这个函数实现就是简单的F_Add : a b;。建好函数之后需要在 TwinCAT 里重新 Activate Configuration把新符号加载进去PLC 要处于 Running 状态。这一点很容易被忽略——很多新手在代码里跑不通以为是通讯问题结果一看 PLC 根本没有激活配置。3.3 .NET 项目的引用与连接参数在 Visual Studio 里创建控制台应用程序引用Beckhoff.TwinCAT.Ads之后连接代码通常长这样using TwinCAT.Ads; // 1. 创建客户端 AdsClient client new AdsClient(); // 2. 连接本机 TwinCAT 运行时 client.Connect(851); // 本机运行时的默认端口 // 3. 或者连接远程 PLC // client.Connect(new AmsAddress(new AmsNetId(192.168.1.1.1.1), 851));端口 851 是 TwinCAT 3 默认的 PLC runtime 端口号。AMS NetId 如果不填Connect重载会自动使用本机路由。远程连接时要注意AmsNetId 通常是你在 TwinCAT 路由里配置的那个 ID和 IP 不对应别把 PLC 的 IP 直接填进去。4. 关键代码逐段解读从打开连接到拿到返回值这一节我会把 Sample16 的完整调用流程拆开每一行代码都说明意图和背后的坑。4.1 获取符号路径信息在调用方法之前需要先拿到方法的符号路径。TwinCAT 3 的符号路径默认就是 PLC 里函数的完整路径。比如函数F_Add如果在TwinCAT_Project.POOL下了那完整路径就是TwinCAT_Project.POOL.F_Add。但这里有个非常关键的坑ADS 方法调用的符号路径并不是直接填函数名那么简单。在 ADS 通讯协议里方法的调用是通过“符号路径 方法名”组合出来的。也就是说你需要的是方法的路径这个路径会注册到 ADS 符号表里而不是 PLC 对象树里的路径。生成方法路径的规则大致是对象路径 . 方法名。为了稳妥可以用 TwinCAT 自带的TwinCAT.Ads.SymbolInfo或者老牌的AdsSymbolicAccess来动态查询也可以硬编码路径。Sample16 中使用的是硬编码字符串原因很简单——示例程序不需要应对动态场景。我在实际项目中遇到过路径大小写的问题。PLC 里的函数名是大小写敏感的方法路径也是。比如 PLC 里定义的是F_Add你写路径时写成f_addADS 查询时不会报错但你会拿不到句柄或者调用时行为异常。所以建议在写代码之前到 TwinCAT 的“符号”窗口确认一下准确的符号名称。TwinCAT 3 的System Manager - PLC的Symbols页签里能看到带完整路径的符号表截图对比一下比自己盲猜靠谱太多。4.2 创建 AdsStream 组装参数ADS 方法调用的数据本质上是一段“序列化之后的参数”。Sample16 的做法是构造一个AdsStream也就是MemoryStream的 ADS 包装按照顺序写入方法的输入输出参数然后用AdsBinaryReader/AdsBinaryWriter来解析。看官方示例中调用一个F_Add的过程核心代码大概是// 1. 组装传入参数 using (AdsStream dataStream new AdsStream(64)) { AdsBinaryWriter writer new AdsBinaryWriter(dataStream); writer.Write(a); // 第一个输入参数 writer.Write(b); // 第二个输入参数 // 此时 stream 的 Position 停留在参数末尾 // 2. 发起调用 int result client.ReadAnyint(methodHandle, dataStream); }这里有个容易糊涂的点为什么是ReadAny而不是WriteAny因为 PLC 方法调用的流程是——上位机把参数先“推”给 PLC然后 PLC 执行方法再把返回值“拉”回来。ADS 协议里这个过程被封装成了一个ReadAny带着输入参数流去读返回值很反直觉但就是这么设计的。你要是直接用WriteAny就只是把参数写进去verification 拿不到结果。AdsStream分配的缓冲区大小也很讲究。太小了会报InsufficientBufferException太大了也没必要。Sample16 里分配 64 字节看起来随意其实是为了容纳一个REAL REAL的输入参数和可能的返回值足够了。但如果你的方法有多个字符串参数64 字节必然不够建议根据参数列表计算一下每个REAL/INT/DINT占 4 字节STRING(80)占 82 字节80 字符 2 字节长度头BOOL占 1 字节但要对齐。4.3 获取方法句柄Method Handle为什么方法调用需要句柄类比一下你要在通讯层描述“我要调用 PLC 里的那个函数”如果每次都传一串长长的字符串路径ADS 协议报文会变得臃肿而且解析效率低。所以 ADS 的做法是——第一次通讯时根据路径拿到一个 4 字节的整数句柄之后每次调用只需要传这个整数 ID 就行。Windows 编程里到处都是这种“句柄化”思想文件句柄、窗口句柄都一个路子。Sample16 里获取句柄的代码如下uint methodHandle 0; using (AdsStream symbolStream new AdsStream(1024)) { AdsBinaryReader reader new AdsBinaryReader(symbolStream); int result client.ReadSymbol(methodPath, symbolStream); methodHandle reader.ReadUInt32(); }这里有个细节ReadSymbol读出来的是一段二进制数据对于方法来说前 4 字节就是句柄。你把这个值保存起来之后每次调用方法就复用这个值。官方示例里methodHandle是个局部变量只获取一次这是对的——句柄在 PLC 项目激活周期内是稳定的除非你重新 Activate Configuration否则不用重新获取。在一次完整项目中建议把“获取句柄”放在初始化阶段只执行一次。如果你在 UI 线程里每次点击按钮都重新获取句柄不仅浪费通讯带宽还有可能因为非线程安全的并发访问导致句柄错乱。4.4 调用方法与解析返回值拿到句柄之后真正的调用动作反而很简单。关键是理解ReadAny/Read返回的字节流如何还原成返回值。看下面这一段object[] resultArgs; int ret client.ReadAny(methodHandle, dataStream, out resultArgs);ReadAny有多个重载最省事的是传入AdsStream和接收结果的对象数组。它会自动把 PLC 返回值按顺序填到resultArgs里第一位是返回值本身对 Function 而言后面依次是VAR_IN_OUT或者引用的输出参数。对于带多个输出参数的方法比如一个方法同时返回温度和湿度官方 Sample16 用的方式是把输出参数直接在AdsStream的“参数段”里预留位置通过二进制读写来拿。实际开发中你也可以用ReadAnyT泛型版直接拿到单个返回值但多返回值时就得回到AdsBinaryReader慢慢解析。我自己用下来的感受是把调用方法的代码封装成一个泛型工具类比如TwinCATMethodInvoker.InvokeMethodT(client, methodPath, params object[] args)会省掉很多重复劳动。下次换项目只要把方法路径和参数对象数组传进去内部帮你搞定句柄缓存和返回流解析谁用谁知道。5. 多返回值与复杂参数类型的处理Sample16 本身举的例子比较简单但真实项目里几个方法返回多个值太常见了。这里单独讲一下从 “1 个返回值” 到 “多个返回值” 升级时会遇到的坑和强制注意事项。5.1 VAR_IN_OUT 参数就是“多个返回值”的本质PLC 里的 Function 除了VAR_OUTPUT可以声明输出参数VAR_IN_OUT还可以声明“引用参数”。引用参数的语义就是你在内部修改它调用结束后外部拿来用。这一个机制天然就是多返回值通道。举个例子FUNCTION F_GetTemperatureStatus : BOOL VAR_INPUT sensorIdx : INT; END_VAR VAR_OUTPUT temperature : REAL; errorCode : WORD; END_VAR要拿到temperature和errorCode在 ADS 调用时你的参数流里不仅要包含sensorIdx这个输入还要为temperature和errorCode预留空间。调用结束后返回流里也是同样顺序。用代码表示AdsStream adStream new AdsStream(64); AdsBinaryWriter writer new AdsBinaryWriter(adStream); writer.Write((short)sensorIdx); // 输入参数 writer.Write((double)0); // 占位temperature writer.Write((ushort)0); // 占位errorCode uint handle GetMethodHandle(client, MAIN.F_GetTemperatureStatus); object[] results; client.ReadAny(handle, adStream, out results); float temp (float)results[0]; // 注意 PLC REAL 对应 .NET float不是 double ushort err (ushort)results[1];5.2 类型映射对照表——最容易翻车的地方类型错位是我见过最多的错误。PLC 里REAL是 4 字节单精度浮点很多从 Python / Java 过来的人第一反应是映射成 C# 的double结果高位低位错位读出来的数据完全不对。这里把常用类型对照表列出来建议存一份在代码注释里PLC 类型.NET 类型字节数注意事项BOOLbool1注意对齐字段间可能有填充BYTE / USINTbyte1无符号WORD / UINTushort2无符号 16 位DWORD / UDINTuint4无符号 32 位INTshort2有符号DINTint4有符号REALfloat4别写成 doubleLREALdouble864 位浮点STRING(80)string822 字节长度前缀 80 字节内容ARRAY原始字节流取决于元素需要手动解析循环5.3 方法调用参数个数过多时的缓冲区规划当参数很多的时候AdsStream初始大小就别再拍脑袋了。比如你有 5 个REAL输入和 3 个REAL输出总共 8 * 4 32 字节加返回值 4 字节给 64 字节就安全。但如果你有字符串比如STRING(80)的配置名占用 82 字节一不小心就InsufficientBuffer。我习惯写一个小的计算辅助方法预估最大缓冲区大小private static int CalcStreamSize(IEnumerableint paramSizes) { int total 0; foreach (int size in paramSizes) { total size; } return Math.Max(64, total 16); // 16 预留 }比手工硬编码安全多了。6. 实操过程实录从建函数到上位机调用一次跑通这一部分是完整的操作流水账按顺序照做可以在十分钟内跑通 Sample16。我把它当成“厨房菜谱”来讲。6.1 PLC 侧操作定义一个可调用的函数打开 TwinCAT XAE新建或打开一个 PLC 项目。在POUs下添加一个Function命名为F_Add。在VAR_INPUT中声明a : REAL; b : REAL;函数体写F_Add : a b;编译通过后Activate Configuration确保 PLC 状态是Run。这里我踩过的一个小坑如果你把函数建在某个 Program 的局部 POU 里比如建在MAIN下面符号路径会变成MAIN.F_Add之类写代码时就要对应好。全局 POU 里定义的路径通常更干净建议一开始统一定义在全局 POU 下。6.2 上位机侧操作完整代码骨架using System; using TwinCAT.Ads; class Program { static void Main(string[] args) { int port 851; string methodPath TwinCAT_Project.GVL.F_Add; using (AdsClient client new AdsClient()) { client.Connect(port); // 获取方法句柄 uint methodHandle client.ReadSymbolHandle(methodPath); float a 1.5f; float b 2.5f; using (AdsStream paramStream new AdsStream(64)) { AdsBinaryWriter writer new AdsBinaryWriter(paramStream); writer.Write(a); writer.Write(b); object[] results; int resultCode client.ReadAny(methodHandle, paramStream, out results); if (resultCode 0) { float sum (float)results[0]; Console.WriteLine($F_Add({a}, {b}) {sum}); } else { Console.WriteLine($ADS Error: 0x{resultCode:X8}); } } } } }6.3 常见运行结果与排查速查表现象可能原因解决措施0x700A错误方法句柄无效或过期重新 Activate Configuration 后重新获取句柄0x7100错误目标设备不存在检查 AMS NetId 和端口号返回 NaN 或异常值PLC 端函数报错或类型映射错误检查函数输入输出数据类型和 C# 类型对应关系抛InsufficientBufferExceptionAdsStream 初始大小不够动态扩展或预估参数总长PLC 端函数不执行但没返回错误方法路径不正确去 TwinCAT 符号表里复制完整路径7. 常见问题与排查技巧实录Sample16 本身很干净但把方法调用放进真实项目里你大概率会遇到下面这些问题。整理一下我自己的排查结论。7.1 为什么我总是拿不到句柄Handle拿不到句柄绝大多数原因是方法路径不对。这里有个排查技巧TwinCAT 3 里可以在Online - Symbol Browser直接浏览当前激活的符号表。找到你要调用的函数右键能看到完整路径。把它复制到代码里然后打印出来和那个路径逐字比对。还要检查一下是不是大小写问题。ADS 的路径匹配是严格区分大小写的这点栽过的人不少。比如F_Add你写成f_addADS 会正常返回一个句柄但这个句柄是无效的不对更常见的是直接返回错误码。7.2 PLC 重新激活后方法调用失效这是个经典的坑你上午 Activate Configuration 跑了半天下午改了 PLC 一个变量又激活了一次留意没有重新获取句柄——完了所有方法调用突然全部失败并报0x700A。原因是每次 Activate ConfigurationADS 符号表都会重建之前的句柄对应的已经不是同一个方法了。解决办法只有一个在 PLC 状态切换或者配置重新激活后强制重新枚举方法句柄。我一般是在上位机里监听RouterNotification检测到配置变更自动重新初始化句柄缓存。7.3 Function Block 方法的调用差异官方 Sample16 是 Function 的调用但很多实际场景——比如控制轴、读取设备状态——用的是 Function Block 的方法。FB 的方法调用需要在路径里包含“实例”信息。假设你有个 FB 类型FB_Axis它有个方法MoveToPosition。在 MAIN 里有个实例Axis1。那调用路径不是FB_Axis.MoveToPosition而是MAIN.Axis1.MoveToPosition。因为这个方法是一个实例方法不是静态函数。如果你只写了类型名ADS 根本找不到目标。还要注意FB 方法的调用不能走“无状态”的假设——同一个实例方法如果多次并发调用PLC 端不保证顺序TSOTwinCAT 同步这块要自己规划。但最常见的单实例、单线程调用这个问题影响不大。7.4 性能调用频率多少算合适ADS 方法调用虽然方便但比变量读写要多一层“流解析 方法调度”的开销。一般来说每秒钟几百次调用没问题但如果是控制闭环里需要高频刷新的数据还是建议用变量读写加循环缓冲。我见过有人把 PV 控制的每个周期都走方法调用结果 CPU 占用高得离谱改成变量读写后负载跌了 70%。方法调用适合偶发性的事件触发不适合高频率数据交换。7.5 引用类型参数的不确定性PLC 里的VAR_IN_OUT引用参数在方法调用时你可以传变量名给 PLC 吗ADS 层不支持这样“按名引用”你得把值写进参数流PLC 按值拷贝进去。如果你需要“回调式”改变 PLC 内部变量值还得靠方法内部的逻辑实现。理解这一点很重要——不要指望方法调用能替代所有变量读写场景。8. 我的实操体会与扩展建议Sample16 提供的并不是一个“花哨”的示例它更像是一把钥匙打开了“上位机与 PLC 逻辑交互”的一扇门。我实际用过方法调用之后最大的体会是它把很多原本需要“约定变量名 轮询布尔标志”的土办法变成了一种更干净、更可控的调用方式。尤其在系统联调阶段别人还在纠结某个位有没有置到位你已经直接拿到方法返回值了。如果你接下来想继续深挖有几个方向我认为很实用把方法调用接口封装成interface再通过工厂统一创建上层业务完全不感知通讯细节。这样后续换用倍福的OPC UA通讯或者换成别的品牌的 PLC业务层不需要改动。注意卸载时的资源释放AdsClient用using或者显式Dispose()不然在频繁断线重连的场景下可能撑爆连接池。如果你的 PLC 方法内部有异步操作比如启动一个轴然后等待到位建议方法本身不要让 PLC 阻塞等待而是采用“启动 返回立即结束”的拆分模式上位机再通过单独的状态变量轮询或订阅通知来判断完成。否则上位机调用这个方法的线程会被 PLC 的执行时间卡住。最后一个建议把官方 Sample16 的源码自己重写一遍不要复制粘贴。重写的过程中你会发现很多细节是官方注释里根本没有讲的比如 ReadAny 和 Read 的差异、句柄的缓存时机、参数流的顺序敏感性。这些是自己踩一遍才会有肌肉记忆的东西。能把这些细节控制住倍福 ADS 的开发你就算入行了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询