TwinCAT3以太网TCP/IP自由协议通讯实战:从客户端到服务端的完整实现
1. 为什么你需要掌握TwinCAT3的TCP/IP自由协议通讯如果你正在用TwinCAT3做项目尤其是需要和上位机、视觉系统、其他品牌的PLC或者一些智能设备打交道那你肯定遇到过通讯问题。厂家提供的标准协议比如ADS虽然方便但有时候就像给你一双固定尺码的鞋合不合脚都得穿。比如你要对接一个老旧的设备它只认自己定义的一串十六进制数据包或者你需要极高的通讯效率标准协议的开销让你觉得不爽。这时候你就需要自己动手丰衣足食——直接使用最底层的TCP/IP Socket通讯也就是我们常说的“自由协议”或“裸套接字”。这听起来有点“硬核”但别怕。我干了这么多年自动化从早期的TwinCAT2到现在的TwinCAT3这种需求太常见了。掌握它你就相当于拿到了数据通讯的“万能钥匙”不再受制于特定的驱动或库。你可以定义任何你想要的报文格式实现最精简、最高效的数据交换。这篇文章我就带你从零开始手把手实现一个TwinCAT3的TCP/IP客户端和服务端。我会把我踩过的坑、调试的小技巧都揉进去保证你跟着做一遍就能在自己的项目里用起来。我们不用那些虚的理论直接上代码看效果目标是让你看完就能动手实操。2. 动手前的准备环境与授权磨刀不误砍柴工开始写代码之前咱们得先把环境搭好。这里有几个关键点不注意的话后面可能会卡住。2.1 TwinCAT3工程与TCP/IP授权首先你得有一个正常的TwinCAT3开发环境。打开Visual Studio对TwinCAT3是VS的插件新建一个“TwinCAT XAE Project”。然后在“Solution Explorer”里添加一个PLC项目这个就是我们写逻辑的地方。接下来是最关键的一步添加TCP/IP Socket授权。很多新手会忽略这个导致功能块调用不了。在TwinCAT的“System Manager”视图里找到你的PLC设备右键选择“Change…”进入配置模式。然后在“License”选项卡下你会看到一个列表里面需要勾选“TF6310 | TC3 PLC TCP/IP Communication”这一项。放心贝克霍夫Beckhoff提供了7天的免费试用授权足够我们学习和测试了。勾选上激活一下这一步就完成了。记住这个授权是针对PLC运行时的没有它你的Socket功能块就无法执行。2.2 必不可少的库文件TwinCAT3把TCP/IP通讯的功能封装成了标准的PLC功能块Function Block FB非常易用。我们主要会用到以下几个它们都位于Tc2_System库中通常新建的PLC项目会自动引用这个库但检查一下总没错FB_SocketConnect 客户端用来主动连接远程服务器。FB_SocketListen 服务端用来监听指定端口。FB_SocketAccept 服务端用来接受客户端的连接请求。FB_SocketSend 发送数据。FB_SocketReceive 接收数据。FB_SocketClose 关闭Socket连接。你可以在PLC项目的“References”里查看是否包含了Tc2_System。如果没有右键“References” - “Add Library…” - 选择“Tc2_System”添加即可。这些功能块就是我们接下来所有操作的核心工具。2.3 一个重要的系统组件TCP/IP Test Tool在真正的硬件PLC比如CX系列上Windows CE/Embedded系统可能默认没有完整的TCP/IP套接字支持。为了让我们写的Socket程序能跑起来需要在PLC的Windows系统里安装一个叫“Beckhoff TCP/IP Test Tool”的组件包。这个安装包通常在你的TwinCAT安装目录下可以找到或者从贝克霍夫官网下载。安装完成后一定要重启PLC这个改动才能生效。如果你只是在电脑上跑TwinCAT3的仿真环境Run in System Tray一般不需要这一步因为你的Windows系统本身已经支持了。但如果是真机部署这步千万别省我早期就因为这个折腾了好久。3. 从客户端做起主动出击的连接者我们先来实现客户端。想象一下你的PLC要主动去连接一台电脑上的数据服务器或者另一台作为服务端的PLC。客户端的流程很清晰创建Socket - 连接服务器 - 发送/接收数据 - 关闭连接。3.1 变量声明给数据安个家在PLC的POU程序组织单元里我们先定义好所有需要的变量。我把关键变量都做了注释你一看就懂。PROGRAM SocketClient VAR // 核心功能块实例 _FB_SocketConnect: FB_SocketConnect; _FB_SocketSend: FB_SocketSend; _FB_SocketReceive: FB_SocketReceive; _FB_SocketClose: FB_SocketClose; // 连接句柄相当于通讯的“身份证” _hSocket: T_HSOCKET; // 你要连接的服务器的地址和端口 _ServerIpAddress: T_IPv4Addr : 192.168.1.100; // 改成你服务器的真实IP _ServerPort: UDINT : 6666; // 端口号确保和服务器监听的一致 // 控制信号通常由HMI按钮触发 bConnect: BOOL; // 触发连接 bSend: BOOL; // 触发发送 bClose: BOOL; // 触发关闭 boConnectErr: BOOL; // 连接错误标志 // 数据缓冲区 nSendByteLength: UDINT : 9; // 本次准备发送9个字节 arrSendData: ARRAY[1..10] OF BYTE : [16#11, 16#22, 16#33, 16#44, 16#55, 16#66, 16#77, 16#88, 16#99, 16#A0]; arrReceiveData: ARRAY[1..100] OF BYTE; // 接收缓冲区预留100字节 // 工具 Timer1: TON; // 用于周期触发接收 Send_R_TRIG: R_TRIG; // 发送上升沿检测 END_VAR这里有几个细节T_HSOCKET 这个句柄类型非常重要。FB_SocketConnect连接成功后会把建立的Socket连接句柄输出到hSocket。后续的发送、接收、关闭功能块都需要传入这个句柄它们才知道操作的是哪一个连接。一个客户端可以创建多个句柄连接多个服务器。T_IPv4Addr 这是TwinCAT定义的IP地址类型直接赋值字符串格式的IP即可比用数组存方便多了。数据准备 我提前把要发送的10个字节数据写在了数组初始化里。实际项目中这里可能是从传感器读取的实数或是组合好的复杂结构体。3.2 程序逻辑一步步实现通讯现在我们在主程序里把这些功能块调用起来。PLC程序是循环执行的所以这些功能块会每个周期都被调用。// 1. 建立连接 _FB_SocketConnect( sRemoteHost: _ServerIpAddress, // 目标IP nRemotePort: _ServerPort, // 目标端口 bExecute: bConnect, // 上升沿触发连接动作 tTimeout: T#2S, // 连接超时时间设2秒比较稳妥 bError boConnectErr, // 连接出错标志 hSocket _hSocket // 输出连接句柄 );当你在HMI上按下“连接”按钮将bConnect置为TRUE一个周期FB_SocketConnect就会开始工作。如果连接成功_hSocket就会得到一个有效值并且bError为FALSE。如果服务器没开或者网络不通bError会变为TRUE。这里有个坑bExecute是上升沿触发但功能块本身有执行时间。在它bBusy为TRUE期间不要再给新的bExecute上升沿否则可能会干扰当前操作。好的做法是用按钮的上升沿直接触发或者自己用R_TRIG做个边沿。// 2. 发送数据 Send_R_TRIG(CLK: bSend, Q ); // 将按钮信号转为上升沿 _FB_SocketSend( hSocket: _hSocket, // 使用连接得到的句柄 cbLen: nSendByteLength, // 发送前9个字节 pSrc: ADR(arrSendData), // 数据源地址ADR是取地址运算符 bExecute: Send_R_TRIG.Q, // 用上升沿触发发送 tTimeout: T#1S );发送相对简单。关键是cbLen和pSrc。cbLen指定发送多少字节你可以发整个数组用SIZEOF(arrSendData)也可以像我一样只发一部分。pSrc必须传入数据的内存起始地址所以要用ADR()函数。同样bExecute建议用上升沿触发避免连续发送。// 3. 接收数据 Timer1(IN: NOT Timer1.Q, PT: T#100MS, Q ); // 生成一个100ms周期的脉冲 _FB_SocketReceive( hSocket: _hSocket, cbLen: SIZEOF(arrReceiveData), // 我希望接收的最大字节数缓冲区大小 pDest: ADR(arrReceiveData), // 数据目的地地址 bExecute: _hSocket 0 AND Timer1.Q, // 已连接且定时触发接收 tTimeout: T#1S, nRecBytes // 实际接收到的字节数这个输出很重要 );接收是被动的你不知道数据什么时候会来。所以通常有两种做法轮询和事件触发。这里我用的是最简单的轮询用一个定时器Timer1每隔100ms尝试读取一次。bExecute条件里加了_hSocket 0确保只在连接建立后才尝试接收。cbLen告诉功能块“我的缓冲区有100字节你最多给我读这么多”而nRecBytes输出参数会告诉你“这次实际读到了多少字节”。根据nRecBytes的值你就能知道是否收到了新数据以及收到了多少然后去处理arrReceiveData数组里对应长度的数据。// 4. 关闭连接 _FB_SocketClose( hSocket: _hSocket, bExecute: bClose, tTimeout: T#1S );通讯结束记得礼貌地关闭连接释放资源。关闭后_hSocket句柄就失效了不能再用于发送接收。4. 搭建服务端稳坐中军帐的接收者现在角色互换让我们的PLC作为服务端等待客户端来连接。服务端的流程比客户端多一步绑定监听 - 接受连接 - 发送/接收数据 - 关闭连接。4.1 服务端变量与监听服务端的变量定义和客户端类似但多了一个监听句柄。PROGRAM SocketServer VAR _FB_SocketListen: FB_SocketListen; _FB_SocketAccept: FB_SocketAccept; _FB_SocketSend: FB_SocketSend; _FB_SocketReceive: FB_SocketReceive; _FB_SocketClose: FB_SocketClose; _hListen: T_HSOCKET; // 监听句柄 _hSocket: T_HSOCKET; // 连接句柄来自被接受的客户端 _LocalIpAddress: T_IPv4Addr : 192.168.1.31; // 本机IP如果有多网卡需指定 _LocalPort: UDINT : 9977; // 监听的端口号 bListen: BOOL; // 启动监听 bAccept: BOOL; // 接受连接 // ... 其他控制信号和数据缓冲区与客户端类似 END_VAR服务端程序的第一步是监听// 启动监听 _FB_SocketListen( sLocalHost: _LocalIpAddress, // 绑定到本机这个IP地址 nLocalPort: _LocalPort, // 监听9977端口 bExecute: bListen, // 上升沿触发通常上电后一直为TRUE即可 tTimeout: T#1S, hListener _hListen // 输出监听句柄 );bListen可以做成上电后自动置TRUE并保持这样服务端就一直处于监听状态。sLocalHost如果设为空字符串则会绑定到所有本地网卡0.0.0.0这在大多数情况下更方便。hListener是后续接受连接的必要输入。4.2 接受连接与数据交换有客户端连上来之后我们需要“接受”这个连接才能建立真正的数据通道。// 接受客户端连接 _FB_SocketAccept( hListener: _hListen, // 传入监听句柄 bExecute: bAccept, // 通常可以一直为TRUE持续等待连接 tTimeout: T#1S, bAccepted , // 输出本次执行是否接受了一个新连接 hSocket _hSocket // 输出与客户端通讯的连接句柄 );这里bAccept也可以一直设为TRUE。当有客户端连接时FB_SocketAccept会自动完成接受操作并通过hSocket输出一个新的连接句柄。重点来了这个_hSocket和客户端的那个_hSocket意义相同后续的发送(FB_SocketSend)、接收(FB_SocketReceive)、关闭(FB_SocketClose)操作服务端都是针对这个句柄进行的。也就是说数据交换的逻辑和客户端几乎一模一样只是触发条件和上下文不同。服务端的发送和接收功能块调用和客户端章节的代码结构完全一致只需要确保使用的是FB_SocketAccept输出的那个_hSocket句柄。同样接收也可以用定时器轮询条件可以设为_hSocket 0 AND Timer1.Q。5. 实战测试与深度调试技巧理论说再多不如跑一遍。我强烈建议你跟着做用网络调试助手如TCP/UDP Socket调试工具作为对端来测试这样能清晰地看到收发数据的原始字节。5.1 客户端连接测试准备服务端 先在电脑上打开网络调试助手选择“TCP Server”模式设置本地IP本机IP和端口比如6666然后点击“启动监听”。配置与连接 在TwinCAT3客户端程序里将_ServerIpAddress和_ServerPort改成调试助手的IP和端口。将程序编译下载到PLC或启动本地运行。在HMI或变量表里给bConnect一个TRUE脉冲。观察现象 如果一切正常网络调试助手的连接列表里会出现一个新的客户端连接显示PLC的IP地址。同时PLC程序中的boConnectErr应为FALSE_hSocket不为0。这一步只建立了通道还没发数据。5.2 数据收发测试客户端发送 在PLC端触发bSend。你应该能在网络调试助手的接收区看到一串十六进制数据11 22 33 44 55 66 77 88 99。这正是我们arrSendData数组的前9个字节。这证明了发送功能正常。服务端回复 在网络调试助手的发送框里同样输入一串十六进制数据比如AA BB CC DD然后点击发送。客户端接收 观察PLC程序中的arrReceiveData数组和nRecBytes变量。稍等片刻取决于你的接收轮询周期你应该能看到数组的前几个字节变成了AA BB CC DD并且nRecBytes等于4。这说明接收通路也打通了。5.3 那些年我踩过的坑与解决之道连接失败 首先检查IP和端口是否正确。其次确认防火墙是否屏蔽了TwinCAT或你指定的端口。最简单的方法是暂时关闭防火墙测试。如果是PLC硬件别忘了安装前面提到的“TCP/IP Test Tool”并重启。发送了数据但对方没收到 用调试助手确认数据是否真的从网卡发出看发送字节数。检查PLC程序中的cbLen参数是不是设成了0或者pSrc地址错了还有一个常见情况是网络字节序问题但对于我们这种发送原始字节的场景通常不会遇到。接收数据不全或乱码 这是最常遇到的问题。第一确认你的接收缓冲区arrReceiveData是否足够大。第二最重要FB_SocketReceive的cbLen参数是你希望读取的最大长度但功能块只会把当前Socket缓冲区里已有的数据读出来最多不超过cbLen。如果对方发了100字节但你调用接收时只到了50字节那nRecBytes就是50。你需要多次调用或者自己设计一个协议比如在数据包头包含总长度然后循环读直到收满。第三检查发送端和接收端对数据格式比如字符串编码的理解是否一致。连接意外断开处理 在实际项目中网络是不稳定的。你需要监控FB_SocketReceive或FB_SocketSend的bError输出。如果它们报错并且错误码提示连接已断开你就需要调用FB_SocketClose清理当前的_hSocket然后根据业务逻辑决定是否重连对于客户端或重新开始接受对于服务端。一个健壮的程序必须包含这些异常处理逻辑。6. 从Demo到项目高级应用与优化建议当你成功跑通了这个点对点的例子算是拿到了入门券。但在真实工业项目中我们需要考虑得更周全。多客户端连接怎么办上面的服务端例子是单线程的一次只能处理一个客户端连接。要处理多个客户端你需要创建一个FB_SocketAccept数组并管理一个T_HSOCKET句柄数组。每接受一个新连接就分配一个句柄和一套独立的发送/接收功能块实例。这通常需要用到动态内存分配或固定大小的连接池是进阶的内容。数据协议如何设计直接收发字节数组太原始了。你需要定义自己的应用层协议。一个简单的例子是“帧头数据长度数据内容校验和”的格式。发送方按这个格式打包接收方先读帧头确认帧开始再读长度知道要收多少数据然后收满指定长度的数据最后校验。这样可以有效解决TCP的粘包问题即多次发送的数据被一次接收到或一次发送的数据被分多次接收。性能与实时性考量 用TON定时器轮询接收的方式简单但实时性不高且占用扫描周期。对于高频率通讯可以考虑在PLC任务中设置更短的循环时间或者探索使用ADS通知或后台任务的方式来异步处理Socket事件但这需要更深入的TwinCAT系统知识。结构化数据发送 你不想每次都手动填充BYTE数组吧你可以定义STRUCT结构体然后使用MEMCPY函数将结构体拷贝到BYTE数组的发送缓冲区或者直接用ADR取得结构体地址作为pSrc发送。接收时反过来操作。这能让你的代码可读性大大提升。例如定义一个包含速度、位置、状态的结构体通讯就变成了直接传递这个结构体非常直观。最后别忘了资源管理。像FB_SocketConnect、FB_SocketAccept这类功能块在连接建立后除非需要改变连接参数否则不要让bExecute一直为TRUE更不要每个周期都给上升沿。保持bExecute为FALSE它们就不会占用不必要的处理时间。连接断开后确保相关的句柄被重置功能块实例回到初始状态为下一次连接做好准备。把这些细节处理好你的TCP/IP通讯程序就能在产线上稳定运行了。