资讯动态

PLC通信实战:Codesys 3.5 TcpSocket自定义TCP报文详解与调试

发布时间:2026/10/15 18:13:00 来源:尧图企业网站定制
做工业控制这几年用Codesys 3.5写程序最大的体会就是网络通信迟早会轮到你来碰。前阵子我接了一个项目PLC除了要采传感器模拟量还要把数据往一台第三方设备上推对方给了个私有TCP报文协议。当时第一反应是找现成的Modbus TCP功能块结果报文格式对不上在现场查了半天资料最后老老实实回到TcpSocket自己拼帧、自己解析。这个事做完之后我把Codesys 3.5里TcpSocket相关的用法、踩过的坑、排查流程整理了一下。这篇文章不是教科书就是把我实际操作中确认过能用的方法和容易出错的细节逐个拆开讲清楚。1. 项目拆解Codesys 3.5里TCP通信到底解决什么问题1.1 为什么需要在PLC里用TcpSocketPLC联网在现在的产线上已经非常普遍但很多时候大家用的都是现成协议比如Modbus TCP、Profinet或者其他总线。这些协议的好处是标准化功能块一拖就能用坏处也明显——报文格式是固定的遇到第三方设备只用私有协议、上位机要自定义数据帧、或者对接的系统只支持原始socket通信现成协议完全帮不上忙。我在那个项目里遇到的情况就是这样。第三方设备是某厂家的检测仪器它只接受一种自定义的TCP数据包帧头、设备号、命令字、长度、数据区、校验码都是自己定义的Modbus TCP根本没法传。这时候TcpSocket就成了唯一的选择。它相当于在PLC里开了一个原生的TCP/IP套接字通道你可以像平时在PC上写网络程序一样自己定义要发什么、怎么收底层连接管理交给TCP协议栈。用TcpSocket还有一个隐藏优势它不依赖具体应用层协议意味着只要TCP能通的设备理论上都能对讲。很多老设备或者嵌入式设备文档里写着“支持TCP/IP”但实际就是自定义报文。这种情况下用TcpSocket去适配比指望厂家改协议现实得多。1.2 TcpSocket与Modbus TCP、EtherNet/IP的边界经常有人问我既然TcpSocket这么万能是不是所有通信都该用socket我的回答是千万别。举一个生活化的例子Modbus TCP像是快递公司提供的标准包装盒你只要把东西放进去、填好单子快递员就能帮你运送而TcpSocket是给你一辆空卡车你想怎么装、往哪儿开都由你自己定。方便性和灵活性往往不可兼得。我一般在选型的时候会这样区分如果对端设备支持Modbus TCP、EtherNet/IP、OPC UA之类的标准协议优先用标准协议因为开发快、调试容易、社区资料多只有在对端只支持自定义TCP报文、或者标准协议满足不了数据交互需求的时候我才上TcpSocket。还要注意一点TcpSocket并不是和Modbus TCP对立的Modbus TCP本身就是跑在TCP之上的你甚至可以在socket层收到的字节流里自己解析Modbus TCP帧。只不过直接用现成功能块会省掉很多事。1.3 适用场景与选型判断根据我这些年经手的项目TcpSocket在Codesys 3.5里主要有几个典型场景PLC作为TCP客户端主动连接上位机软件或者MES系统上传数据。这是最常见的用法PLC间歇性地把生产数据打包发送。PLC作为TCP服务器等待设备或上位机来连接。适用于上位机主动读取、或者多条PLC由同一台监控站统一采集的场景。与第三方设备对接用对方定义的私有协议交互。这类通常双方都是“你说的算”发送和接收都必须按对方文档逐字节搞定。长连接数据流传输比如视觉系统输出检测结果、读码器上传码值很多这类设备提供的也是纯socket接口。场景清楚了后面就是选型问题。如果只是和自家HMI通信没必要自找麻烦如果是要对接外部系统且文档里明确给了“Socket通信接口”那就果断用TcpSocket。我的经验是判断标准就是一条通信双方要传输的内容是否需要自己组织字节流。需要就上TcpSocket不需要就优先考虑标准协议。2. 环境准备加载库文件与控制器网络检查2.1 先别急着写代码确认控制器网卡和IP很多人上来就在Codesys里拖功能块结果连不上最后发现PLC的物理网卡都没启用。这个我踩得多了。Codesys 3.5运行的环境很杂有IPC上的软PLC有嵌入式控制器有带Windows CE的工业电脑。不同硬件对网络接口的支持不一样有的控制器有多个网口但只能配置第一个作为应用程序通信。所以项目一开始先去控制器设备视图中把网络接口配置好确认IP地址、子网掩码、网关。很多Codesys控制器可以在设备描述里直接配置网络接口也有的是在操作系统的网络设置里配。总之先保证从调试电脑ping得通控制器再谈socket连接。如果ping都失败后面所有调试都是白费。另外要注意Codesys本身在线编程也走TCP/IP所以如果你能用Codesys在线登录控制器那说明网卡和IP大概率没问题。这时候可以把精力放到Socket功能块本身上。2.2 在库管理器里挂载Socket库Codesys 3.5最大的特点之一就是功能可扩展Socket通信不是内置在标准函数库里的需要额外添加库文件。不同厂商的控制器预装情况不同有些控制器厂商已经在系统里集成了Socket库有些则需要你手动添加。具体操作很直观在工程树里双击“库管理器”然后在“添加库”的搜索框里输入Socket你会看到跟Socket相关的库一般名字就是“Socket”或者类似的关键词。选中最新的可用版本点击添加。添加完成之后在“POUs”或者功能块列表里搜索能看到TcpClient、TcpServer、TcpListen、TcpAccept、TcpSend、TcpReceive等函数块。这里有个经验尽量选控制器厂商专门适配过的库版本有的库在特定CPU上会有不兼容现象。如果找不到Socket库去控制器厂家的官网下载对应设备包并导入这是最常见的处理方式。2.3 工程设置里要提前避开的坑库加好只是开始。Socket通信和普通逻辑扫描有个很大的不同TCP连接是异步的连接建立、数据到达都需要时间而PLC程序是周期扫描的。所以如果你在同一个扫描周期里既发连接指令又立刻检查连接状态大概率会失败。我曾经在工程里犯过一个低级错误把TcpConnect和TcpSend写在同一个IF块里指望一次扫描就完成连接和发送。结果当然是失败。后面我学乖了把TCP通信做成一个独立的状态机一个扫描周期只推进一个状态连接未建立之前绝不发数据。还有一点要提前注意几乎所有的Socket库都要求你先定义好发送和接收缓冲区。缓冲区类型通常是ARRAY OF BYTE这样能精确控制每一个字节。如果直接用STRING或者WORD数组后面做字节序转换和校验码计算时会非常难受。3. 核心实现TCP客户端和服务器的状态机设计3.1 客户端模式实例代码拆解客户端模式是最常用的。下面这段ST代码是我在一个项目里实际跑通过的思路去掉业务逻辑只保留核心通信流程。不同库的功能块接口名会有差异但思想是一致的先连接再判断握手成功最后才收发数据。VAR fbClient : TcpClient; xConnReq : BOOL; xConnOpen : BOOL; xConnClosed : BOOL; xError : BOOL; wErrorCode : WORD; sServerIP : STRING(15) : 192.168.1.101; uiServerPort : UINT : 5001; bySendBuf : ARRAY[0..255] OF BYTE; byRecvBuf : ARRAY[0..255] OF BYTE; xSendReq : BOOL; xRecvReq : BOOL; udiRecvLen : UDINT; udiSendLen : UDINT; eConnState : BYTE; // 0空闲 1连接中 2已连接 3关闭 END_VAR在程序区里我用一个简单的状态机控制连接过程CASE eConnState OF 0: // 空闲状态触发连接 IF xConnReq THEN fbClient(xConnect : TRUE, xClose : FALSE, ipAddress : sServerIP, uiPort : uiServerPort); eConnState : 1; xConnReq : FALSE; END_IF 1: // 等待连接完成 IF fbClient.xConnected THEN eConnState : 2; ELSIF fbClient.xError THEN wErrorCode : fbClient.wErrorCode; eConnState : 3; END_IF 2: // 已连接执行接收和发送业务 // 接收处理 IF xRecvReq THEN fbClient(xConnect : FALSE, xClose : FALSE, ipAddress : sServerIP, uiPort : uiServerPort); udiRecvLen : fbClient.Receive(byRecvBuf); xRecvReq : FALSE; END_IF // 发送处理 IF xSendReq THEN fbClient(xConnect : FALSE, xClose : FALSE, ipAddress : sServerIP, uiPort : uiServerPort); udiSendLen : fbClient.Send(bySendBuf); xSendReq : FALSE; END_IF 3: // 错误状态复位后再重新连接 IF xConnReq THEN fbClient(xConnect : FALSE, xClose : TRUE); eConnState : 0; xConnReq : FALSE; END_IF END_CASE这段代码不能说直接照抄就完事因为库的接口差异很大但状态机骨架是通用的。核心思路是连接指令只触发一次然后在后续扫描周期里不断查询连接状态等到真正连接上再去收发。很多新手就是在这一步混乱反复触发连接导致socket资源被耗光。3.2 服务器模式监听、接收、处理服务器模式相对难一点因为它需要三个角色共同协作一个监听器、一个连接对象、一套收发缓冲。原理上PLC作为服务器时会先打开一个监听端口客户端连进来后库内部会建立一个会话实例之后你和这个会话做收发。我常用功能块组合是这样的TcpListen负责监听端口TcpServer负责管理单个客户端连接再配合TcpAccept主动接收新连接。流程是这样的启动TcpListen指定监听端口比如5001每个扫描周期检查TcpListen的新连接事件当有客户端连接请求时调用TcpAccept建立会话会话建立成功后在会话功能块上执行Send/Receive。一个容易忽视的细节服务器模式下的接收网络数据可能是分片到达的。客户端发来100字节但PLC一次Receive可能只拿到40字节。所以需要把接收到的数据放入一个累积缓冲区然后按自定义协议去判断“是否已经收到完整的一帧”。如果协议设计得不好这个问题会在现场让你头疼很久。3.3 协议帧设计与字节序处理TcpSocket把字节流交给你协议的完整定义就得自己做了。我给自己定了一个比较实用的通用帧格式帧头固定2字节比如0xAA 0x55 设备地址1字节 命令字1字节 数据长度2字节大端 数据区N字节 校验码1字节异或全部。这里最关键的是字节序。TCP/IP协议规定的网络字节序是大端也就是高字节在前。如果设备端是基于Windows或者Linux的小端系统直接内存拷贝byte数组就会有字节顺序错乱。所以我在代码里写了一个简单转换函数把WORD转成两个BYTE、把DWORD转成四个BYTE发的时候手动把高字节放在前面。// WORD转两个字节大端模式 byHigh : WORD_TO_BYTE(shrHandle(wTemp, 8)); byLow : WORD_TO_BYTE(wTemp AND 16#00FF);接收端的解析正好相反把收到的两个字节重新拼接成WORDwTemp : SHL(BYTE_TO_WORD(byRecvBuf[3]), 8) OR BYTE_TO_WORD(byRecvBuf[4]);这些活儿看着简单但在Codesys里如果直接用WORD数组去转符号位和隐式转换问题会让人崩溃。所以我在项目里的建议是所有网络数据缓冲区都用BYTE数组手动控制每一个字节不要依赖编译器自动转换。4. 调试实战抓包、状态判断与断线重连4.1 用Wireshark确认TCP三次握手当PLC和对方设备连不上时不要先急着怀疑自己的功能块配置。我一般的排查顺序是先用Wireshark在电脑上抓包确定TCP握手是否真的发生。操作方法很简单把PLC和电脑接到同一个交换机上在电脑上设置好抓包网卡使用过滤规则tcp.port 5001然后让PLC发起连接。如果能看到SYN、SYN-ACK、ACK三条报文说明TCP层已经通了问题大概率出在应用层报文格式或者功能块逻辑上。如果只看到SYN重试那就是对端IP不通、端口没开放或者被防火墙挡了。有一次我调试PLC连接数据库网关在Wireshark里发现PLC不断发SYN但对方没有回应。查了半天发现对方服务器运行在另一台机器上端口虽然相同但IP配置错了。Wireshark很快就能定位这种问题比在Codesys里反复看错误码高效得多。4.2 常见错误码与状态字判断Socket库的错误码各家定义不太一样但含义基本一致。最常见的几个可以归类总结症状可能原因处理方向连接超时IP不可达、端口未开、防火墙拦截检查物理链路和防火墙连接被拒绝端口没有服务监听确认服务器是否启动、监听地址是否正确发送失败连接已经断开、缓冲区未丢检查连接状态主动重连前先关闭接收超时对端长时间无数据在程序中增加看门狗或心跳超时处理我习惯在功能块里读取状态字和错误码把它放到一个监控变量里。这样通过Codesys的在线监控或者Trace一眼就能看出连接处于哪个阶段。不要只在通信异常时才去翻错误码一定要做成持续监控因为现场的不稳定是间歇的反应过来时故障可能已经消失了。4.3 断线重连的看门狗设计TCP有一个很坑的特性物理网线断开时连接不会立刻关闭TCP协议栈还会尝试重传若干次期间你是无法知道对端已经失联的。所以断线检测不能只依赖TCP自身的状态要在应用层做心跳。我在项目里的做法是在协议里加一个心跳命令。PLC作为客户端每隔5秒发一个心跳帧如果连续3个心跳周期没有收到对端回复就认为连接失效然后主动关闭socket等待2秒后重新触发连接。这个逻辑写在状态机里并不复杂// 心跳超时计数 IF heartBeatAckReceived THEN heartbeatCnt : 0; ELSE heartbeatCnt : heartbeatCnt 1; END_IF IF heartbeatCnt 3 THEN xForceClose : TRUE; // 强制关闭当前连接 END_IF // 关闭完成后进入重连状态 IF xClosed AND xForceClose THEN eConnState : 0; xForceClose : FALSE; END_IF这里最关键的是关闭动作一定要等真正关闭完成再进入重连。如果socket正处于ESTABLISHED但实际已经无法通信的状态你直接尝试重连通常会报“地址已占用”或“连接仍存在”。处理办法是在重连之前调用Close功能块并且轮询功能块内部状态控制直到它报告连接已完全关闭再开始下一次连接流程。5. 常见问题速查与避坑经验5.1 典型问题汇总表下面这张表是我用TcpSocket这几年最常遇到的几个问题基本涵盖了新手期一大半的崩溃现场。问题现象根本原因解决方法连接一直处于“连接中”对方服务器未启动或IP配置错先ping再telnet端口测试程序下载后socket卡死旧通信不在初始状态复位应用或重启控制器发送数据对方收到乱码字节序、编码不一致统一使用BYTE数组并手动大端转换接收数据总是少字节没有处理TCP粘包拆包自定义协议长度字段循环接收完整帧PLC重启后不会自动连上连接触发在初始化前执行把连接触发放在状态机上延迟启动两个客户端同时连接失败服务器功能块不支持多连接增加会话实例或者改用更强大的库5.2 我踩过的几个隐蔽的坑第一个坑就是使用STRING当网络缓冲区。Codesys的STRING不是一个裸的char数组它自带长度前缀且受字符集影响。当初我直接用一个STRING变量接收TCP数据结果收到的字节有变化我拿去算校验怎么都算不对。后来改成ARRAY OF BYTE问题瞬间解决。第二个坑是功能块实例重复使用导致状态串扰。我在一台设备上同时连了3个不同的服务器图省事只建了一个TcpClient实例一个周期里对三个目标分别赋值。结果后一次赋值覆盖了前一次连接请求经常出现第一个连接刚建立又被关闭。最后老老实实为每一路通信单独建立功能块实例彻底解决。第三个坑是“下载应用程序后socket资源不释放”。Codesys在线下载程序时旧任务的socket可能不会自动关闭导致新程序启动后报端口被占用或资源不足。遇到这种情况我一般是先停止应用再下载然后重新运行。或者在程序初始化阶段先对所有socket对象执行Close并延时等待确保进入干净状态。第四个坑是缓冲区太小导致接收不完整。我最初定义了一个64字节的接收缓冲结果对方一帧报文超过64字节后面的数据直接被丢弃或者缓存到下次。这里一定要根据协议里最大数据长度来定义缓冲区不能拍脑袋。而且Receive能返回实际接收字节数必须判断返回值再处理不要假设一次接收就是完整的一帧。5.3 提升通信可靠性的几条建议现场跑久了我慢慢总结出一条规律TCP通信写出来容易但稳定运行很难。想减少后期维护的心力可以从这几个方面下手。第一协议里必须带长度字段和校验字段。长度字段用于判断一帧是否完整校验字段用于验证数据是否被传坏。这两个字段缺一不可否则遇到干扰时你光靠接收端是发现不了问题的。第二在程序里做接收超时看门狗。不仅是心跳超时比如接收一帧数据如果超过N毫秒没有收到后续字节就丢弃半包数据重新等待帧头。这样能防止系统因为一个半截报文而长期卡死。第三通信功能块尽量放在独立Task里并且不要和高速运动控制放在同一个临界优先级。socket收发本身比较耗时如果放在高优先级任务里会影响运动控制的稳定性。通常我会把网络通信放到一个周期10-50ms的独立循环任务中和主逻辑任务隔开。第四所有调试用的IP、端口不要直接写死在代码里而是做成符号常量或者上电初始化的全局变量。现场经常要改对端IP如果代码里到处是常量改起来容易漏。最后说点实在的TcpSocket在Codesys 3.5里并不是一个“新增功能”很多库文件已经存在很久了但真正用好的前提是你对TCP/IP协议本身有感觉。我在这个项目里最深的一个体会写PLC程序的人不能只把网络通信当成几个功能块的堆叠更要理解连接是异步的、数据是流式的、缓冲区是有限的。希望这篇分享能帮你少走一些弯路。如果你们在实际项目里也碰到过TcpSocket的怪问题欢迎一起交流毕竟这种底层的通信坑往往不是一个人能全部踩完的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑