
1. 项目概述为什么我选择让EASY系列PLC当socket主站先交代一下背景。我手头有一台汇川EASY系列PLC具体型号是EASY320平时主要是做现场设备的逻辑控制。这次项目有一个特殊需求现场有一批第三方设备它们只开放了基于TCP/IP的socket接口既不是Modbus TCP也不是Profinet这类工业总线协议而是最原始的——上位机或者控制器直接往指定IP和端口发报文设备按报文格式返回数据。这种情况下很多人的第一反应是加一个协议转换网关或者用上位机做中转。但现场条件不允许加额外硬件而且上位机中转有个致命问题一旦上位机死机或者网络抖动整个链路就断了设备直接失控。所以我把目光放在了PLC本身——汇川EASY系列是否具备socket通信能力能不能直接作为TCP客户端也就是主站去主动连接远端设备答案是肯定的。EASY系列虽然定位是小型PLC但它的以太网口支持原生socket编程可以在程序里创建TCP连接主动向服务器发起请求收发自定义报文。这个能力比想象中重要得多它意味着PLC可以绕过协议转换器直接对接各种只提供socket接口的设备——比如视觉相机、扫码枪、称重仪表、第三方控制器甚至是一些老旧的工业设备。这篇文章就把我从零开始到最终让EASY320作为socket主站跑通全流程的经验完整记录下来。内容包括开发环境搭建、socket功能块怎么调、报文怎么组、数据怎么解析、踩了哪些坑以及最后实际运行的稳定性表现。如果你也在用汇川EASY系列想走以太网socket通信这篇文章可以直接当参考手册用。2. 环境准备与硬件接线动手之前必须确认的事2.1 硬件清单和固件版本先说硬件。汇川EASY系列目前主流的几款型号包括EASY320、EASY521等都自带以太网口。我用的EASY320网口是标准RJ45支持10/100M自适应物理上就是一个普通网口接线和连电脑网口一模一样。有一点需要特别提醒EASY320的以太网口是支持socket通信的但必须确认PLC固件版本不能太低。我第一次拿到设备时固件版本是V1.0程序里怎么也找不到socket相关的功能块后来查资料发现socket库是在较新的固件版本里才集成的。升级固件到V1.2之后功能块才出现。所以拿到设备第一件事先看一下固件版本最好升级到最新正式版。2.2 网络拓扑设计这个项目的拓扑非常简单PLCEASY320IP地址设为192.168.1.10远端设备IP地址为192.168.1.50端口号5020这里是自己定义的例程端口交换机普通工业交换机也可以用网线直连如果是现场有多台设备要同时通信建议把PLC、设备、上位机都放在同一个网段避免跨网段路由带来的延迟和配置麻烦。我的习惯是PLC使用固定IP不要用DHCP这样重启后地址不变程序里连接目标IP也不用改。2.3 开发软件与功能块库EASY系列使用汇川的InoProShop软件进行编程软件基于CODESYS平台如果你用过Codesys上手基本无缝。打开软件后新建工程时需要选择对应的PLC型号然后会加载对应的设备描述文件。这里重点说socket相关的库。在InoProShop的库管理器里需要手动添加一个名为Ethernet Communication或者类似名称的库具体名称因版本而异通常在库列表里可以搜到Socket相关关键字。添加之后程序里就能调用TCP连接、发送、接收等功能块了。库的版本和PLC固件版本要匹配否则编译会报错或者运行时行为异常。我调试时因为库版本太新、固件版本太老曾经出现了连接功能块一直返回超时的问题后来重新下载匹配版本的库才解决。3. Socket主站功能块详解每一个接口都必须搞清楚3.1 核心功能块一览EASY系列socket库提供了几个核心功能块我实际用到的有这几个功能块名称功能关键参数Socket_Open建立TCP连接ServerIp目标IP、ServerPort目标端口、ConnectTime超时时间Socket_Send发送数据Buff数据缓冲区、Len发送长度Socket_Recv接收数据Buff接收缓冲区、Len接收长度Socket_Close关闭连接无这几个功能块的调用方式和普通的功能块类似每个功能块都有EN使能和ENO输出使能外加各种输入输出参数。使用前先在程序里实例化然后按顺序调用。3.2 建立连接的完整流程TCP通信的特点是面向连接所以第一步永远是建立连接。在实际程序中我用了一个布尔变量b_ConnectReq作为连接请求信号上升沿触发Socket_Open功能块IF b_ConnectReq THEN Socket_Open_Instance( EN : TRUE, ServerIp : 192.168.1.50, ServerPort : 5020, Timeout : 3000 ); END_IF注意几个细节ServerIp是字符串类型必须写完整的IP地址注意引号Timeout的单位是毫秒我习惯设置为3000也就是3秒。如果3秒内连不上功能块会把Done或者Error标志位置位方便程序里判断。连接是否成功不能只看功能块有没有被调用还要看输出参数。Socket_Open输出里通常有Done、Busy、Error这几个标志。Done为TRUE表示连接成功Error为TRUE表示失败同时会有一个错误码输出根据错误码可以定位问题。我把这些状态都存到DB变量里方便上位机监控。3.3 数据发送与接收连接建立之后就可以收发数据了。发送功能块Socket_Send的核心输入是一个缓冲区缓冲区本质上是字节数组。在CODESYS体系里我通常用ARRAY [0..255] OF BYTE来定义缓冲区。发送逻辑是这样的// 先将需要发送的报文按字节填入缓冲区 Send_Buffer[0] : 16#01; // 设备地址 Send_Buffer[1] : 16#03; // 功能码 Send_Buffer[2] : 16#00; // 起始地址高字节 Send_Buffer[3] : 16#00; // 起始地址低字节 // ... 按设备协议填充 // 调用发送功能块 Socket_Send_Instance( EN : TRUE, Buff : ADR(Send_Buffer), Len : 8, Done Send_Done, Error Send_Error );最关键的一点是Len参数它告诉功能块要发送多少个字节。这个值必须和Buff里实际填充的数据长度一致多填或者少填都会导致报文解析错误。接收数据相对简单一些Socket_Recv同样传入一个缓冲区功能块接收数据后就往里写。需要特别注意的是TCP是流式协议一次Recv收到的数据长度不固定不能假设一次就能收完整一帧报文。我通常的做法是在接收完成标志位触发后检查实际接收到的字节数再根据协议去解析。3.4 多设备通信时的实例化管理有些项目里PLC需要同时和多个socket设备通信。这时候有两种方案一种是轮询式的一个时刻只处理一个设备的收发另一种是并行式的为每个设备单独实例化一组socket功能块。我在实际项目中发现小型PLC的算力有限并行处理多个socket连接容易造成CPU负荷偏高。我的建议是如果设备数量不超过3台可以直接并行实例化如果超过3台最好用轮询的方式在一个状态机里分时处理每个设备的收发这样可以大幅降低PLC扫描周期的压力。4. TCP连接状态机设计从连接到稳定通信的关键4.1 为什么需要一个状态机TCP通信不是调用一次功能块就完事了那么简单。真实场景里设备随时可能重启、网线可能断开、PLC可能需要重连。如果只是线性地调用连接-发送-接收一旦链路中断程序就卡死了。我采用的方法是设计一个简单的连接状态机。状态机的核心思路是把通信过程拆分成几个有限状态每个状态对应一个明确的动作状态之间通过条件切换。这样程序的逻辑会非常清晰也方便排查问题。4.2 状态机状态定义与切换条件状态含义进入条件退出条件IDLE空闲初始状态或连接断开后收到连接请求进入CONNECTINGCONNECTING连接中发送连接请求连接成功进入RUNNING或超时/失败回到IDLERUNNING通信中连接成功发送/接收出错或链路断开回到IDLE具体实现时我用一个整数变量state来表示当前状态用CASE语句来实现状态切换。下面是一个简化的状态机框架CASE state OF 0: // IDLE IF b_ConnectReq THEN state : 10; // 进入连接状态 END_IF 10: // CONNECTING // 调用Socket_Open // 如果Done则state : 20 // 如果Error则state : 0并记录错误码 20: // RUNNING // 调用Socket_Send和Socket_Recv // 如果Error则state : 0并触发重连 END_CASERUNNING状态内部我还会再细分为发送等待和接收等待两个子状态避免发送和接收同时进行导致缓冲区冲突。4.3 断线重连与异常处理这部分是现场运行最关键的。TCP连接断开后如果程序不处理PLC会一直傻等导致整个逻辑停滞。我的处理策略是在RUNNING状态中如果检测到发送超时、接收超时或者功能块Error标志立即关闭连接将状态切回IDLE。IDLE状态下延迟3秒后自动重新发起连接。延迟的目的是给远端设备留出恢复时间也避免频繁重连导致网络风暴。重连次数不做限制一直重试。但为了防止无限循环刷屏每次重连间隔至少3秒。这个策略在我现场运行了三个月非常稳定。设备重启、网线被误拔PLC都能在几秒内自动恢复通信整个过程不需要人工干预。4.4 状态机与主程序的配合状态机放在PLC主程序的某个周期任务里执行任务周期我设置的是10ms。这种周期设置可以保证socket收发的实时性同时不会因为任务过于频繁而影响其他逻辑。在实际工程中状态机的运行状态、当前错误码、收发数据计数我都映射到了Modbus寄存器中这样上位机或者HMI可以实时查看通信链路的健康状况。排查问题的时候直接看状态变量比翻程序快得多。5. 报文构造与数据解析跨过协议这道坎5.1 报文格式的确定socket通信最大的坑就是报文格式完全由设备厂商自定义。拿到第三方设备的协议文档时首先看清楚几个信息字节序是大端高字节在前还是小端低字节在前帧头帧尾有没有固定的起始符和结束符长度字段报文长度是固定值还是包含在报文内校验方式CRC、LRC、校验和等以我这次对接的设备为例它的协议是这样的字段长度说明帧头2字节固定为16#AA 16#55命令字1字节01表示读02表示写数据长度1字节数据区长度数据区可变实际数据CRC162字节CRC校验低字节在前5.2 组帧与循环冗余校验的实现组帧在PLC程序里本质上就是填充字节数组。需要注意的一点是在IEC61131-3编程环境里数组偏移是从0开始的而协议文档里通常说第1字节、第2字节心里要有个映射关系免得对应错。CRC16在PLC里需要写一个小功能块来实现。因为CRC计算是逐字节的位操作在PLC中实现时需要注意数据类型的位宽推荐使用BYTE、WORD类型进行操作避免用到INT时出现符号位的问题。我之前在组帧时犯过一个低级错误CRC功能块算出的校验值和设备的校验值永远对不上。排查了很久才发现原来是CRC算法的初始值和结果异或值设错了设备文档里写的是初始值0xFFFF、结果异或0x0000我软件里的默认参数却是初始值0x0000。所以组帧之前一定要先把CRC参数核对清楚。5.3 接收数据的拆分与转换接收到的一串字节需要按协议拆分出各个字段。在CODESYS里我习惯先把接收到的字节数组拷贝到一个独立的解析数组里然后用指针或者数组偏移去提取数据。这里有一个非常常见的问题多字节数值类型比如16位整数的高低位顺序。收到的字节可能是高字节在前也可能是低字节在前必须协议文档为准。现场调试时可以用一个已知数据去验证——比如让设备返回一个已知值然后看PLC解析出来的数值是否正确。如果发现数值明显不对比如读数大得离谱十有八九是字节序处理反了。另外浮点数的解析更麻烦。EASY系列的PLC和第三方设备之间如果涉及到浮点数传输需要确认双方的浮点字节序是否一致。常见的是IEEE754大端但有些设备会按小端存储这样解析出来完全是乱码。我的建议是如果有条件最好在设备侧先设置成与PLC一致的字节序如果设备侧不可配置PLC里就需要做一个字节交换的处理把32位浮点的四个字节顺序调整一下再接成REAL。6. 实操过程从新建工程到第一个Socket报文跑通6.1 新建工程与初始设置打开InoProShop新建工程选择PLC型号。我选的是EASY320软件会自动加载对应的设备信息和基础库。在工程树里双击Program进入主程序编辑区。在写socket程序之前先做几个基础设置PLC的IP地址在设备配置页里把PLC的IP设置为固定地址我这里设为192.168.1.10子网掩码255.255.255.0。通信周期设置默认情况下CODESYS的Task周期是10ms这个周期对socket通信来说足够了不需要改。库引用在Library Manager里添加socket通信所需的库。把库添加好之后在线编译确认没有错误再下载到PLC。这个过程是纯环境准备阶段但要仔细检查编译输出如果库版本不匹配编译会提示缺少某个功能块这时候就要换库版本了。6.2 写一个最简单的连接程序在验证socket之前我建议先写一个最简程序只做一件事连接服务器连接成功后把状态写到某个变量里。这个程序足够简单方便隔离问题。下面是我当时的测试代码片段VAR fbOpen : Socket_Open; bConnectReq : BOOL; bConnected : BOOL; sTargetIP : STRING : 192.168.1.50; nTargetPort : WORD : 5020; END_VAR // 主程序 bConnectReq : TRUE; // 上电自动连接 IF bConnectReq THEN fbOpen( ServerIp : sTargetIP, ServerPort : nTargetPort, Timeout : 3000, Done bConnected ); END_IF把这个程序下载到PLC运行后观察bConnected变量。如果变成TRUE说明连接建立成功。如果一直是FALSE先检查IP配置是否正确再检查目标设备是否在监听、防火墙是否封了端口。6.3 测试Socket服务器的选择在没有真实设备时可以先用PC上的网络调试工具模拟一个TCP服务器方便调试PLC的连接和数据收发。我当时用的是SocketTool和网络调试助手这类软件在电脑上创建了一个TCP Server监听5020端口然后让PLC去连接。这样就能在电脑上直观看到PLC发过来的报文内容也可以手动下发数据模拟设备返回。这个方法强烈推荐大家先做一遍。它有几个好处一是可以确认PLC的socket功能是否正常二是可以仔细检查自己组装的报文是否符合协议三是在真实设备因为各种原因连不上的时候可以单独验证PLC侧的程序逻辑缩小问题范围。6.4 把发送和接收加进来连接跑通之后再把发送和接收逻辑加进去。发送的数据不能死填要按设备的协议周期性地发送请求。接收的数据要实时检查并解析。下面是我当时调试时用的发送接收逻辑的简化版// 每500ms发送一次读命令 IF b_Connected AND (b_TimeUp) THEN // 组帧 Send_Buffer[0] : 16#AA; Send_Buffer[1] : 16#55; Send_Buffer[2] : 16#01; // 读命令 Send_Buffer[3] : 16#02; // 数据长度 Send_Buffer[4] : 16#00; // 地址高 Send_Buffer[5] : 16#01; // 地址低 // 计算CRC CRC16(Buff : ADR(Send_Buffer), Len : 6, CRC : CRC_Value); Send_Buffer[6] : CRC_Value MOD 256; // CRC低字节 Send_Buffer[7] : CRC_Value / 256; // CRC高字节 // 发送 Socket_Send_Instance(Buff : ADR(Send_Buffer), Len : 8); END_IF注意这里我用了一个时间标志b_TimeUp它的作用是控制发送频率。TCP通信一定要控制报文发送频率不能每个扫描周期都发否则远端设备会被淹没。我一般用TON定时器每500ms产生一个脉冲用来触发一次发送。6.5 接收数据的处理流程接收数据时我的处理流程是判断Socket_Recv的Done标志是否有新数据到达。读取实际接收的字节数存入n_RecvLen。把接收缓冲区的内容复制到一个独立的解析数组中。校验帧头和CRCCRC不对直接丢弃等待下一帧。CRC校验通过后按照协议字段依次解析出各个数据。之所以要把接收缓冲区的内容复制出来是因为socket功能块的接收缓冲区在下一次Recv调用时会被覆盖如果不及时复制数据就会丢失。在实际项目中这段复制逻辑可以用MEMCPY或者循环赋值实现。7. 常见问题与排查技巧实录我踩过的坑都在这里了7.1 问题速查表我在整个调试过程中遇到过不少问题我把它们整理成一个速查表方便后来者快速定位现象可能原因解决办法连接功能块一直返回超时IP地址配置错误检查PLC IP、目标IP、子网掩码连接功能块一直返回超时目标设备未启动监听确认设备端服务器已运行端口已开放连接立即建立但发送后无响应发送频率过高增加发送间隔建议500ms以上接收缓冲区一直无数据目标设备未主动返回数据检查报文格式、命令字是否正确接收数据乱码字节序不对确认大小端模式调整数据解析顺序CRC校验一直失败CRC参数错误核对初始值、异或值、多项式运行一段时间后连接断开设备侧主动断开增加自动重连逻辑缩短重连间隔功能块编译不通过库版本不匹配下载与固件匹配的socket库版本7.2 bind: only one usage of each socket address这个报错的启发看标题里大家搜过这个错误bind: only one usage of each socket address (protocol/network address/port)这个报错大类是在电脑端做socket编程时出现的意思是某个IP和端口组合已经被另一个socket占用了。虽然这不是PLC侧的直接报错但调试时要特别注意端口冲突问题——比如PLC作为客户端连接设备时本地端口是系统自动分配的一般不会冲突但如果你同时开了多个网络调试工具占用了同一个本地端口就很容易遇到起不来的情况。在用电脑模拟服务器时如果你已经在一个软件里监听了5020端口再开另一个软件监听同一端口就会报这个错误。所以调试时要确保一个端口只能被一个程序监听。7.3 汇川EASY系列的独特注意点这套EASY系列PLC的socket功能我在实际使用中总结了几个专属注意点注意一socket功能块不能和普通时序逻辑混在一个程序段里不加区分地调用。因为socket功能块的执行等外部事件扫描周期内可能没有立即完成如果没有对Busy标志做处理程序逻辑会被拖慢。我的做法是把socket相关的调用放到独立的任务中这个任务的周期适当放宽比如20ms或50ms以保证功能块有充足的时间完成内部的状态处理。注意二多个设备通信时接收数据要加队列或者缓冲区管理。如果PLC同时连接多个设备设备返回数据的时间点不一致可能出现数据交叉覆盖。我为每台设备分配了独立的接收缓冲区并且保证在一次任务周期内只处理一个设备的接收数据其他设备的数据先存入缓冲区下一周期再处理。注意三不要使用全局变量来传递socket数据。这个是我踩过的最惨的坑。因为socket数据量大、刷新频率高如果用全局变量传递容易导致数据在不同任务之间被覆盖。正确的方式是每个功能块实例都定义自己的输入输出变量通过接口参数传递数据流清晰可控。7.4 现场排查的几个经验现场问题往往比实验环境复杂。我总结了几个排查经验的优先级顺序第一先看物理链路。网线有没有插紧、交换机端口指示灯是否正常、设备有没有上电这些问题虽然基础但现场一半的通信故障其实是物理链路问题。第二再用PC模拟设备验证PLC程序。拔掉设备的网线把电脑连到PLC的网口上用网络调试助手模拟设备看PLC程序是否能正常收发。这样可以确认PLC侧的程序是否正常把问题缩小到设备侧。第三抓包。如果PC和设备同时出现在网络里可以用Wireshark抓包查看PLC发出的报文和设备返回的报文内容。一旦看到报文的具体字节内容很多问题就一目了然了。比如CRC不对、字节序反了、报文长度不对这些都可以从抓包里直接看出来。8. 文中用到的关键代码汇总为了方便直接抄作业我把几个关键的功能块定义代码汇总在这里。功能块实例化VAR // Socket功能块实例 fb_Open : Socket_Open; fb_Send : Socket_Send; fb_Recv : Socket_Recv; fb_Close : Socket_Close; // 连接参数 s_ServerIP : STRING : 192.168.1.50; n_ServerPort : WORD : 5020; // 数据缓冲区 arr_SendBuf : ARRAY [0..255] OF BYTE; arr_RecvBuf : ARRAY [0..255] OF BYTE; n_SendLen : INT; n_RecvLen : INT; // 状态标志 b_IsConnected : BOOL; b_SendDone : BOOL; b_RecvDone : BOOL; b_Error : BOOL; n_ErrorCode : WORD; END_VAR状态机主逻辑CASE n_CommState OF 0: // IDLE - 等待连接 IF b_ConnectReq THEN n_CommState : 10; END_IF 10: // CONNECTING fb_Open( ServerIp : s_ServerIP, ServerPort : n_ServerPort, Timeout : 3000, Done b_IsConnected, Error b_Error, ErrorCode n_ErrorCode ); IF b_IsConnected THEN n_CommState : 20; ELSIF b_Error THEN n_CommState : 0; // 延时3秒后重试 END_IF 20: // RUNNING // 发送周期请求 // 接收数据解析 // 如果错误关闭连接回到IDLE END_CASECRC16计算功能块部分FUNCTION_BLOCK FB_CRC16 VAR_INPUT pData : POINTER TO BYTE; nLen : INT; END_VAR VAR_OUTPUT nCRC : WORD; END_VAR // 内部变量省略 // 实现标准Modbus CRC16算法注意初始值和结果异或值整个代码的核心思路就是实例化socket功能块用状态机控制连接状态通过独立的发送/接收缓冲区完成数据交互。这个框架可以套用到任何基于socket的第三方设备通信中只需要修改报文格式和解析逻辑即可。9. 稳定性优化与运行效果现场跑了三个月我才敢写这篇文章9.1 通信周期的选择socket通信的实时性取决于PLC的任务周期和网络延迟。EASY320的默认任务周期是10ms对于大部分socket通信场景这个周期已经足够快。但如果你需要更高实时性建议把socket通信放在单独的高优先级任务中并把任务周期设定在5ms。需要注意的是任务周期越短CPU占用率越高。我实际测试过当任务周期为10ms时CPU占用率大概在30%左右当任务周期缩短到5ms时CPU占用率上升到50%以上。所以除非迫不得已不建议把socket通信周期设置得太激进。9.2 数据安全的处理socket通信是明文传输的如果现场有网络安全要求建议在应用层做简单的加密或者至少加鉴权机制。比如在报文中加入设备ID和密码字段设备端校验通过后才响应。另外串口转以太网或者无线桥接的场景网络质量不稳定更容易出现数据丢包和乱序。这种情况下建议在报文协议中增加序号字段PLC侧检测到序号不连续时主动丢弃当前帧请求设备重发。9.3 运行效果这个项目上线后PLC和设备之间的socket通信已经连续运行了三个月没有出现过一次通信中断。设备的每次请求响应时间都在100ms以内完全满足现场工艺要求。PLC在设备重启、断网重连等异常场景下都能自动恢复通信稳定性达到预期。要说不足就是EASY320的内存空间有限如果接收缓冲区定义得太大会影响PLC的程序容量。我的建议是接收缓冲区按需定义在满足最大报文长度的前提下尽可能小一般256字节足够。10. 一些想提醒后来者的经验这篇文章写到这里核心内容基本都覆盖了。从环境搭建、功能块使用、状态机设计、报文解析到稳定性优化、常见问题排查可以说是一套完整的EASY系列socket主站通信方案。最后分享几个个人体会第一socket通信本身并不难难的是报文的适配和异常的鲁棒性处理。协议文档一定要反复读字节序、CRC、帧格式这些细节一个字母都不能错。第二调试阶段PC模拟工具是最好用的伙伴。PLC端的程序先用网络调试助手模拟验证不要一上来就接真实设备那样问题太多很难定位。第三PLC的socket通信虽然灵活但也带来了程序复杂度上升和CPU占用率增加的问题。在有标准工业总线协议可用的场景下优先选标准协议只有设备确实只开放socket接口时才走这条路线。希望这篇文章能帮你少走弯路也欢迎大家在实际项目中遇到问题随时交流。我的经验是只要把状态机和报文解析这两块理清楚EASY系列做socket主站这条路走起来会很顺。