ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++与TwinCAT 3 ADS通讯实战:从原理到排错的全指南

C++与TwinCAT 3 ADS通讯实战:从原理到排错的全指南 先说个真实场景现场PLC程序跑得好好的双击Twincat 3.0里的Activate Configuration也没报错轴能转、IO能采但你用C写了个上位机想读几个变量连了半天返回一个0x1024十进制4132一脸懵。这个错误几乎每个做倍福二次开发的人都遇到过网上中文资料零散得很官方文档又是英文且偏难啃。我做了几年倍福上位机开发ADS通讯这块踩过不少坑这次把我整理过的东西一次性写清楚包含了从连接原理、工程配置到变量寻址、类型映射、高频读写性能优化最后还有排查链路希望能帮你少走弯路。ADS全称Automation Device Specification是倍福自己的通讯协议TwinCAT 3和外部程序打交道基本都走它。C要跟倍福通讯绕不开ADS。下面从原理讲到代码再讲到坑。1. ADS通讯到底在解决什么问题1.1 为什么选C而不全用倍福自家方案倍福生态里跟PLC交换数据不止ADS一条路常见的还有OPC UA、Modbus TCP以及TwinCAT自带的ADS。OPC UA的好处是跨平台、跨厂商但你要在C里集成一个OPC UA客户端库配置节点、证书、命名空间复杂度并不低。Modbus TCP简单但数据组织太原始一次要搬一整块寄存器和PLC里结构体变量对应起来非常痛苦。ADS不一样它是倍福的亲儿子TwinCAT 3对外通讯优先支持能直接按变量名读写PLC内部变量权限控制、实时性、效率都拉满。选择C而不是C#主要看场景。TcAdsDll提供的C接口是底层最基础的APIC#和VB.NET都是对这层API的封装。如果你做的是测量系统、视觉系统、运动控制里的上位机逻辑需要追求低延迟和精细的内存控制或者现有的代码库就是C写的那直接用C调TcAdsDll是最合理的路径。另一个原因是调试方便C接口做的事情很直白返回值、错误码一目了然出问题容易定位不会像高级封装那样把底层错误包起来。1.2 认识两个关键身份信息AMS NetId和PortADS能够通讯的前提是你能唯一定位到目标设备上的目标程序。这里牵扯两个概念AMS NetId和AMS Port。AMS NetId长什么样类似192.168.0.1.1.1一共六组数字前四组通常是设备IP地址后两组是设备内部分配的编号。注意AMS NetId和你网卡的IP可以不一样它是倍福设备在AMS路由器里的标识是逻辑地址。你在TwinCAT开发环境里能看到本机NetId在Windows命令行执行TwinCAT.exe /getnetid也能拿到。AMS Port则是设备内部程序监听的端口号。TwinCAT 3里最常用的是851对应PLC运行时10000是System Service用来做系统级操作还有900是IO设备等。不同端口代表不同的服务通讯的时候必须指定对。很多人连接失败就是把851写成了48898之类的东西——48898是ADS over TCP/IP的TCP端口号跟AMS Port完全是两码事。清楚这两个信息之后你在上位机里构造一个AmsAddr结构体把NetId和Port填对剩下的就是通过TcAdsDll的接口把请求发给Windows上的Router也就是TwincAT Router服务Router再通过TCP 48898端口转发给目标设备。这句话基本概括了C与TwinCAT 3 ADS通讯的完整路径。2. 环境准备TcAdsDll与C工程配置2.1 在哪里找到正确的Dll版本你要在C里调ADS需要三个东西TcAdsDll.lib、TcAdsDll.dll以及头文件TcAdsAPI.h有的版本叫AdsClient.h。装了TwinCAT 3或者TwinCAT XAE开发环境之后这些文件通常在C:\TwinCAT\3.1\Components\Ads\目录下。具体位置会因为安装版本略不一样我用的版本中文件分布在C:\TwinCAT\3.1\Components\Ads\TcAdsDll\和SDK目录里网上也有人从倍福官网的ADS SDK单独下载。这里要特别提醒第一坑32位和64位的Dll版本必须和你的编译目标平台一致。你工程是x64就加载64位版TcAdsDll.dll是Win32就加载32位版。混着用的话调用AdsPortOpen时可能直接崩溃或者返回一个莫名其妙的错误码。我之前在Visual Studio里默认x86编译连的是64位的Dll结果一跑就崩查了半天才反应过来。VS工程里链接器设置的附加库目录和运行时的PATH都要对应上。2.2 工程配置三个容易翻车的地方第一个是头文件路径。TcAdsAPI.h用到了#include windows.h你在Win32控制台程序里没问题但如果用了某些严格警告级别的项目配置可能因为头文件里的结构体对齐方式差异报警。建议把ADS头文件放在项目include目录里单独管理不要和系统头混在一起。第二个是字符编码。TcAdsAPI.h里有一部分函数是窄字符版本一部分参数类型跟宽字符无关但和字符串相关的函数在C下容易和你工程的Unicode设置打架。我的经验是用多字节字符集Multi-Byte Character Set编译或者干脆所有字符串都用char*并保持工程设置一致别在宽窄字符之间来回转。第三个是依赖项。TcAdsDll.lib不要用#pragma comment(lib, TcAdsDll.lib)硬编码除非你把lib放在工程目录里且位数匹配。我建议在工程属性里配置好附加包含目录和附加库目录这样切Debug/Release、切x86/x64的时候路径不会错。这个细节能省掉后续很多奇怪的无头绪报错。配置完这些你的C工程就具备了调用ADS接口的条件。下面写第一段实际代码。3. 第一个C读写程序从连接成功到读到变量3.1 核心API的调用链路TcAdsDll的C接口核心就几个函数链路非常清晰AdsPortOpen()打开本机的ADS端口得到一个端口句柄。AdsGetLocalAddress(AmsAddr*)获取本机AMS地址这一步通常用来方便地构建本机通讯地址。AdsSyncReadWriteReq()、AdsSyncReadReq()、AdsSyncWriteReq()同步读写请求。AdsPortClose()关闭端口。读和写的逻辑都有点像你给某个服务发一个RPC请求构造目标地址AmsAddr指定功能码传数据缓冲区函数返回ADS错误码。0表示成功非0就是出错了。3.2 读一个INT变量的最小实现一个典型的最小程序长得像这样#include windows.h #include TcAdsAPI.h #include iostream #include cstring int main() { long nPort AdsPortOpen(); if (nPort 0) { std::cerr AdsPortOpen failed std::endl; return -1; } AmsAddr addr; memset(addr, 0, sizeof(addr)); addr.port 851; // 本地调试时直接用本机地址 AdsGetLocalAddress(addr); unsigned short value 0; unsigned long bytesRead 0; // 假设PLC里有一个全局变量 nTestValue类型是INT long err AdsSyncReadWriteReq( nPort, addr, ADSSYMB_REQ, // 服务ID读符号变量 0, 0, value, // 返回数据缓冲区 sizeof(value), (void*)nTestValue, // 变量名字符串 10 // 变量名长度 ); if (err 0) { std::cout Read value: value std::endl; } else { std::cerr Error: 0x std::hex err std::dec std::endl; } AdsPortClose(); return 0; }别急着运行有几个点说一下。ADSSYMB_REQ是读取符号变量时用的服务ID它配合indexGroup0, indexOffset0表示我不通过地址方式读而是通过变量名方式读。AdsSyncReadWriteReq这个名字有点绕——它实际上是“先写请求数据再读响应数据”在这里写入的是变量名字符串读出的就是变量值。很多人第一次用会疑惑为什么读变量要用ReadWrite而不是Read因为你需要把变量名传给ADS服务端这是由ADS协议决定的。3.3 写变量同样套路写变量的代码逻辑类似用AdsSyncWriteRequnsigned short newValue 66; long err AdsSyncWriteReq( nPort, addr, ADSSYMB_REQ, 0, 0, newValue, sizeof(newValue) );注意AdsSyncWriteReq没有ReadWrite的“写入请求数据”那一步因为请求数据就是你要写的值本身直接放在缓冲区里传过去就行。但变量名怎么传这就微妙了——AdsSyncWriteReq在indexGroup0, indexOffset0时内部其实不接受变量名。你要通过符号方式写单个变量最保险的是用AdsSyncReadWriteReq把变量名作为写的那一部分数据把目标值放在读缓冲区里。等等这样不对。重新理一下。AdsSyncReadWriteReq的签名大致是long AdsSyncReadWriteReq( long nPort, AmsAddr* pAddr, unsigned long nIndexGroup, unsigned long nIndexOffset, unsigned long nLength, // 读缓冲区长度 void* pData, // 读缓冲区存放结果 unsigned long nWriteLength, // 写入数据长度 void* pWriteData // 写入数据请求参数 );对于读符号变量nIndexGroupADSSYMB_REQ(0x0000F003)nIndexOffset0pWriteData写变量名pData接收值。对于写符号变量仍然用AdsSyncReadWriteReq但pWriteData写变量名pData不用来接收结果而是放要写的值这看起来别扭但ADS就是这样的机制通过这个服务可以同时携带“参数”和“返回值”写变量时参数是变量名返回值缓冲区用来承载要写入的值。一些官方示例里确实这么干。实际上更常规的做法是先通过AdsSyncReadWriteReq拿到符号句柄symbol handle然后用句柄去重复读写这个我们放到第4节详细讲。这里先把最简单的读法跑通重点理解接口的请求/响应模型。跑通上述程序后如果返回值不是0恭喜你正式踏进ADS踩坑大门。第一个拦截你的大概率就是0x10244132这个错误值得专门拿出来说后面第7节会展开。4. 变量寻址的两套体系符号句柄与索引寄存器4.1 符号方式最省事的做法用变量名直接读写类似上面例子这是最直观的方式。但每次都传变量名字符串ADS服务端需要动态解析名字到地址性能上会有损耗对于高频读写比如1ms周期是不可接受的。解决办法是先解析一次拿到“符号句柄”之后用句柄读写。获取符号句柄的核心代码unsigned long handle 0; unsigned long bytesRead 0; long err AdsSyncReadWriteReq( nPort, addr, ADSSYMB_REQ, 0, 0, handle, sizeof(handle), (void*)nTestValue, 10 ); if (err 0) { // 拿到句柄后后续读写都走这个句柄 unsigned long readReq 0x0000F005; // ADSSYMB_VALBYHANDLE err AdsSyncReadReq( nPort, addr, readReq, handle, sizeof(value), value ); }这里的ADSSYMB_VALBYHANDLE0x0000F005是“通过句柄读取变量值”的服务ID你把句柄放在indexOffset的位置一次调用就是一次读数。这种方式比每次传名字快得多而且语义清晰。句柄有个特性每次PLC重新激活配置或重启后句柄可能失效。所以在程序里不要缓存句柄到本地文件每次连接建立后重新获取。我见过有人把句柄写进配置文件结果PLC重启后上位机怎么读都返回错误折腾了很久。4.2 索引组/索引偏移方式适合大块数据的做法第二种寻址方式是基于IndexGroup和IndexOffset的地址访问这其实就是ADS协议“原始”的寻址方式。你可以把它理解成直接访问PLC内存区域。常见的IndexGroup有两类。一类是IO区域比如ADSDAT_IO160x00001020对应16位IO区域ADSDAT_IO320x00001040对应32位IO区域。你在Twincat里看到的IW输入字、QW输出字就是这些区域的一部分用偏移量定位到具体通道。另一类是数据区域比如ADSDAT_IO16上偏移到某个地址或者在Twincat 3里默认的Data Area。IndexGroup和IndexOffset的组合可以精确到PLC变量在内存中的偏移。但问题是你很难手工计算出某个全局变量在内存区里的准确偏移所以这个方式更多用于读取固定地址的光谱数据、IO映射数据或者通过Twincat的TCNCO工具提前算好偏移量。实际项目中我的经验是读一些零散的、需要“一眼看懂”的变量用符号方式字符串或句柄读一批固定结构的数组、例如几百个轴的位置数据、一批传感器采集结果用地址方式一次性读一个大缓冲效率明显更高。4.3 两套体系的适用场景两套体系不是互斥的可以混合用。一个工程里我通常会在初始化阶段把关键的几十个变量都用符号方式解析成句柄后续循环里全部走句柄对于周期性批量上传的数据单独用地址方式开一块内存读取。混合使用的时候注意别搞混服务ID不然返回的要么是错误码要么是乱数据。5. 数据类型的跨语言映射内存布局决定生死5.1 基本类型对照表C和TwinCAT 3的PLC类型不是一一对应的尤其要注意位宽和符号性。我常用的一张对照表如下Twincat 3类型位宽(bit)C对应类型说明BOOL8bool / BYTE一个字节别用1bit位域去读BYTE8uint8_t无符号字节WORD16uint16_t无符号16位DWORD32uint32_t无符号32位SINT8int8_t有符号8位INT16int16_t有符号16位最容易搞错的是它不等于C intDINT32int32_t有符号32位C的int在多数平台是它LINT64int64_t有符号64位REAL32float单精度浮点LREAL64double双精度浮点STRING可变char[]通常固定长度包含\0这里最大的坑是TwinCAT的INT是16位而C的int通常是32位。你用AdsSyncReadWriteReq读一个INT变量却传给函数一个int的缓冲区指针结果只写入了半个int导致高16位是未初始化的野数据。轻则数值不对重则破坏栈上其他数据。我的规矩是所有ADS相关数据定义一律使用stdint.h里的定长类型比如int16_t、uint32_t从源头上根治位宽问题。5.2 结构体、数组和字符串的坑结构体在ADS通讯里是按内存连续排布的PLC侧的结构体成员会按PLC编译器的对齐规则排内存。C侧定义对应的结构体时如果对齐方式不一致读出来的数据会整体错位。比如PLC侧结构体TYPE ST_AxisData : STRUCT nPosition : DINT; nVelocity : DINT; fAcc : REAL; bEnable : BOOL; END_STRUCT END_TYPEC侧如果直接写struct AxisData { int32_t nPosition; int32_t nVelocity; float fAcc; bool bEnable; };大概率是对的因为成员都是4字节或1字节默认对齐下没有空洞。但如果成员里有WORD或者BYTE混合排列双方编译器的对齐规则就可能产生不同的空洞填充读出来数据就飘了。解决方式是两边都明确指定对齐或者在C侧把结构体按1字节或4字节对齐声明#pragma pack(push, 1) struct AxisData { int32_t nPosition; int32_t nVelocity; float fAcc; uint8_t bEnable; }; #pragma pack(pop)注意#pragma pack(1)能保证C侧不做填充但PLC侧内存布局是它自己定的你不能让PLC配合你改对齐所以要对齐的是C侧去贴合PLC侧。PLC侧默认成员之间不填充倍福的ST结构体不太倾向于加padding但为了稳妥你只要定义C结构体时用pack(1)并且成员顺序和PLC完全一致就不会错。字符串方面PLC的STRING(20)是固定20字节的字符数组最后带一个隐含的结束符实际能存19个字符。C侧读取时用char buf[20]就行。中文场景下要注意编码倍福PLC内部字符串一般按Windows本地代码页存储你在C侧读出来打印到控制台可能正常一旦上位机是Linux或者Qt环境中文会乱码。乱码不一定是通讯错了是编码不一致。稳妥做法是PLC侧字符串统一用英文/ASCII或者上位机做编码转换别指望PLC配合UTF-8。数据对齐和类型映射这块属于那种报错少、出错隐蔽的“软坑”程序能跑数值不对排查起来最费劲。第7节我会具体讲排查链路这里先记住一个原则C侧每个读缓冲区的类型、大小必须精确匹配PLC侧变量别图省事用默认的int、long。6. 高频场景的通讯架构轮询、通知与多线程6.1 轮询周期的取舍很多项目一上来就是写个while循环每10ms读一次轴位置。这在变量少、周期不敏感的场景没问题但要注意几个副作用。第一ADS请求本身有网络开销。同步请求从发起等待响应到数据返回在本地连接下通常几百微秒跨网段可能几毫秒。如果你的控制周期是1ms同步轮询根本跑不动。第二同步轮询遇到偶发网络延迟会阻塞你整个线程如果上位机还承担UI刷新界面会卡。第三请求频率太高会把Router和PLC侧的通信负载拉高占用PLC循环时间。我的工程经验是100ms以上周期随便同步读10ms~50ms周期用句柄方式读尽量避免字符串解析1ms~5ms周期别用ADS做高频点对点读写优先选EtherCAT的分布时钟或者把数据缓冲好在PLC里整块传大数据量的块传输优先地址方式整读而不是一条条变量名读。6.2 通知Notification机制的使用如果不想让上位机疯狂轮询ADS有通知机制你告诉PLC“这个变量一变就通知我”数据变化时Router主动推送给上位机。使用通知需要调用AdsSyncAddDeviceNotificationReq注册一个回调这个机制在TwinCAT 3里依然有效。但实际用下来我觉得通知机制更适合变量变化频率低、事件驱动的场景比如报警状态、按钮信号。对于几毫秒就变化一次的高速测量值通知回报会产生非常密集的消息处理不过来反而丢数据。认真的高频系统还是得靠“PLC缓存一批数据上位机整块取走”的模式。6.3 线程模型与生命周期管理C上位机用ADS通讯多线程是绕不开的。我的建议是单开一个专门的通讯线程里面做循环读写通过原子变量或消息队列和UI线程交互不要让UI线程直接调同步的ADS接口除非你的UI对卡顿无感。另一个细节是关闭顺序先停止读写循环再AdsPortClose()。如果读写线程还在跑主线程就把端口关了轻则异常退出重则直接崩溃。这个问题在程序退出时最容易踩我的做法是加一个退出标志读写线程循环里检查标志确认退出后再关端口。ADS接口本身是线程安全的可以多线程同时调用但我不建议你多个线程并发读同一个端口。因为底层Router的机制在并发请求下响应和请求的对应依赖调用顺序并发高时极端情况可能串包。如果你有很多变量要读在一个通讯线程里顺序读一定比多线程并发读更稳。7. 踩坑实录从4132到数据错位的完整排查链路7.1 错误0x10244132的几种真面目回到开头的0x1024。这个十进制4132的错误码在ADS世界里出现频率极高但含义在不同上下文里有细微差别。ADS错误码是32位的高位16位标识错误来源组件低位16位是具体错误号。0x1024的低16位0x1024就是4132。在TwinCAT系统层面0x1024常被解读为“实时任务看门狗超时”或“设备不可用”之类在ADS通讯请求返回时它更多指向“目标AMSNetId或Port不可达”意思是Router根本不知道该把请求投递到哪里或者目标设备不存在、程序没运行。排查顺序请严格照这张表来排查步骤检查内容常见原因1Router是否运行右键托盘TwincAT图标看Router是否启动2AMS NetId是否填对本机执行TwinCAT.exe /getnetid比对程序里的AmsAddr3Port是否正确PLC程序运行时用851System Service用100004目标设备是否在Router路由表打开TwinCAT Router配置查看静态路由表是否添加了目标设备5防火墙是否拦截放行UDP/TCP 48898端口TCP用于ADS通讯这五步走完绝大多数4132都能解决。其中最容易忽略的是第4步——如果你连接的是远程倍福控制器比如CX5140本地电脑的Router必须添加一条指向远程设备IP的静态路由否则就算NetId写对了Router也不知道把包往哪送。添加路由不能用CMD的route命令要在TwincAT Router配置界面里加填远程设备的IP和AMS NetId。7.2 远程连接失败Router与防火墙远程连接倍福控制器时除了静态路由还有几个细节。控制器端的防火墙要放行TwincAT相关端口。CX系列默认情况WinCE/Windows系统的防火墙可能阻止连接你要手动允许。另外有些客户现场的交换机开了端口隔离策略导致UDP广播和TCP请求不通这种网络层面的问题最隐蔽因为从电脑上ping设备IP是通的但实际上TCP 48898端口是不通的。我遇到过现场把交换机VLAN配错了上位机跟PLC的IP能互ping但ADS就是连不上最后用telnet检查48898端口才发现是端口不通。如果你在TwinCAT 3开发环境里能用“Choose Target System”扫到并激活设备但你的C程序连不上那问题多半出在你的程序里NetId或者Port配置上。反过来如果你在开发环境里都扫不到设备那优先排查网络别折腾代码。7.3 数据错位的三板斧排查数据错位这类“软问题”比硬错误更难查我的排查套路固定三板斧。第一板斧是确认字节序。倍福PLC数据在内存中是小端模式这和绝大多数x86/ARM小端处理器一致所以一般不用转换。但如果你上位机跑的是某些网络设备或大端模式系统比如某些嵌入式板子就要做字节序转换否则读出的REAL会变成天文数字。第二板斧是打印原始十六进制。读回来的数据先别急着转成float、int把它以十六进制字节数组打出来和PLC侧在线监视的内存数据比对。如果字节相同但数值解释不对就是类型定义错误如果字节本身就不同那就是通讯寻址或句柄的问题。这一步能快速区分是“读错了”还是“翻译错了”。第三板斧是核对偏移量。PLC侧结构体里如果加了新变量所有后续成员的偏移都会变。C侧结构体忘改的话前面的数值正常后面的全部错乱。我的做法是给C侧结构体写一个静态断言用offsetof检查关键成员的偏移是否符合预期编译期就能抓出一批问题。这三板斧下来基本没有排查不了的数据错位问题。8. 最后再分享一个实用技巧用TwinCAT Development环境辅助调试ADS很多人写C ADS通讯时习惯在TwinCAT里放几个变量来回试然后在自己程序里打日志。其实TwinCAT Development环境自带的ADS监视工具很方便只是大家不常用。TwinCAT XAE里有一个“ADS Monitor”窗口在System Manager里可以添加能实时监视ADS路由器的连接、各个端口的通讯负载、错误计数。当你怀疑自己的上位机请求是不是有问题时打开这个窗口操作一下能看到请求是否到达、错误码是什么。这是个很好的辅助手段比单靠C侧打印错误码直观得多。还有一个实践技巧是先用TwincAT自带的“TcXaeShell”里用AdsClient测试连接。TwinCAT XAE自带一个简单的ADS测试客户端命令行里输入TC3ADS相关命令或者用系统管理器里的Device Editor直接操作能快速确认NetId和Port是否OK。确认了通迅链路是通的再回到你C代码里找问题能节省大量时间。我个人做了这么久倍福上位机开发最大的体会是ADS通讯本身并不难难的是各种配置和环境的细节。编程接口就那么几个函数真正让你抓狂的永远是Router路由、NetId、端口、防火墙、数据对齐这些东西。所以我把这篇的重点放在这些不容易从例程里直接学会的地方希望你在现场少走点弯路。如果你在跑通上述示例后还有别的报错先按第7节的排查链路走一遍80%的问题能自己解决。
返回列表