干PLC通信调试这些年,汇川EASY系列我接触得不算少,但真正把它的以太网口拿来做socket从站、而不是单纯做Modbus TCP从站的项目,反而是最近两三年才多起来。原因也很简单,现场设备越来越需要“主动连接”“批量上传”“自定义帧交互”这类能力,标准的Modbus TCP虽然稳定,但灵活性很受限。这篇文章就把我自己的做法完整捋一遍,覆盖设计思路、PLC侧从站程序编写、上位机联调,以及一堆只有踩过坑才会注意到的细节。不管你是第一次听说socket从站这个概念,还是已经写了几版但总在某个环节卡住,这篇都适合你花几分钟过一遍。
1. 内容整体设计与思路拆解
1.1 为什么选择socket而不是Modbus TCP
很多人一提到PLC以太网通信,第一反应就是Modbus TCP。这东西确实好上手,HMI、上位机组态软件基本都原生支持,但真正做项目的时候你会发现它的局限挺明显。
Modbus TCP本质上是一个主从轮询协议,上位机作为主站不停地发请求,PLC作为从站只能被动应答。这种模型在数据量小、点位固定、采集周期不敏感的场合没什么问题,可一旦牵扯到变长数据、大批量数组上传,或者需要PLC主动上报故障信息,Modbus TCP就非常别扭。比如要上传一段几百字节的配方数据,用Modbus TCP就要拆成多个寄存器地址,反复读写,不仅慢,协议帧还显得臃肿。
socket从站就完全是另一套思路。它基于TCP/IP,PLC在以太网上创建一个TCP服务端,打开一个端口等着客户端来连接。一旦连接建立,数据交换是双向的、主动的,想发什么就发什么。帧格式可以自己定义,今天用固定长度结构体,明天换成JSON都行。对于汇川EASY系列这种小型PLC来说,这个能力其实是被很多工程师忽略的。
我自己的偏好很明确:只要能自己定义通信协议、数据量又不小、且不希望被轮询周期绑死的项目,我优先用socket从站方案,而不是硬塞进Modbus TCP的框架里。
| 对比项 | Modbus TCP从站 | Socket TCP从站 |
|---|---|---|
| 通信模型 | 主从轮询,只响应不主动 | 客户端-服务端,连接后双向收发 |
| 协议帧 | 固定Modbus帧结构,可读但冗余 | 自定义帧结构,按需设计 |
| 大数据包 | 需拆寄存器多次读写 | 一个帧直接承载任意长度数据 |
| 主动上报 | 基本实现困难 | 天然支持 |
| 开发门槛 | 低,现成控件多 | 略高,需自行约定协议 |
| 调试手段 | 简单 | 需要抓包工具辅助 |
1.2 从站角色的设计思路:为什么让PLC做服务端
这个项目标题里有个关键词是“从站”,放在socket语境下,我的理解就是PLC在通信里充当TCP服务端,上位机软件、触摸屏或者另一个控制器作为TCP客户端主动来连。
为什么要把PLC放在服务端位置,而不是反过来让PLC当客户端主动去连上位机?很多第一次做这个事的人会有疑问。我实际做下来的结论是:对于车间通信这类场景,上位机通常才是那条“主线”。MES、SCADA、视觉系统它们都是统一调度的角色,由它们发起到各台PLC的连接,逻辑上更顺,也更好管理。再者,如果PLC做客户端,就得让它知道上位机的IP和端口,上位机一旦换了IP或服务没启,PLC这边就要处理各种连接失败的重试逻辑,浪费PLC程序资源不说,排查问题也麻烦。
反过来,PLC做服务端的好处非常明显。PLC启动后在固定端口监听就行,不关心谁来连、什么时候连,客户端连不上可以自己重试。即使上位机软件崩溃重启,PLC端服务依然在,等客户端恢复后再次接入即可。这个模型下,PLC程序核心就三件事:建服务、等连接、处理收发数据,清爽很多。
我见过有的方案是用smart PLC做智能从站,把socket服务端跑在PLC里,对外提供稳定的数据接口,上位机侧再多的客户端都能同时接入,这种架构在产线设备数据集中采集时特别实用。
1.3 项目解决的痛点与适用场景
梳理清楚这套socket从站方案,本质上是为了解决三类项目的共性痛点:
第一类是设备数据需要主动上抛。比如设备发生故障停机时,PLC希望立刻把故障码推送给上位机,而不是等上位机下一轮轮询才能读到。
第二类是通信协议需要定制。有些上位机是C#、C++工程师写的,他们希望后端拿到的是一段带校验的原始数据帧,而不是把Modbus地址一一映射到数据库里。socket能让他们用最顺手的方式收发数据。
第三类是多客户端并存。一台PLC的数据可能要同时发给本地HMI、远程监控系统、扫码枪系统等多个接收方,socket服务端天然支持多连接,虽然EASY系列在连接数量上不会太多,但胜在每个连接都是独立通道,互不干扰。
这些场景合起来,就是该项目标题背后最常见的实际需求。如果你正好也遇到这几种情况之一,这篇文章能给你省下不少自己摸索的时间。
2. 硬件与软件准备:先确认你手头的EASY系列有哪些底牌
2.1 硬件选型与端口能力确认
做技术方案最怕的就是拿着型号臆想功能,结果到了现场发现硬件根本没这个能力。所以第一步一定是打开选型手册,确认你手里的汇川EASY系列PLC具体型号是不是自带以太网口、以太网口是否支持socket通信。
EASY系列是一个家族,不同子型号的网口能力并不完全一致。大部分带网口的型号都支持标准以太网通信,但个别低成本型号可能只支持工程下载和Modbus TCP,未必开放灵活的socket接口。这里我给一个非常实在的建议:直接去汇川官网下载对应型号的硬件手册,翻到“通信”章节,看通信规格表里有没有TCP、UDP、Socket字样,有的话再放心往下做。
另外要留意网口类型。大部分EASY系列用的是标准RJ45百兆口,用普通网线就能连。接法上,如果PLC和上位机直连,建议用直通线也行,现在绝大多数网口都支持自动翻转;如果走交换机,那就更无所谓了。要注意的是,网口指示灯状态一定要会看,Link灯不亮就说明物理链路都有问题,别急着查程序。
硬件层面还有一个容易被忽略的指标——CPU处理能力。小型PLC的socket通信解包、组帧都是需要CPU参与的工作,如果程序里还有大量运动控制或者高速逻辑,CPU负荷一高,通信响应就会变慢。所以做方案时最好把socket从站的数据量控制在合理范围,比如每秒几十次交互完全没压力,但如果你要它每秒收发几千包数据,那就不现实了。
2.2 编程软件与Socket指令库
汇川EASY系列对应的编程软件,不同时期的型号配套会有差异。老一批的EASY型号用AutoShop体系比较多,新一些的或者中大型系列则更常见InoProShop,但无论哪一套,socket通信的编程思路本质上都是相通的。我的建议是,开工前先把你软件的指令手册电子版下载到本地,搜关键字“socket”“TCP”“通信”这几个词,看清楚软件提供哪些现成功能块。
很多工程师一上来就想自己造轮子,从零写TCP协议栈,这完全没必要。官方提供的socket功能块或者通信库一般至少包含这几个能力:创建服务端、监听端口、等待客户端连接、接收数据、发送数据、关闭连接。你把它们当成串口通信里的初始化、读、写就行,只是底层从RS485换成了以太网。
我实际用下来,更推荐用汇川官方提供的通信例程或者封装好的协议库,而不是完全裸调socket底层接口。因为你从零写很可能漏掉连接管理、超时处理这些繁琐环节,而官方库这些逻辑都已经处理过,可靠性高得多。
具体用ST语言还是梯形图,取决于个人习惯。ST语言写通信状态机更直观,尤其是多状态跳转的时候,梯形图会变得非常臃肿。如果你对ST不太熟,建议借这个项目顺便练一下,通信程序用ST写真的比梯形图舒服太多。
3. PLC侧从站程序设计:核心配置与实操步骤
3.1 配置IP与端口:从站的关键起点
PLC做socket从站,第一件事是固定一个IP地址,别用DHCP。现场交换机一旦重启,DHCP分配的地址变了,上位机就找不到了。固定IP的配置在各品牌PLC里都有专门页面,EASY系列一般是在通信参数或者以太网配置里设置,操作很简单,填IP、子网掩码、默认网关就行。
IP规划上有一条很重要的经验:PLC和控制它的上位机最好都在同一个网段,比如PLC设192.168.1.10/24,上位机网卡设192.168.1.20/24。如果跨网段访问,必须保证路由可达,现场很多人卡在这一步,两边IP在不同网段,数据包根本送不进来,还以为是程序有问题。
端口选择上千万别随便用。Modbus TCP默认占用502端口,使用socket通信时尽量避开一些知名端口,选择一个高位端口,比如60000、65000这类。我习惯用60000+工位号,这样多台设备连到同一台上位机时,端口不容易冲突,排查起来也直观。
如果你在PLC程序里同时启用了Modbus TCP服务和socket TCP服务,要注意它们不要绑同一个端口,否则软件层面就会撞车,轻则服务起不来,重则整个以太网通信异常。记得提前把端口规划写进项目文档里。
3.2 Socket服务端功能块调用过程
接下来是核心部分:在PLC里搭建socket服务端。
整体流程可以抽象成这么几个厚实的状态:初始化服务端 → 开始监听端口 → 等待客户端连接 → 连接建立后循环处理收发 → 客户端断开后回到等待连接状态。我建议把这个状态机写在ST程序里,结构清晰,后期改起来也方便。
初始化阶段主要做两件事:绑定端口、启动监听。不同软件版本里,功能块名字可能略有不同,但参数基本逃不出本地端口、连接超时、最大连接数这几项。拿EASY系列举例,假设我开放端口60000,就让服务端在该端口上做好监听准备。
之后程序进入等待客户端连接阶段。有客户端请求接入时,功能块会返回成功信号,同时返回一个连接标识符。这个标识符在后续收发数据时必须带着,因为通信库要用它来区分是哪条连接在收发数据。如果你支持多个客户端同时接入,每个连接标识符都对应一个独立的通信通道。
连接建立后,收数据和发数据是两个并行处理的任务。收数据时,PLC要做好接收缓冲区的管理,及时把收到的帧读走并清空缓冲区,不然下一帧数据来了没地方放,就会丢帧。发送数据时,要把待发送的帧内容放到发送缓冲区,调用发送功能块一次性发出去。
有点要注意的是,小型PLC里的socket收发数据都是基于循环扫描的方式。数据不会像PC那样有独立线程去等待,每条指令执行完就会继续跑后面的逻辑,所以我们必须用“数据到达标志位”配合轮询来处理。收到一帧完整数据后,程序的通信处理区会解析帧内容,执行指令,组装响应帧,再发送出去。
// 伪代码示意:socket服务端主流程(非实际厂商指令,仅表达逻辑) bListenDone := FALSE; bClientConnected := FALSE; WHILE bServerRunning DO IF NOT bListenDone THEN bListenDone := SocketListen(Port := 60000); END_IF; IF bListenDone AND NOT bClientConnected THEN bClientConnected := SocketAccept(ClientHandle => hClient); END_IF; IF bClientConnected THEN IF SocketRecv(hClient, RxBuffer, RxLen) THEN IF RxLen > 0 THEN ProcessFrame(RxBuffer, RxLen); SocketSend(hClient, TxBuffer, TxLen); END_IF; END_IF; END_IF; END_WHILE;这段伪代码不是某个具体型号的真实指令,但它体现的状态机结构是通用的。实际工程中有的EASY型号软件可能把上述接收、处理、回发逻辑拆得更细,但主脉络一定是这个流程。
3.3 自定义协议与数据解析建议
既然走了socket通信,协议就得自己定。我见过不少项目,没有正式定义好协议就上手写代码,结果联调时两边各按各的理解发包,对不上,然后互相推诿,非常浪费时间。我的建议是,在写代码前先花半小时在文档里把帧格式固定下来。
一个成熟的通信帧,至少要包含这几个要素:帧头、命令字、数据长度、数据体、校验码。帧头是固定的20字节或32字节,用来给接收方识别帧起始位置。命令字告诉对方这条帧是要做什么,比如读取设备状态、写入参数、下发配方。数据长度用来标明数据体有多少字节,方便接收方判断一帧是否完整。数据体放实际交互的数据。校验码用CRC16或CRC32都行,用来防干扰、防错帧。
字节序这个问题必须特别提醒。PLC端和上位机端的CPU平台可能不同,大小端规则并不一定一致。比如西门子和汇川PLC内部,多字节变量的存储方式就可能不一样。如果通信双方约定用UInt16存储温度值,但一边按大端解析一边按小端发送,那读出来的数值永远是错的。办法就是在协议文档里明确写清大小端规则,并在上位机测试阶段专门验证一次。
心跳机制也不能省。上位机每隔1到2秒发一条心跳帧,PLC收到后清一次超时计数。如果超过设定时间没收到任何数据,基本可以断定这条连接已经死了,PLC侧就主动关闭这个连接,释放资源。很多通信问题表面上是“连不上”,底层其实是“死连接占坑”,心跳机制能很好地把这种隐患处理掉。
4. 上位机客户端模拟与联调测试
4.1 用Python快速搭建TCP测试客户端
现场做socket从站联调,最核心的验证工具就是一个能主动连接PLC的TCP客户端。虽然上位机组态软件通常自带通信测试工具,但很多时候不够灵活,没法自定义帧内容。我习惯直接用Python写一个几十行的小脚本当测试客户端,改起来快,还不用安装什么重型软件。
Python的socket库是标准库,不需要额外安装依赖。在Windows或者Linux上都直接用。
下面这段代码是一个最简单的TCP客户端,连接PLC的60000端口,发送一条测试帧,然后接收PLC返回的数据:
import socket import time PLC_IP = "192.168.1.10" PLC_PORT = 60000 def send_frame(sock, data: bytes): sock.sendall(data) print(f"[TX] {data.hex().upper()}") def recv_frame(sock) -> bytes: try: data = sock.recv(1024) print(f"[RX] {data.hex().upper()}") return data except socket.timeout: print("[WARN] recv timeout") return b"" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) sock.connect((PLC_IP, PLC_PORT)) print(f"[INFO] connected to {PLC_IP}:{PLC_PORT}") test_frame = bytes.fromhex("AA 55 01 02 03 04 05 06") send_frame(sock, test_frame) recv_frame(sock) time.sleep(1) sock.close()这段代码的关键点有三处:connect方法建立TCP连接;sendall把完整帧发出去;recv在设定超时内接收PLC返回的数据。真正做测试时,你只需要改test_frame的内容为你协议里的任何一帧即可。
跑这个脚本之前,先把PLC侧程序下载进去并运行起来,确认服务端已经处于监听状态。连不上时,脚本会直接抛ConnectionRefusedError或者超时异常,这些信息对后面排查问题非常有价值。
4.2 抓包验证与常见参数核对
写完测试客户端,别急着当成调试结束,抓包这一步绝对不能省。我用Wireshark抓现场通信包已经成了固定习惯,因为很多问题光靠两端打印日志根本看不出来,比如TCP重传、半开连接、数据帧字节序错乱,这些都得从报文层面才能定位。
抓包时先打开Wireshark选择对应网卡,设置一个过滤条件把范围缩小。比如PLC地址是192.168.1.10,端口是60000,就填:
ip.addr == 192.168.1.10 && tcp.port == 60000这个过滤可以同时看到发给PLC的客户端请求和PLC返回的数据。正常情况下,抓包能看到完整的TCP三次握手,然后是客户端发数据、PLC回数据的交替帧。
抓包过程中有几个关键参数要核对:确认SYN包是否发到PLC,如果SYN包一直在重传但没有SYN-ACK回应,那多半是PLC侧服务没起或者端口没监听;确认数据帧内容里各字节的顺序是否与协议一致,大小端问题在这里一眼就能看穿;确认TCP的ACK、SEQ是否正常,如果出现大量重传或者乱序,说明网络链路质量有问题,查网线、查交换机、查现场干扰。
很多工程师对抓包有畏难情绪,觉得Wireshark太专业。实际上你不需要理解TCP/IP协议的每一个细节,只需要抓住连接建立、数据收发、挥手断开这三段关键流程,能看懂标志位和数据内容,就已经够解决90%的通信问题了。
5. 常见问题与排查技巧实录
5.1 端口占用问题:bind only one usage的根源与应对
做socket通信的人一定见过这么一条报错:bind: only one usage of each socket address。这句话的意思是同一个IP加端口只能被一个socket绑定,如果你试图再次绑定,就会触发这个错误。
PLC侧虽然不直接在屏幕上给你弹这个报错,但套接字资源管理逻辑是完全一样的。PLC重启后如果旧连接没有被及时清理,再重新监听同一个端口时就会失败,具体表现是服务端一直启动不了,或者上位机怎么都连不上。
解决办法有几个层面。第一,PLC重启后本身会清空所有运行时状态,只要程序中正确释放了之前的socket句柄,通常不会有问题。第二,断开连接时一定要显式调用关闭函数,很多通信死锁都是漏了这一步。第三,如果上位机软件是自开发的,调试的时候Ctrl+C中断程序,旧socket可能还处于TIME_WAIT状态,紧接着重新运行就会报同样的错。这时候不是代码有重大bug,而是系统还没释放完端口,等几十秒或者把进程彻底杀干净再启动就行了。
排查这个问题的命令也很简单。Windows下用netstat命令看一下端口占用情况:
netstat -ano | findstr 60000如果看到有进程占用,再配合tasklist命令查是哪个PID占用的,确认是残留进程就直接结束掉。这个问题在开发调试阶段几乎人人都能遇到,心态放平就好,处理手法熟了一次就会。
5.2 客户端连不上PLC:IP、网关、防火墙三板斧
客户端连不上PLC,是最常见的故障,但原因往往并不复杂,我排查时基本就是冲这三板斧去的。
第一板斧查IP连通性。先用ping命令验证上位机和PLC是不是真的能互相访问。能ping通说明链路通,ping不通就依次检查网线是否松动、交换机端口状态、两边的网段是否一致。很多现场“连不上”问题,最终定位就是上位机网卡配了个172段的地址,PLC却是192.168.1.10,两边完全不同网段,数据包根本出不去。
第二板斧查防火墙。Windows系统默认会开启防火墙,如果测试客户端是自开发的,防火墙可能直接把入站连接或者出站连接拦截掉了。之前一个项目,PLC侧服务完全正常,但上位机一查询就是超时,折腾了一圈发现是Windows防火墙默认拦截了60000端口的入站连接,在防火墙“高级设置”里加了一条自定义入站规则允许TCP 60000后才彻底解决。
第三板斧查PLC侧实际监听状态。如果IP通、防火墙没拦,就看PLC侧程序是不是真的跑到了监听状态。判断方法有一个特别直接:在PLC编程软件的在线监视里看监听状态位有没有置位。如果状态位没置位,说明socket初始化还没成功,去查端口是否被其他功能占用、函数块的参数配置是否正确。
这三板斧依次过一遍,基本能把95%的连不上问题解决掉,剩下5%再考虑抓包定位。
5.3 通信中断与重连机制设计
通信跑着跑着突然断了,或者上位机软件重启后重连不上,这类问题也很典型。socket has closed unexpectedly这类的报错,我看到的次数特别多,它表明通信对端已经关闭了连接,但本端还在尝试收发数据。
PLC作为服务端,遇到这类问题的处理思路要非常明确:客户端断开是正常现象,服务端程序必须能自动感知断开,然后回到等待连接状态。实现上一般有两种方式,第一种是当recv返回0或者错误码时,认为连接已断开,然后主动关闭socket并释放连接标识符;第二种是依赖前面提到的心跳超时机制,如果长时间收不到客户端的数据帧,就主动断开这条僵死连接。
上位机侧则必须实现断线重连的逻辑。TCP连接天生不携带永久状态,客户端程序要每隔几秒尝试重新连接一次,不能一失败就退出或者无限卡在等待里。我见过太多自研上位机重连逻辑写得敷衍,断一次就再也连不回来,最后背锅的往往是PLC这边的“通信不稳定”。
这里分享一个我自己的设计习惯:在协议帧里加一个递增的帧序号字段。每发一帧数据,序号就加一。接收方通过检查序号是否连续来判断是否丢过帧,一旦发现序号跳变,就可以触发重连或者请求补发。这个设计成本极低,但能让整个通信链路的可靠性提升一个台阶。
还有一条很关键的实操经验:多客户端连接时,每个客户端都对应一个连接资源,如果客户端频繁断开重连,PLC端的连接资源可能会出现堆积。建议在PLC程序中定期检查当前活动连接数,超过配置上限时,自动断开最先建立的连接或者所有僵死连接,只保留最新的一条。这个“淘汰旧连接”的策略虽然有点暴力,但在现场非常见效,能避免很多说不清道不明的运行时崩溃。
| 常见现象 | 排查方向 | 快速处理 |
|---|---|---|
| 上位机提示连接被拒绝 | PLC服务未启动 / 端口不符 | 确认监听状态位,核对端口号 |
| 连接后立即断开 | 防火墙拦截 / PLC主动断开 | 检查防火墙规则,查看PLC日志 |
| 偶发通信超时 | 网络链路质量 / 缓冲溢出 | 抓包看重传率,优化接收缓冲区 |
| 上位机重启后始终连不上 | 端口残留占用 | netstat查端口状态,等系统释放 |
| 数据帧能收到但解析错误 | 大小端 / 协议格式不一致 | 抓包对比字节序,统一协议 |
6. 项目经验与避坑总结
这个项目做完,我最大的感触是:socket从站本身不复杂,难的是整个过程里的细节管理。协议格式、端口规划、大小端约定、心跳机制、超时处理,哪一项没想清楚,都会在联调阶段以各种方式给你添堵。
我的建议是,做这类项目一定先把“通信协议说明书”写在代码前面。不需要很长,就两页纸,把帧结构、字段定义、示例数据写清楚,然后PLC工程师和上位机工程师拿这同一份文档开发,两边就不会跑偏。很多项目协调不好,说白了就是没有这一份双方都认可的契约。
开发顺序上也有讲究。我通常是先用Python脚本模拟上位机,把PLC侧逻辑调通,再让上位机组的同事按照约定好的协议对接。这样等上位机软件正式出手时,通信链路已经被验证过一遍了,问题面会小很多。
最后再留一个我觉得特别值的小技巧。socket调试时,可以先在两台PC之间做一次回环测试,一台跑Python TCP服务端脚本,一台跑TCP客户端脚本,先验证你写的协议帧和收发逻辑本身没有毛病,然后再把服务端换成PLC。这样可以帮你把“通信框架问题”和“PLC实现问题”彻底隔离,排查起来会极其有效率。这个习惯我一直保留到现在,每次做新的通信协议都先走一遍这个流程,省下来的时间是实打实的。