ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TwinCAT3与C++上位机ADS异步通信:从原理到工程落地

TwinCAT3与C++上位机ADS异步通信:从原理到工程落地 搞工控上位机的人十有八九都会遇到这个问题TwinCAT3的实时核跑得飞快1毫秒甚至更短周期地在刷数据但你的C上位机却在Windows里忙着绘曲线、存数据库、处理UI两边完全不是一个节拍。如果还用老办法去循环读取PLC变量要么CPU烧得离谱要么数据延迟大到没法看。我最早做这套东西的时候也踩过不少坑后来把基于ADS的异步通信方式吃透了才算真正把这层窗户纸捅破。这里说的ADS不是搞射频仿真那个ADS而是倍福TwinCAT体系里的Automation Device Specification这是Win32上位机与实时核之间交换数据的标准协议。TwinCAT3作为实时控制系统C作为上位机语言ADS作为连接通道异步通知作为数据流向核心机制这套组合几乎是目前做非实时上位机与实时控制器通信最实用的方案之一。这篇东西适合正准备做TwinCAT3上位机开发的电气工程师、软件工程师也适合明明照着官方例程写了一大堆代码但通信还是卡顿、掉线、数据对不上的朋友。我把从通信模型到工程落地的完整思路以及实际调试中遇见的那些坑一次讲清楚。1. 为什么要用异步通信先从实时核和上位机的“时差”说起1.1 单机周期和数据交换的本质TwinCAT3实时核本质上是一个运行在Windows底层、由微软补丁授权调度的独立实时环境。实时任务不依赖Windows的线程调度机制它有自己的任务周期比如伺服轴控制通常是1ms或更短。这意味着PLC侧变量每一毫秒都在变化而Windows侧进程的线程调度粒度一般在几毫秒甚至更差两者天然存在时间尺度上的错位。要把这两个世界连起来ADS协议是中间的翻译官。ADS不是简单的网口透传它基于AMSAutomation Message Specification报文在TwinCAT的Server端维护着一套符号表和句柄机制上位机可以通过符号名拿到变量的访问权。通信链路一旦建立数据就不只是“能通”还要保证时序、保证一致性甚至要在不同周期任务之间做安全隔离。但很多人写ADS通信时第一个想法就是开个while循环每隔几毫秒去读一次PLC数据。这种做法在变量少、实时性要求不高的Demo里确实能跑一旦变量多了、刷新快了问题就接踵而至。CPU占用大幅上升、Windows线程被卡住、通信偶尔超时甚至因为频繁请求导致Ads服务端响应不过来。1.2 同步轮询为什么不可靠同步轮询的问题不在于“请求—响应”这个模式本身而在于工业现场的数据变化往往是突发性的。你去查一个数据点时它可能已经变了10次也可能一次都没变而你白白占用了带宽。更致命的是同步请求会让Windows侧线程阻塞在等待响应的过程中这时候如果TwinCAT实时核任务因为抖动或优先级问题响应慢了你的上位机线程也会跟着一起卡。举个例子用AdsSyncReadReq去读一个1ms周期任务的变量如果上位机每5ms轮询一次理论上平均延迟在2.5ms左右看起来可以接受。但实际上Windows不是实时系统线程在内存访问、硬盘IO、GC如果是C#等情况下都可能被挂起一旦发生调度延迟轮询间隔可能从5ms变成15ms甚至50ms。实时核那边数据早就不知道刷到哪一帧了。这里还不说多个变量顺序读取时请求队列排队导致的累计延迟。所以用同步方式去追赶实时核的节奏方向就是错的。正确思路应该是让实时核在数据变化的时候主动通知你而不是你反复去问。1.3 异步通知模式从“主动问”变成“被动听”ADS异步通知Notification机制恰好解决了这个问题。你给ADS服务端注册一个通知告诉它“我关心这个变量你按照我指定的周期把数据发给我”。服务端收到注册请求后会在实时环境或ADS中间层按照设定周期采集数据然后通过异步消息通道推送给客户端。这个模式的好处在于客户端线程不用阻塞等待响应只需要在回调里处理推过来的数据。回调触发时机由ADS服务端决定数据到达时一定是“新鲜的”。客户端可以一边接收数据一边干其他事完全没有同步等待的阻塞风险。等你真正把几个上千点数组的实时波形刷起来之后会发现异步模式在CPU占用和延迟稳定性上几乎是碾压同步轮询的。2. ADS通信模型拆解Server/Client到底谁是谁2.1 通信双方与信息走向ADS协议里的Server和Client和平时写Web服务时想的Server/Client不完全一样。TwinCAT实时系统这边是ADS Server它维护实时变量、提供符号访问和通知功能。你的C程序是ADS Client通过调用TcAdsDll的API建立连接、声明变量、注册通知、接收回调。整个通信链路可以简化成三层。底层是AMS路由器负责把报文从Windows的TCP/IP路由到TwinCAT实时核中间层是ADS服务端负责解析请求并访问PLC符号表顶层就是你的C客户端程序通过TCP端口连接AMS路由器发送ADS指令。数据流向是双向的Client可以读、可以写也可以注册通知。写操作也可以做成异步但实际项目中同步写更常见因为写入频率通常比读取频率低很多。这里有一个常见的误区很多人以为用ADS通信就得装TwinCAT XAE开发环境或者至少装个运行时授权。其实对于纯上位机开发只要目标机器上有TwinCAT 3.1 runtime哪怕是当前开发电脑上启动的本地运行时并且能通过路由访问C客户端程序就可以正常工作。你需要的最核心依赖就是TcAdsDll.dll以及对应的头文件和库文件。2.2 AMS NetId、端口号和路由配置的关键性ADS通信能不能建立99%的坑都出在AMS NetId和端口号上。AMS NetId是TwinCAT节点的唯一标识长得很像IP地址但每一段是独立的比如192.168.250.1.1.1。你运行TwinCAT XAE时系统会自动分配一个NetId通常和本机第一个网卡IP有关但并不是一回事。客户端想要连接Server必须知道两件事目标机器的AMS NetId以及目标ADS端口号。TwinCAT3系统中端口851通常是PLC Runtime的ADS端口852是NC运动控制的端口853是NC IO的端口。如果你连接的是本机运行时NetId就是本机的AMS NetId如果连接远程工控机还需要在TwinCAT路由设置里把远程设备的路由信息加进去否则客户端的请求根本传不过去。端口和NetId是通信的“门牌号”写错一个字母都直接失败。我见过不少人拿着官方样例代码把NetId写成了本机IP地址或者没有在路由视图里添加Server地址结果一直报“ADS Server not found”。排查这类问题第一反应应该是检查这两个参数而不是怀疑代码逻辑。2.3 为什么C适合做ADS客户端C做ADS客户端相比C#和LabVIEW最大的优势就是没有托管环境的管理开销。Windows下调用TcAdsDll直接走原生API数据从到达网卡到回调函数触发中间没有GC停顿、没有跨语言封送开销延迟更可控。对于高频数据采集场景比如几十个变量、1ms周期同时刷新C版本能稳扎稳打吃下来。而且C和TwinCAT3的底层亲和度很好。TwinCAT本身用C开发ADS接口C客户端可以直接拿到接近底层的句柄操作方式甚至能对TcAdsDll做二次封装。如果你团队同时有PLC工程师和软件工程师C上位机也方便维护和长期迭代。当然代价就是你得自己管理内存和句柄生命周期但这些在工业软件场景里完全可以接受。3. 核心环节实现从PLC侧准备到C客户端落地3.1 PLC侧变量准备实时任务的“数据窗口”先看PLC侧。要给别人访问的数据不能随手放在某个FB的局部变量里。最方便的做法是在一个全局变量列表GVL里定义所有需要交换的变量并且取清晰、可预测的名字。比如VAR_GLOBAL bCommandStart : BOOL; // 启动命令 bRunning : BOOL; // 运行状态 nCurrentPos : DINT; // 当前位置 dTargetVelocity : LREAL; // 目标速度 arrWaveform : ARRAY[0..99] OF LREAL; // 波形数据 END_VAR这些变量会在TwinCAT的符号表中生成对应的符号名C客户端就是通过类似GVL.bCommandStart这样的字符串来访问它们的。注意命名别带中文、别带奇怪的字符ADS虽然支持Unicode符号但跨平台和跨版本时容易出幺蛾子老老实实用英文字母加下划线最稳妥。同时在PLC侧要把任务周期控制好。如果你的变量是给上位机做监控的通常放在一个10ms或者50ms周期的任务里就够了如果是伺服轴位置跟踪可能放到1ms任务里。这里的周期会直接关联到异步通知里你设置的NotificationCycleTime两者不匹配时会有一层隐性同步损耗后面参数部分再细讲。3.2 VS2019工程配置与TcAdsDll接入开发环境建议用Visual Studio 2019或2022C桌面开发组件。你先从TwinCAT安装目录找到TcAdsDll相关文件一般在C:\TwinCAT\AdsApi\TcAdsDll\下里面包含TcAdsDll.dllTcAdsDll.libads.h、tcadsdef.h等头文件在VS工程里做三件事项目属性-VC目录-包含目录添加头文件路径链接器-常规-附加库目录添加lib所在路径链接器-输入-附加依赖项添加TcAdsDll.lib。运行时记得把TcAdsDll.dll放到exe同目录或者放到系统PATH里。配置好之后把工程字符集设为“使用多字节字符集”或统一用UTF-8避免在传递变量名时出现字符编码不一致导致符号找不到。这一步看似简单实际现场出错率极高尤其在不同语言版本的Windows上窄字符串和宽字符串混用会让符号名寻址失败回头看整个代码没有任何语法问题就是连不上。3.3 异步通知监听主程序代码骨架下面给一个最简可用的异步通知读取示例基于TcAdsDll的C接口风格便于看清完整流程。实际项目里不建议裸写这么多API但理解这个骨架比封装重要得多。#include windows.h #include stdio.h #include tcadsdef.h #include tcadsdll.h // 通知回调ADS服务端推送数据时触发 long __stdcall NotificationCallback(AmsAddr* pAddr, AmsNotificationHeader* pNotif, uint32_t userData) { if (pNotif pNotif-nDataSize sizeof(double)) { double value 0.0; memcpy(value, (uint8_t*)pNotif sizeof(AmsNotificationHeader), sizeof(double)); printf(CurrentSpeed: %.3f\n, value); } return 0; } int main() { long err 0; AmsAddr serverAddr; uint32_t port 0; // 1. 打开ADS端口 port AdsPortOpenEx(); if (port 0) { printf(AdsPortOpenEx failed\n); return -1; } // 2. 设置服务器AMS NetId和端口 err AdsGetLocalAddressEx(port, serverAddr); serverAddr.port 851; // PLC Runtime端口 // 3. 连接测试获取服务器状态 uint16_t state 0; err AdsSyncReadStateReqEx(port, serverAddr, state, nullptr); if (err ! ADSERR_NO_ERR) { printf(Server connect failed, err0x%X\n, err); AdsPortCloseEx(port); return -1; } // 4. 获取符号句柄 uint32_t symHandle 0; err AdsSyncReadWriteReqEx(port, serverAddr, ADSIGRP_SYM_HNDBYNAME, 0, sizeof(symHandle), symHandle, GVL.dTargetVelocity, 0, nullptr); if (err ! ADSERR_NO_ERR) { printf(Symbol handle failed, err0x%X\n, err); AdsPortCloseEx(port); return -1; } // 5. 注册异步通知 uint32_t notifyHandle 0; AmsNotificationHeader notifHeader[BASE_NUM_NOTIFICATIONS]; err AdsAddNotification(port, serverAddr, ADSIGRP_SYM_VALBYHND, symHandle, ADSTRANS_SERVERCYCLE, 10, notifyHandle, (AdsNotificationFuncEx)NotificationCallback, 0, notifHeader); if (err ! ADSERR_NO_ERR) { printf(AddNotification failed, err0x%X\n, err); AdsPortCloseEx(port); return -1; } printf(Notification registered, waiting... Press ENTER to exit.\n); getchar(); AdsDelNotification(port, notifyHandle); AdsPortCloseEx(port); return 0; }注意这里用到了ADSIGRP_SYM_HNDBYNAME通过符号名获取句柄再用ADSIGRP_SYM_VALBYHND对句柄做通知注册。这是相对底层但通用的做法。如果你用的是较新的TcAdsDll版本官方还提供了AdsGetSymHandleByName这样的高层封装底层原理是一样的。回调函数里要注意不能做重活不要写文件、不要弹窗、不要调用阻塞API否则数据推送会积压。正确姿势是把数据拷到自己管理的内存池或队列里由另一个工作线程去消费。3.4 数据写入与请求回调除了读取上位机经常还需要往PLC写命令和参数。写入建议用同步写因为写入操作频率低、实时性要求相对宽松而且同步写能立即拿到错误码便于确认写入是否成功。核心代码如下// 写BOOL型变量 bool bStart true; uint32_t hRoot 0; err AdsSyncReadWriteReqEx(port, serverAddr, ADSIGRP_SYM_HNDBYNAME, 0, sizeof(hRoot), hRoot, GVL.bCommandStart, 0, nullptr); if (err ADSERR_NO_ERR) { err AdsSyncWriteReqEx(port, serverAddr, ADSIGRP_SYM_VALBYHND, hRoot, sizeof(bool), bStart); }这里有个经验对BOOL变量写入时不要直接写一个bool类型就完事要确认目标PLC变量类型。TwinCAT3的BOOL在内存中占1字节但某些版本会做对齐最好先读一遍再写或者用和PLC类型严格对应的类型。写入数值类变量INT、DINT、REAL、LREAL时对应好位宽REAL是4字节LREAL是8字节写错会导致数据变成乱七八糟的数。异步通知的回调线程是TcAdsDll内部管理的线程上下文由DLL自动创建。如果你要在回调里更新GUI界面比如MFC或Qt不能直接操作UI控件否则线程冲突轻则界面卡死重则直接崩溃。标准做法是把回调里的数据丢到一个线程安全队列UI线程通过定时器取数据来刷新。4. 参数、性能与细节决定通信质量的不只是代码4.1 周期选择与抖动控制ADS异步通知的周期参数放在AdsAddNotification的第三个参数在示例里是ADSTRANS_SERVERCYCLE和第四个参数10单位毫秒里。ADSTRANS_SERVERCYCLE表示按Server的周期扫描来触发ADSTRANS_CLIENTCYCLE表示按Client侧周期触发还有ADSTRANS_ONCHANGE表示变量变化即触发。实际使用中周期值要大于或等于PLC任务周期否则通知频率会超过实时任务刷新次数产生大量重复数据。比如PLC任务周期是2ms你设置通知周期1ms那么至少有一半回调读到的是同一帧旧数据白白浪费带宽。推荐把通知周期设置成PLC任务周期的整数倍2倍或5倍甚至10倍视监控需求而定。抖动方面如果发现回调触发时间间隔忽大忽小先检查Windows侧有没有开启电源节能模式再把网络优先级调高。对于本机通信这种抖动一般很小对于远程通信走的是TCP/IP和AMS路由抖动就取决于交换机和服务端负载了。如果要做精确波形采集建议在PLC侧给数据打时间戳上位机以时间戳对齐波形而不是依赖回调到达时间。4.2 符号映射与命名规范用符号名访问变量确实方便但每次通过ADSIGRP_SYM_HNDBYNAME获取句柄都会产生一次请求。如果变量很多比如有上百个建议程序启动时一次性获取所有句柄并缓存起来运行过程中直接用句柄访问避免频繁字符串解析。还有一个容易被忽略的点TwinCAT3符号名大小写敏感。PLC里定义的是nCurrentPos你写成ncurrentpos句柄查询会失败。符号名里的数组访问也有一套规则比如读整个数组和读数组中某个元素用到的符号名带下标方法不一样官方文档里的写法是arrWaveform[0]这样。但建议只访问整个数组再用指令集把需要的元素抠出来效率更高。另外如果你在PLC里对某个结构体变量做了AT地址重映射或者使用了带命名空间的库功能块符号名可能会带上库前缀比如LibName.FBName.VarName。这种情况下最稳妥的方法是在TwinCAT XAE的符号表视图里查看准确的符号名复制出来直接用不要自己手拼。4.3 线程亲和性与回调线程处理TcAdsDll的回调线程由DLL内部创建默认线程优先级不算高。如果你发现高频通知时数据丢失可以尝试手动降低通知频率而不是拼命提高回调处理速度。因为ADS通知机制本身在数据量过大时有内部队列消费不过来时会丢包。工业控制场景丢几帧监控数据通常可以接受但如果数据是用于闭环控制建议把关键控制逻辑放PLC上位机只做监控和记录千万不要用Windows侧做实时闭环。如果你同时开了多个通知注意回调线程可能被复用不保证同一变量的两次回调必然在同一个线程里。因此回调里访问的共享数据必须加锁或用原子操作。我通常会在初始化时创建一个环形缓冲区所有回调都往缓冲区写数据消费者线程定时取数这样既不阻塞回调也避免频繁加解锁。4.4 同步/异步混用场景实际项目里很少只用一种通信模式。我的习惯是命令类、参数类的写入用同步写状态监控和波形采集用异步通知周期性的慢速数据汇总用同步读。这样组合的好处是关键写入能快速知道结果高频采集不阻塞主流程低频数据又不占通知资源。混用时要特别注意句柄的生命周期管理。同一个变量的句柄可以同时用于同步读写和注册通知但如果一个线程在写、另一个线程在回调里读同一块内存需要自己做数据一致性保护。最简单的手段是把读取到的数据拷贝到独立结构体保证每次回调都是完整的一份数据快照。5. 常见问题与排查技巧实录5.1 典型报错与解决速查表错误码/现象可能原因解决办法0x70000ADSERR_CLIENT_PORTNOTOPEN没有先调用AdsPortOpenEx检查端口初始化逻辑0x70100ADSERR_DEVICE_SRVNOTFOUNDAMS NetId或IP路由错误核对Server地址和路由配置0x71001ADSERR_DEVICE_SYMBOLNOTFOUND符号名拼写错误或不带前缀从符号表复制准确符号名0x9800TCP连接失败远程目标没有运行TwinCAT Runtime确认目标机器授权和运行状态回调频率远低于设定通知队列溢出或回调处理过慢降低频率、提升回调处理速度程序退出时崩溃没有删除通知就关闭端口先AdsDelNotification再AdsPortCloseEx这个表我每次做新项目都会扫一遍90%的通信问题都能在这里找到对应项。尤其是0x71001新手最容易踩因为TwinCAT3的符号名有时会在前面自动拼接一个MAIN.或其他任务名前缀你自以为是全局变量实际符号名是MAIN.GVL.nCurrentPos这样冗长的形式。5.2 找不到服务器或端口占用排查“Server not found”时分三步走。第一步确认本机和目标机的TwinCAT版本一致或兼容3.1.4024和4022的某些API行为有差异第二步在TwinCAT System Manager的Routes视图里添加目标设备把AMS NetId和IP地址填对第三步用Telnet或者TwinCAT自带的Router Diagnostics工具测试851端口的连通性。如果是本机连接最简单的方式是先用AdsGetLocalAddressEx拿到本机NetId打印出来和PLC的AMS NetId对比。不要想当然地觉得本机的NetId一定是“127.0.0.1.1.1”那只是默认安装时的一种情况。很多工控机装双网卡NetId可能会绑定到业务网卡上和你以为的本机IP完全不是一回事。端口占用方面851/852是固定端口理论上不会冲突但如果同一台机器上跑多个TwinCAT实例或者有别的软件占用了TCP 48898端口AMS路由器默认端口就会连接失败。这类问题看Windows事件日志能快速定位。5.3 数据不一致、偶发超时问题数据不一致最常见的原因是数组数据跨帧更新。ADS通知一个数组变量时底层会一次性发送整段数据正常来说不存在撕裂。但如果你在回调里拿到的是大数组指针PLC侧在同一个周期里又同时更新了这组数据边缘可能受任务周期影响。真正要避免的是读一个数组的某个元素同时又读该数组的另一个元素两个句柄在不同时间到达组合出来的数据对应不上同一帧时刻。偶发超时则通常和Windows电源管理有关。如果你用的是笔记本开发默认的“平衡”电源模式会降低CPU频率和网络设备性能。做工业通信调试时把电源模式设为“高性能”网卡里关闭节能以太网会极大减少超时次数。还有一个高级技巧如果超时发生在高强度数据写入时可以适当降低写频率把多次写请求合并成一次AdsSyncWriteReqEx写结构体。TwinCAT支持一次写入一段连续地址把多个变量放入一个结构体里写一个结构体相当于写一批变量效率能提高一个数量级。5.4 调试利器最后分享几个调试工具都是我把通信调通全靠它们撑起来的。第一个是TwinCAT自带的信息系统InfoSystem可以实时查看ADS端口状态、通知数量和连接数。第二个是Wireshark的ADS协议解析插件虽然不常用但遇到诡异的报文问题时能帮大忙。第三个是TcAdsDll自带的示例代码位于TwinCAT安装目录下的AdsApi\TcAdsDll\Samples里面有几个现成的C工程哪怕不用它的代码结构里面关于句柄和通知参数的写法也足够当字典查。如果是远程通信我还会在PLC里加一个简单的诊断变量记录最近一次ADS通信的错误码和时间戳。这样当上位机那边报“通信超时”时可以从PLC侧确认是根本没收到请求还是请求到了但响应没有返回。很多模糊问题一瞬间就有答案了。注意ADS异步通知的注册者在退出程序时必须先删除通知再关闭端口顺序反了极其容易出现回调函数访问已释放内存的问题。这种崩溃是偶发的最恶心上线前一定要重点验证退出逻辑。从我实际操作的项目来看ADS异步通信最适合的场景是“PLC做实时控制上位机做监控和管理”而不是拿C去替代PLC做闭环。把周期设置成PLC任务的整数倍回调里只做数据快照不做重计算命令写入保持同步退出时严格销毁通知这套流程走下来基本不会出大问题。如果后续有跨平台需求可以在Linux上用ADS over TCP的方式做类似通信原理一样只是TcAdsDll换成了第三方的ADS库整体思路不用推翻重来。
返回列表