ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++上位机与倍福TwinCAT ADS通讯实战:从路由配置到排错指南

C++上位机与倍福TwinCAT ADS通讯实战:从路由配置到排错指南 干工控的人十有八九都跟“上位机和 PLC 到底怎么说话”较过劲。这两天我在做倍福 TwinCAT 3.0 和 C 上位机之间的通讯用的正是 ADS 协议项目里还涉及到倍福 CX5140 嵌入式控制器的数据采集。第一次接触 ADS 的人看到 AMS NetId、端口号、Symbol Handle 这些词基本直接犯懵连上之后又发现变量读出来全是乱码或者一写就断线。这篇文章把我在实际项目里验证过的 ADS 通讯完整流程写出来从路由器配置到 C 代码再到排错思路适合正在用 C 对接倍福控制器的朋友参考。我会尽量把关键步骤讲透少说废话。1. ADS 通讯到底在传什么1.1 AMS 和 ADS 的关系先把概念理清楚。ADSAutomation Device Specification是倍福定义的设备间通讯协议它并不直接跑在以太网帧上而是跑在 AMSAutomation Message System之上。你可以把 AMS 想象成一家快递公司ADS 是快递单上的地址和运单规则TwinCAT 系统的 AMS Router 就是这家快递公司的分拨中心。每一台参与通讯的设备都会有一个 AMS NetId这是一个 6 字节地址常见写法类似“192.168.1.10.1.1”。前 4 个字节通常和 IP 地址对应后 2 个字节用于区分同一台机器上的不同子系统。NetId 后面还要带端口号Port用来定位到具体的服务比如 TwinCAT 3 里的 PLC Runtime 默认端口是 851。很多人第一次看代码把 851 当成网络端口去填或者把 IP 地址和 AMS NetId 混在一起这就是连不上的最直接原因。AMS Router 负责根据 NetId 和 Port 把报文转给对应的进程。所以你在 Windows 上装好 TwinCAT 后并不是直接和 PLC 建立一条 TCP 连接就完事了而是先要和本地或远端的 AMS Router 通信再由 Router 把报文送进 TwinCAT 系统。这也是为什么 ADS 通讯经常要配置路由Route不配置路由PLC 根本不知道往哪送。1.2 为什么不用 TCP 裸写有的工程师会觉得既然 TwinCAT 支持 TCP/IP那直接用 Socket 读写不行吗可以做但没必要。ADS 的优势在于它帮你处理了符号寻址、句柄管理、错误码分类、异步通知这些事而且和 PLC 的变量表深度绑定。你用 TCP 裸写需要自己维护变量地址映射PLC 工程里改一个变量上位机代码就要跟着改后期维护成本非常高。ADS 则可以直接按符号名比如“MAIN.nCount”去读写变量。你只要告诉它变量名字它通过 AMS 查到对应的内存地址再完成数据交换。再加上 ADS 还支持通知模式PLC 变量变化时主动推给上位机不用上位机不断轮询这在实时性要求高的场景里比裸写 Socket 稳得多。下图是三种常见方式的简单对比通讯方式维护变量地址实时通知实现难度适用场景裸 TCP/UDP Socket自己维护麻烦难做较高自定义私有协议Modbus TCP靠寄存器地址需轮询中跨品牌设备对接ADS按符号名自动解析支持 ServerOnCha低倍福控制器上位机当然ADS 也有它的局限。它毕竟是倍福自己的协议非 Windows 系统需要用倍福提供的 ADS 库或者第三方的 ADS 封装库跨平台支持不如 Modbus 通用。如果项目只是偶尔读几个点位Modbus 也能干但如果你要高频、大批量、带事件通知地操作 TwinCAT 变量ADS 是首选。1.3 几个绕不开的核心概念AMS NetId6 字节地址标识一台设备。同一个 PLC 工程里PLC 侧有一个 NetIdPC 侧也有一个 NetId两者都要配正确。Port标识设备内的服务端口。PLC Runtime 常用 851NC 轴控制常用 900 或 901TwinCAT 系统服务常用 10000。实际用哪个要看目标服务的配置。SymbolPLC 侧的变量符号比如MAIN.nVelocity。通过 Symbol 读写变量不需要关心变量在内存里的绝对地址。Symbol HandleC 通过 ADS 读取某个变量的句柄可以理解为打开了一个文件描述符后续读写都靠这个句柄。IndexGroup / IndexOffsetADS 协议里的索引寻址方式比较复杂。我们直接用 Symbol Handle 的方式能避开大部分索引问题但心里还是要有个概念比如0xF003表示“根据名称获取句柄”0xF005表示“通过句柄读写数据”。这几个概念就像开车时的仪表盘、方向盘和油门分开看都不复杂组合在一起才容易让人发懵。下一章先把环境配好再谈代码。2. 环境准备和基础配置2.1 软件版本怎么搭配TwinCAT 3 本身不是一个独立软件它是基于 Visual Studio 的集成环境XAE。安装 TwinCAT 3 之前要先装好对应版本的 Visual Studio或者至少安装 TwinCAT 官方提供的 VS Shell。以我常用的 TwinCAT 3.1 build 4024 为例它支持 VS 2013、2015、2017、2019、2022但要注意 64 位工程和 32 位工程在引用 ADS 动态库时略有差别。C 上位机这边我一般用 Visual Studio 2019 或 2022项目配置里需要把 TwinCAT 安装目录下的 ADS 头文件和库文件路径加进去。典型的目录是C:\TwinCAT\3.1\Components\Ads\TcAdsDll实际路径以你安装的 TwinCAT 版本为准最好是打开安装目录确认一下。然后在 VS 工程属性里附加包含目录添加 TcAdsDll 所在目录。附加依赖项添加 TcAdsDll.lib如果编译 64 位程序注意选择对应平台的 lib。如果这些都配好了代码里直接写#include TcAdsDef.h和#include TcAdsAPI.h就不会报找不到头文件。2.2 路由器配置最容易卡壳的一步很多 C 侧调 ADS 报错不是代码问题而是路由没配。在倍福的世界里路由就是“哪个 NetId 可以访问哪个 NetId”。如果你是 PC 和本地安装的 TwinCAT 通讯TwinCAT 一般在安装时会自动创建一个本机路由但如果你要和远程控制器比如 CX5140通讯必须在 PC 侧添加一条静态路由。操作路径大概是双击系统托盘里的 TwinCAT 图标打开 TwinCAT 环境在 System 菜单下进到 Route 设置点击 Add Route。需要填的信息有Remote IP Address目标控制器的 IP。AMS NetId目标控制器的 AMS NetId一般在控制器面板或者 TwinCAT 3 的 System Information 里能看到格式类似192.168.0.10.1.1。Transport Type选 TCP/IP。Add Route 后系统可能会提示输入用户名和密码对应控制器的管理员账号权限。这里有个很多人忽略的细节如果项目中用的是 CX5140 这类嵌入式控制器不要只看得上“可以 Ping 通”就觉得网络没问题。ADS 通讯需要 PC 和控制器之间能互相通过 AMS 路由找到对方光丢包通个 ICMP 是不够的。另外控制器的 ADS 通讯服务默认使用的是 AMS Router和网页配置是两套体系别搞混。从 CX5140 的 EDZ 设备描述文件导入问题也顺便说一句。如果项目中需要把 CX5140 的硬件描述导入 TwinCAT 工程可以在倍福官网找到对应设备型号的 EDZ 压缩包解压后放到C:\TwinCAT\3.1\Config\Io\EtherCAT或通过 TwinCAT 的设备安装向导导入。这个动作不影响 ADS 通讯但会关系到后续的硬件配置是否能正确识别。2.3 PLC 工程侧的变量可见性设置路由配好代码写完结果还是读不到变量很可能是 PLC 工程的 Symbol 没有导出。TwinCAT 3 默认并不是把所有变量都对外可见需要在工程属性里开启。在 TwinCAT 3 的 PLC 工程上右键进入属性找到 Symbol 相关设置通常有类似“Publish symbols of this project to external ADS”的选项。把这个选项打开再选择导出全部符号或者按需选择需要导出的变量。如果你只想让上位机能访问某个具体的全局变量列表也可以在对应的 POU 或全局变量表上右键检查其 Symbol 属性是否启用。实际开发中我习惯把需要上位机访问的变量都放在VAR_GLOBAL里这样寻址最简单比如MAIN.nSpeed中的MAIN是程序组织单元名nSpeed是变量名。如果你用的是结构化变量或数组ADS 也支持按结构体整体读写但前提是定义的数据类型和 C 侧的内存布局一致这个后面会细说。3. C 侧通讯代码实战3.1 连接初始化先打开本机端口C 侧使用 TwincAT 提供的 ADS DLL 时步骤很固定打开 ADS 端口设置目标地址然后就可以发起读写请求。看一下最基础的初始化代码#include Windows.h #include string.h #include TcAdsDef.h #include TcAdsAPI.h #pragma comment(lib, TcAdsDll.lib) static AmsAddr g_amsAddr; void InitAdsAddress() { // 以远程控制器为例对方的 NetId 是 192.168.0.10.1.1 g_amsAddr.netId.b[0] 192; g_amsAddr.netId.b[1] 168; g_amsAddr.netId.b[2] 0; g_amsAddr.netId.b[3] 10; g_amsAddr.netId.b[4] 1; g_amsAddr.netId.b[5] 1; // PLC Runtime 默认端口是 851 g_amsAddr.port 851; } int main() { long err AdsPortOpen(); if (err ! 0) { // 打开 ADS 端口失败多数情况是本地 TwinCAT 服务没起来 return err; } InitAdsAddress(); // 接下来就可以读写 AdsPortClose(); return 0; }AdsPortOpen()是必须要调的它会让当前进程和本地 AMS Router 建立连接。如果你连这一步都返回非零优先检查本机是否已经安装并运行了 TwinCAT Runtime 或 TwinCAT 服务。有人觉得我只跑 C 上位机不需要启动 TwinCAT这是不对的。ADS 客户端要正常通信路由器和至少一个 TwinCAT 系统进程必须在跑。3.2 读取一个整数变量的完整流程读取变量用 Symbol Handle 最省心。先通过符号名获取句柄然后用句柄读取数据。示例代码如下long ReadInt16FromPlc(const char* symbol, short value) { unsigned long hVar 0; // 根据符号名获取句柄 long err AdsSyncReadWriteReq( g_amsAddr, ADSIGRP_SYM_HNDBYNAME, // 0xF003 0, sizeof(hVar), // 输出数据长度 hVar, // 输出句柄 (unsigned long)(strlen(symbol) 1), (void*)symbol // 输入符号名 ); if (err ! 0) return err; // 通过句柄读取变量值 err AdsSyncReadReq( g_amsAddr, ADSIGRP_SYM_VALBYHND, // 0xF005 hVar, sizeof(value), value ); // 释放句柄 AdsSyncWriteReq( g_amsAddr, ADSIGRP_SYM_RELEASEHND, // 0xF006 hVar, sizeof(hVar), hVar ); return err; }注意AdsSyncReadWriteReq的参数顺序非常容易写错。它的用途是先给 PLC 发一段数据再从 PLC 读回一段数据。在获取句柄这个场景里写入的是符号名读出的是句柄。很多人第一次写的时候把符号名和句柄的指针放反了结果要么返回参数错误要么读出一堆莫名奇妙的数字。当你在 PLC 侧定义的是INT16 位有符号整数时C 侧对应的是short或int16_t。如果你用int去接收一个 PLC 的INT数据通常也能读出来但高位的两个字节可能是垃圾数据只有低 16 位有效。所以类型匹配非常重要。PLC 的INT C 的shortPLC 的DINT C 的intPLC 的REAL C 的floatPLC 的LREAL C 的double。3.3 写入变量和控制状态写入的思路和读取一样先拿句柄再写。写布尔量可能是现场最常用的操作long WriteBoolToPlc(const char* symbol, BOOL bValue) { unsigned long hVar 0; long err AdsSyncReadWriteReq( g_amsAddr, ADSIGRP_SYM_HNDBYNAME, 0, sizeof(hVar), hVar, (unsigned long)(strlen(symbol) 1), (void*)symbol ); if (err ! 0) return err; err AdsSyncWriteReq( g_amsAddr, ADSIGRP_SYM_VALBYHND, hVar, sizeof(bValue), bValue ); AdsSyncWriteReq( g_amsAddr, ADSIGRP_SYM_RELEASEHND, hVar, sizeof(hVar), hVar ); return err; }这里有个小坑PLC 侧的BOOL在内存里是 8 位但 C 里的BOOL在 Windows 下是 4 字节。如果你直接用 C 的bool类型它一般也是 1 字节跨平台时可能不一致。我一般会在代码里明确用uint8_t来对应 PLC 的BOOL避免因为字节宽度不同导致数据错位。如果你确实用了BOOL很多情况下也能工作因为 ADS 只拷贝你指定的长度但为了规范统一用定长类型更稳妥。写字符串时更要注意长度。PLC 的STRING默认是 80 字节C 侧往句柄写值时长度参数要填字符串长度加 1结尾的\0并且不能超过 PLC 变量定义的最大长度否则会返回长度错误。3.4 用通知模式代替轮询如果上位机需要高频监控 PLC 变量比如速度、位置这些不要写死循环每秒读几百次。ADS 提供 Notification 机制客户端注册通知后PLC 侧变量变化时会主动推数据给上位机。这个方式既省 CPU又能做到很低的延迟。注册通知的核心代码大致是void CALLBACK AdsCallback(AmsAddr* pAddr, AdsNotificationHeader* pNotif, unsigned long hUser) { // 数据紧跟在 AdsNotificationHeader 后面 short* pVal (short*)((unsigned char*)pNotif sizeof(AdsNotificationHeader)); // 在这里处理变量值注意回调运行在 ADS 内部线程不要做耗时操作 } unsigned long hNotify 0; err AdsSyncAddDeviceNotificationReq( g_amsAddr, ADSIGRP_SYM_VALBYHND, hVar, sizeof(short), ADSTRANS_SERVERONCHA, // 服务端检测到变化时通知 0, // 最大延迟 10, // 通知周期单位 ms AdsCallback, 0, hNotify );等通讯结束时记得调用AdsSyncDelDeviceNotificationReq删除通知否则 PLC 侧会一直尝试往客户端推送数据。4. 常见通讯故障和排查技巧4.1 连接都建立不起来先查路由和端口如果调用AdsSyncReadReq后返回0x810或者0x816说明 AMS 路由层根本没找到目标设备。常规排查顺序是物理链路PC 和控制器的网线、IP 是否同一网段。控制器侧是否运行了 TwinCAT 系统服务PLC 程序是否已经激活。PC 侧路由表里有没有目标控制器的 NetId。防火墙是不是把 ADS 端口挡了TwinCAT 服务的端口 48898 或 48899 要允许。目标端口号填对没有PLC Runtime 一般是 851。在这个阶段最直接的办法就是打开 TwinCAT 的路由配置工具添加路由时如果显示成功至少说明网络层面没问题。如果添加路由失败TwinCAT 一般会给出具体原因这比在 C 代码里猜要高效得多。4.2 能连上但读不到变量值如果连接正常但按符号名读变量返回0x741或0x742大概率是符号名不对或 Symbol 没有导出。这里建议先用 TwinCAT 的开发环境确认变量完整路径。比如你在 PLC 工程里定义了一个全局变量nCount但它在GVL里那符号名可能是GVL.nCount如果变量在PRG_MAIN这个程序里符号名是PRG_MAIN.nCount。提示ADS 符号名是区分大小写的MAIN.nCount写成main.nCount很可能就找不到。还有一类情况是变量在 PLC 工程里勾选了“不导出符号”常见于结构体变量或某些内部变量。遇到这种问题回到 PLC 工程属性里把 Symbols 导出打开重新激活一遍配置再试。4.3 超时和周期性断线周期性断线比直接连不上更恶心。我碰到过两种情况一种是上位机用死循环疯狂读取把 AMS Router 和 PLC 的通讯通道打爆了另一种是网络中有短暂的丢包而代码里没有做重试和超时处理。解决思路很直接加宽超时时间不要用默认的几十毫秒就连通信的“极限值”。用 Notification 替代高频轮询。每次会话只打开一次 ADS 端口不要在循环里反复AdsPortOpen/AdsPortClose。如果 PLC 任务周期很短上位机读取频率最好保持在任务周期的 1/2 以下避免数据还没更新就读到了旧值。4.4 报错 4132 这类错误码怎么查网上经常有人问“倍福报错 4132 是好事还是坏事”这种问题没有上下文很难回答。4132 是十进制表示先用计算器转成十六进制也就是0x1024。然后对照 ADS 错误码表看它属于哪一类。0x1000段位的错误码大多是状态或参数类问题一般不是网络断开而是当前 PLC 状态不允许做这个操作比如程序没运行就尝试写变量或者句柄已经失效。建议大家在工程目录里放一份 ADS 错误码表常见错误看几个就记住了。下面是我整理的使用频率最高的几个错误码十六进制常见含义典型场景0x0000无错误正常0x0700设备内部错误内存不足或资源异常0x070A访问被拒绝变量写保护或权限不足0x0740目标端口不存在PLC Runtime 端口填错0x0741目标设备不存在AMS NetId 配错0x0742服务不支持当前设备不支持该 ADS 服务0x0743参数无效输入参数长度或句柄错误0x0749AMS NetId 无效路由配置里的 NetId 错误0x0810无路由到目标路由表里没有对应条目0x0814ADS 超时网络异常或通讯负载过高0x0816路由未找到远端设备无法匹配 NetId在实际项目里我处理这类错误码的经验是先分清是“网络层没通”还是“应用层被拒绝”。网络层错误优先查路由表和防火墙应用层错误优先查变量名、变量可见性、端口号和操作权限。这个分类能省下很多排查时间。5. 比官方文档更实用的几个经验5.1 先用 TwinCAT 自带工具验证我见过不少同事在 C 代码里排查半天最后发现是 PLC 侧根本没激活。所以现在我的习惯是写代码之前先用 TwinCAT 自带的 ADS 测试工具或者简单的 C# 测试程序试一下。如果工具能读到变量值说明整个链路是通的再上手写 C 就很有把握。如果工具也读不到那就别纠结代码了回头查 PLC 工程和路由配置。5.2 变量地址尽量用符号名别手算偏移ADS 支持用 IndexGroup 和 IndexOffset 直接访问 I/O 镜像区这在某些极高性能场景下更快但非常容易踩雷比如不同的 TwinCAT 版本、不同的任务配置I/O 地址会变化。除非你已经对过程映像的布局非常清楚否则我建议优先用符号名加句柄的方式。它的性能对绝大多数上位机场景都够用而且 PLC 工程改造后不用频繁改地址映射。5.3 把 ADS 通讯封装成独立模块不要在主业务代码里到处散落AdsSyncReadReq。我在项目里通常会封装一个TwincatAdsClient类里面统一处理连接、句柄缓存、通知回调、错误码转换。这样即使以后要切换到更高级的通讯框架或者从 C 换成 C#业务层代码基本不用动。句柄缓存也很重要如果某个变量几千次循环里每次重新获取句柄性能会很差而且会堆积大量未释放的句柄最终导致通讯失败。5.4 流程顺序比什么都重要最后再分享一个我自己的习惯凡是碰到 ADS 通讯问题一定按“网络通不通 → 路由有没有 → 符号有没有导出 → 代码类型对不对 → 权限够不够”的顺序查。很多人一上来就怀疑代码结果绕了半天发现 PLC 工程的 Symbol 导出被关掉了。顺序对了很多问题几分钟就能定位顺序反了一晚上可能都白搭。ADS 通讯说难不难说简单也不简单它连接的是 PLC 工程师和上位机工程师两套思维体系。希望这篇文章能让你少走点弯路。
返回列表