1. 项目缘起与整体设计思路
车间里那台欧姆龙CP1H已经跑了快六年,一直靠RS-232串口跟上位机通讯,采集数据、下发配方。串口这东西,短距离、低速率、点对点,平时凑合能用,可一旦产线要接入MES、要做集中监控,问题就全冒出来了:布线麻烦、速率上不去、多台上位机抢一个口、稍微远一点就丢包。后来产线改造,要求把三台CP1H的数据统一汇总到一台工控机上,还要能远程读写DM区做配方切换,串口方案彻底扛不住了。
摆在面前的路其实就两条:一是加串口服务器做透传,二是直接上以太网,用CP1H自带的以太网口走FINS/TCP。串口服务器便宜、改动小,但本质还是串口,速率和并发是硬伤,而且多了一层透传,调试的时候故障点变多,出了问题不好定位。CP1H本身带一个以太网口(CP1H-XA/CP1L部分型号需要另配CP1W-CIF41模块),原生支持FINS/TCP和FINS/UDP,走这条路是“正规军”打法,速率、并发、稳定性都上了一个台阶。
最终方案定下来:CP1H通过内置以太网口,采用FINS/TCP协议与上位机通讯。上位机用C#写一个通讯服务,负责轮询采集和指令下发。选FINS/TCP而不是FINS/UDP,理由很直接——TCP有连接状态、有重传、有确认,工业现场电磁环境复杂,UDP丢包了你都不知道,TCP虽然开销大一点,但胜在可靠,出了问题能通过连接状态判断。FINS/UDP适合对实时性要求极高、能容忍少量丢包的场景,我们这种数据采集和配方下发,可靠性优先。
这套方案的核心价值在于:用PLC自带的以太网能力,把原本分散的串口通讯统一到标准以太网上,布线简单(一根网线进交换机)、速率高(100Mbps)、支持多连接(CP1H的FINS/TCP最多可同时支持多个Socket连接,具体数量取决于型号和固件版本,常见为2-4个主动连接加若干被动连接)。适合谁参考?产线设备工程师、上位机开发、自动化集成商,尤其是手里有一堆欧姆龙PLC、正被串口通讯折磨的同行。
下面我把从硬件接线、PLC参数配置、FINS帧结构、上位机代码实现到现场调试踩坑的完整过程拆开讲,尽量把每个“为什么”说清楚,让你看完能直接抄作业。
2. 硬件准备与网络规划:别让物理层拖后腿
2.1 CP1H以太网口的硬件确认
先泼一盆冷水:不是所有CP1H都带以太网口。CP1H-XA和CP1H-X系列本体是不带以太网的,只有CP1H-XA40DR-A这类特定型号,或者通过选装CP1W-CIF41以太网选件板才能上网。我手上这台是CP1H-XA40DR-A,本体自带一个RJ45口,省了选件板的钱。如果你手里是CP1L或者老款CP1H,先翻一下手册确认有没有以太网口,没有的话要么加CIF41,要么换CPU。
CIF41这个选件板插在CP1H的选件槽里,装好之后需要在CX-Programmer里做IO表登记,否则PLC不认。我见过有人插上板子就直接配IP,结果死活连不上,折腾半天才发现IO表没更新。这个坑后面会细说。
2.2 网络拓扑与IP规划
现场网络很简单:一台工控机(上位机)、三台CP1H、一台交换机。交换机用普通的工业级非网管交换机就行,FINS/TCP对交换机没有特殊要求,不需要IGMP Snooping那些花哨功能。但有一点要注意:别把PLC和办公网混在一起,办公网的广播风暴、大流量下载会干扰PLC通讯,最好用独立网段或者VLAN隔开。
IP规划我习惯这样分:
| 设备 | IP地址 | 子网掩码 | 说明 |
|---|---|---|---|
| 工控机 | 192.168.1.10 | 255.255.255.0 | 上位机,FINS客户端 |
| CP1H-1 | 192.168.1.11 | 255.255.255.0 | 1号机,FINS服务端 |
| CP1H-2 | 192.168.1.12 | 255.255.255.0 | 2号机 |
| CP1H-3 | 192.168.1.13 | 255.255.255.0 | 3号机 |
节点号(Node Number)的规划同样重要。FINS协议里每个设备都有一个节点号,范围1-254,用来标识网络中的设备。我一般让节点号和IP最后一段保持一致,比如192.168.1.11的节点号就是11,这样记起来方便,排查的时候一眼就能对上。节点号在PLC的以太网设置里配,上位机这边在FINS帧里也要填目标节点号,两边必须一致,否则PLC直接不理你。
注意:节点号不能重复,同一网络里如果有两台设备节点号一样,通讯会间歇性失败,而且这种故障很难查,因为不是完全不通,是时通时断。
2.3 线缆与接地
网线用超五类屏蔽线,水晶头按T568B标准压。工业现场我强烈建议用屏蔽网线,而且屏蔽层要单端接地(接在交换机或工控机侧),PLC侧不接,避免地环路。别小看这个,车间里变频器一开,非屏蔽线丢包率能到百分之几,屏蔽线加正确接地之后基本为零。
接地方面,PLC的FG端子要接大地,工控机也要接同一大地,保证等电位。我遇到过PLC和工控机分别接不同地,结果通讯时好时坏,用示波器看网线差分信号上叠加了共模干扰,后来统一接地就稳了。
3. PLC侧配置:CX-Programmer里的关键参数
3.1 以太网参数设置入口
打开CX-Programmer,连上PLC(串口或USB),在工程树里找到“设置”-“内置以太网端口”,双击进入设置界面。这里有几个标签页:FINS/TCP、FINS/UDP、FTP、邮件等。我们只关心FINS/TCP和FINS/UDP。
先配“IP地址”和“子网掩码”,按上面的规划填。然后配“FINS节点号”,填11(对应1号机)。这里有个细节:CP1H的以太网设置里,“FINS节点号”和“IP地址”是分开配的,节点号可以跟IP最后一段不一样,但为了好记,建议保持一致。
3.2 FINS/TCP服务端设置
在FINS/TCP标签页里,需要设置“自动分配FINS节点号”和“TCP端口号”。CP1H作为服务端(被动连接),默认端口是9600。这个端口号是FINS/TCP的标准端口,上位机连接时目标端口填9600。
“自动分配FINS节点号”这个选项,如果勾选,PLC会自动给连接的客户端分配节点号;如果不勾,需要手动指定。我一般不勾选,手动指定,因为自动分配有时候会跟已有节点号冲突,而且手动指定之后,上位机这边的节点号是固定的,排查方便。
CP1H的FINS/TCP支持多个连接,具体数量看型号。CP1H-XA40DR-A内置以太网口支持最多4个FINS/TCP连接(主动+被动合计),如果不够用,可以考虑用FINS/UDP或者换更高端的型号。我们三台PLC,每台一个连接,够用。
3.3 路由表与网络号
如果只是单网段通讯,网络号和节点地址都用默认值就行:网络号0,节点地址就是节点号,单元地址对于CPU单元是0。但如果你有多网段或者通过网关,就需要配路由表。我们现场是单网段,所以路由表不用动。
单元地址(Unit Address)这个概念容易搞混。对于CP1H的CPU单元,单元地址是0;如果是CIF41选件板,单元地址是1(取决于插在哪个槽)。上位机发FINS指令时,目标单元地址要填对,填错了PLC不响应。我一开始用CIF41的时候,单元地址填了0,结果一直超时,后来查手册才知道CIF41的单元地址是1。
3.4 IO表登记(CIF41用户必看)
如果你用的是CIF41选件板,装好之后必须在CX-Programmer的IO表中登记。步骤:在线工作状态下,双击“IO表和单元设置”,找到选件板所在的槽位,把CIF41添加进去。不登记的话,PLC启动时不会初始化以太网口,IP都配不进去。
登记完之后,断电重启PLC,让以太网口初始化。一定要断电重启,软复位有时候不生效。我试过只做软复位,结果IP还是旧的,折腾了半小时才发现要断电。
3.5 配置下载与验证
所有参数配好后,下载到PLC,然后断电重启。验证方法:在工控机上ping PLC的IP,能ping通说明物理层和IP层没问题。ping不通的话,先查网线、交换机、IP是否同网段,再查PLC的以太网口指示灯是否正常(LINK灯亮、ACT灯闪烁)。
ping通之后,可以用欧姆龙官方的FINS通讯测试工具(比如CX-Integrator或者第三方的FINS测试软件)发一条简单的指令,读一下DM区,确认FINS/TCP服务正常。这一步很关键,先把PLC侧确认没问题,再动上位机代码,否则两边一起调,出了问题不知道是哪边的。
4. FINS/TCP协议帧结构深度拆解
4.1 FINS协议的分层理解
FINS(Factory Interface Network Service)是欧姆龙的一套通讯协议,可以跑在多种物理层上:串口、以太网、Controller Link等。跑在以太网上时,有两种封装方式:FINS/TCP和FINS/UDP。FINS/TCP就是在TCP之上再包一层FINS帧头。
理解FINS帧结构,我习惯把它分成三层:TCP层、FINS/TCP头、FINS指令帧。TCP层就是标准的TCP连接,不用管。FINS/TCP头是TCP特有的,用来做连接管理和节点号交换。FINS指令帧是核心,包含命令码、目标地址、数据区地址和数据。
4.2 FINS/TCP头的结构
FINS/TCP头有两种:命令帧头和数据帧头。连接建立后,客户端先发一个“FINS/TCP节点号交换”命令,服务端回应,之后才发真正的FINS指令。
节点号交换命令的格式(客户端到服务端):
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 4 | 0x46494E53 | ASCII "FINS" |
| 4 | 4 | 长度 | 后续字节数 |
| 8 | 4 | 0x00000000 | 命令码,0表示节点号交换 |
| 12 | 4 | 错误码 | 客户端填0 |
| 16 | 4 | 客户端节点号 | 客户端自己的FINS节点号 |
服务端回应:
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 4 | 0x46494E53 | "FINS" |
| 4 | 4 | 长度 | 后续字节数 |
| 8 | 4 | 0x00000001 | 命令码,1表示节点号交换响应 |
| 12 | 4 | 错误码 | 0表示成功 |
| 16 | 4 | 服务端节点号 | PLC的FINS节点号 |
节点号交换完成后,就可以发FINS指令了。FINS指令帧的FINS/TCP头:
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 4 | 0x46494E53 | "FINS" |
| 4 | 4 | 长度 | 后续字节数 |
| 8 | 4 | 0x00000002 | 命令码,2表示FINS指令 |
| 12 | 4 | 错误码 | 客户端填0 |
| 16 | 4 | 长度 | 后续FINS帧长度 |
4.3 FINS指令帧的核心字段
FINS指令帧(也叫FINS命令帧)结构如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| ICF | 1 | 信息控制字段,一般填0x80 |
| RSV | 1 | 保留,填0 |
| GCT | 1 | 网关计数,填2 |
| DNA | 1 | 目标网络号,单网段填0 |
| DA1 | 1 | 目标节点号,PLC的节点号 |
| DA2 | 1 | 目标单元地址,CPU单元填0,CIF41填1 |
| SNA | 1 | 源网络号,填0 |
| SA1 | 1 | 源节点号,上位机的节点号 |
| SA2 | 1 | 源单元地址,填0 |
| SID | 1 | 服务ID,随便填,响应会原样返回 |
| MRC | 1 | 主命令码 |
| SRC | 1 | 子命令码 |
| 数据 | 变长 | 命令参数 |
以读DM区为例,MRC=0x01,SRC=0x01。数据部分包含起始地址(2字节)和读取字数(2字节)。比如读D100开始的10个字:
- 起始地址:0x0064(100)
- 读取字数:0x000A(10)
响应帧里,MRC和SRC会加上0x80(比如0x01变成0x81),表示响应,数据部分就是读到的内容。
4.4 常用命令码速查
| 操作 | MRC | SRC | 说明 |
|---|---|---|---|
| 读DM区 | 0x01 | 0x01 | 读数据存储区 |
| 写DM区 | 0x01 | 0x02 | 写数据存储区 |
| 读CIO区 | 0x01 | 0x01 | 读CIO区(地址不同) |
| 写CIO区 | 0x01 | 0x02 | 写CIO区 |
| 读HR区 | 0x01 | 0x01 | 读保持继电器区 |
| 读定时器 | 0x01 | 0x01 | 读定时器当前值 |
| 运行/停止 | 0x04 | 0x01/0x02 | 控制PLC运行模式 |
地址区的区分靠“数据区代码”:DM区是0x82,CIO区是0xB0,HR区是0xB2,等等。这个代码在FINS指令的数据部分里,读DM区时数据部分第一个字节就是0x82。
提示:FINS指令帧里所有多字节数值都是大端序(高位在前),跟x86的小端序相反,写代码的时候要注意字节序转换。我一开始没注意,读出来的数据全是反的,查了半天才发现是字节序问题。
5. 上位机代码实现:从Socket到FINS封装
5.1 技术选型与整体架构
上位机用C#写,.NET Framework 4.7.2,Socket用System.Net.Sockets.TcpClient。为什么不用现成的FINS库?一是现场环境不方便引入第三方依赖,二是自己封装一遍,出了问题能定位到字节级别,调试的时候心里有底。
整体架构分三层:Socket通讯层负责TCP连接和收发字节流;FINS协议层负责组帧和解析;业务层负责轮询采集和指令下发。三层之间用队列解耦,Socket层收到数据后丢到接收队列,协议层从队列取数据解析,业务层调用协议层接口。
5.2 TCP连接与节点号交换
连接建立很简单:
TcpClient client = new TcpClient(); client.Connect("192.168.1.11", 9600); NetworkStream stream = client.GetStream();连接成功后,先发节点号交换命令。注意,节点号交换是FINS/TCP特有的,FINS/UDP没有这一步。代码如下:
byte[] nodeExchange = new byte[20]; // "FINS" nodeExchange[0] = 0x46; nodeExchange[1] = 0x49; nodeExchange[2] = 0x4E; nodeExchange[3] = 0x53; // 长度 = 12 nodeExchange[4] = 0x00; nodeExchange[5] = 0x00; nodeExchange[6] = 0x00; nodeExchange[7] = 0x0C; // 命令码 = 0 nodeExchange[8] = 0x00; nodeExchange[9] = 0x00; nodeExchange[10] = 0x00; nodeExchange[11] = 0x00; // 错误码 = 0 nodeExchange[12] = 0x00; nodeExchange[13] = 0x00; nodeExchange[14] = 0x00; nodeExchange[15] = 0x00; // 客户端节点号 = 10 nodeExchange[16] = 0x00; nodeExchange[17] = 0x00; nodeExchange[18] = 0x00; nodeExchange[19] = 0x0A; stream.Write(nodeExchange, 0, 20);服务端会回20字节,第16-19字节是PLC的节点号。收到之后,后续FINS指令里的目标节点号就用这个值。
5.3 FINS指令组帧函数
组帧函数接收命令码、数据区代码、起始地址、长度等参数,返回完整的字节数组:
byte[] BuildFinsCommand(byte mrc, byte src, byte[] data) { // FINS/TCP头 16字节 + FINS帧头 10字节 + 数据 int finsFrameLen = 10 + data.Length; byte[] frame = new byte[16 + finsFrameLen]; // FINS/TCP头 frame[0] = 0x46; frame[1] = 0x49; frame[2] = 0x4E; frame[3] = 0x53; // 长度 = 8 + finsFrameLen int tcpLen = 8 + finsFrameLen; frame[4] = (byte)(tcpLen >> 24); frame[5] = (byte)(tcpLen >> 16); frame[6] = (byte)(tcpLen >> 8); frame[7] = (byte)tcpLen; // 命令码 = 2 frame[8] = 0x00; frame[9] = 0x00; frame[10] = 0x00; frame[11] = 0x02; // 错误码 = 0 frame[12] = 0x00; frame[13] = 0x00; frame[14] = 0x00; frame[15] = 0x00; // FINS帧长度 frame[16] = (byte)(finsFrameLen >> 24); frame[17] = (byte)(finsFrameLen >> 16); frame[18] = (byte)(finsFrameLen >> 8); frame[19] = (byte)finsFrameLen; // FINS帧头 frame[20] = 0x80; // ICF frame[21] = 0x00; // RSV frame[22] = 0x02; // GCT frame[23] = 0x00; // DNA frame[24] = 0x0B; // DA1 = 11 frame[25] = 0x00; // DA2 = 0 frame[26] = 0x00; // SNA frame[27] = 0x0A; // SA1 = 10 frame[28] = 0x00; // SA2 frame[29] = 0x00; // SID frame[30] = mrc; frame[31] = src; // 数据 Array.Copy(data, 0, frame, 32, data.Length); return frame; }读DM区的数据部分:
byte[] ReadDMData(ushort startAddr, ushort count) { byte[] data = new byte[4]; data[0] = 0x82; // DM区代码 data[1] = (byte)(startAddr >> 8); data[2] = (byte)startAddr; data[3] = (byte)count; return data; }注意,读DM区的数据部分只有4字节:数据区代码1字节、起始地址2字节、读取字数1字节。读取字数最大是1字节,也就是最多读255个字,超过要分多次读。这个限制我踩过坑,想一次读500个字,结果PLC返回错误码,查手册才知道单次最多255字。
5.4 响应解析与错误处理
响应帧的解析要检查几个地方:FINS/TCP头的错误码、FINS帧的MRC/SRC是否加了0x80、FINS帧末尾的结束码。结束码是2字节,0x0000表示成功,其他值表示各种错误。
bool ParseResponse(byte[] resp, out byte[] data) { data = null; // 检查FINS/TCP错误码 int tcpErr = (resp[12] << 24) | (resp[13] << 16) | (resp[14] << 8) | resp[15]; if (tcpErr != 0) return false; // 检查FINS结束码(在数据末尾2字节) int endCodePos = resp.Length - 2; int endCode = (resp[endCodePos] << 8) | resp[endCodePos + 1]; if (endCode != 0) return false; // 提取数据(去掉FINS帧头10字节和结束码2字节) int dataLen = resp.Length - 16 - 10 - 2; data = new byte[dataLen]; Array.Copy(resp, 16 + 10, data, 0, dataLen); return true; }结束码的常见值:0x0000成功,0x0101命令码不支持,0x0201数据区不存在,0x0301地址越界,0x0401数据长度不对。调试的时候把结束码打出来,对照手册查,比瞎猜快得多。
5.5 轮询采集与并发处理
三台PLC,每台轮询周期200ms,读100个DM字。如果串行轮询,三台加起来600ms,勉强够用。但为了留余量,我用多线程并发,每台PLC一个线程,各自维护自己的TCP连接和轮询循环。
并发要注意线程安全:每台PLC的Socket和缓冲区是独立的,不共享,所以不需要加锁。但写日志、更新UI的时候要跨线程,用Invoke或者ConcurrentQueue。我一开始没注意,多个线程同时写日志文件,结果日志内容错乱,后来改成每个线程写自己的日志文件,或者用锁保护,才正常。
注意:CP1H的FINS/TCP连接数有限,如果上位机频繁断开重连,PLC侧可能来不及释放旧连接,导致新连接被拒绝。解决办法是连接复用,建立连接后保持长连接,不要每次读写都重连。如果必须重连,重连前先发一个断开命令,或者等几秒再重连。
6. 现场调试实录:那些手册上不会写的问题
6.1 连接超时:先查物理层再查协议
第一次调试,上位机连PLC,Socket.Connect直接超时。排查顺序:先ping PLC的IP,ping不通;查网线,发现水晶头压得不好,有一根线没压到位;重压水晶头,ping通了;再连Socket,还是超时;查PLC的FINS/TCP设置,发现端口号配成了9601(默认是9600),改回9600,连上了。
这个案例说明:连接超时不要一上来就怀疑协议,先把物理层和IP层确认清楚。ping通只说明IP层通,不代表TCP端口开着。可以用telnet 192.168.1.11 9600测试端口,如果telnet能连上,说明TCP服务正常,问题在上位机代码;如果telnet连不上,问题在PLC侧。
6.2 节点号交换失败:节点号冲突
有一台PLC,节点号交换命令发过去,PLC回的响应里错误码非0。查了半天,发现这台PLC的节点号跟另一台配重了,都是11。FINS网络里节点号必须唯一,冲突了PLC会拒绝。改成13之后正常。
节点号冲突的隐蔽性在于:不是完全不通,而是时通时断,因为两台PLC可能交替响应。排查方法是把其他PLC先断电,只留一台,看是否正常,然后逐台加回来,定位冲突的那台。
6.3 读数据返回错误码0x0301:地址越界
读D100开始的100个字,返回0x0301(地址越界)。CP1H的DM区范围是D0-D32767,D100+100=D200,没越界啊。后来查手册发现,CP1H的DM区虽然标称D0-D32767,但实际可用的连续区域有限,某些区域被系统占用。具体哪些区域可用,要看型号和系统设置。解决办法是分段读,或者换到CIO区。
这个坑让我意识到:手册上的地址范围是理论值,实际可用范围要实测。调试的时候先用小批量读,确认地址可用,再扩大范围。
6.4 数据字节序错误:读出来全是反的
读D100的值,PLC里设的是1234(0x04D2),上位机读出来是0xD204(53764)。这是典型的字节序问题。FINS协议是大端序,C#的BitConverter是小端序,转换的时候要手动翻转:
ushort value = (ushort)((data[0] << 8) | data[1]);所有多字节数值都要注意字节序,包括地址、长度、数据。我后来写了一个SwapBytes函数,统一处理,避免遗漏。
6.5 通讯间歇性中断:电磁干扰
产线一开变频器,通讯就断。用Wireshark抓包,发现大量TCP重传。查物理层,网线是非屏蔽的,而且跟变频器动力线走同一个线槽。换成屏蔽网线,单独走线槽,屏蔽层单端接地,问题解决。
工业现场通讯稳定性,物理层占七成。协议层再健壮,物理层丢包严重也白搭。我的经验是:能用屏蔽线就用屏蔽线,能分开走线就分开走线,接地一定要做好。
6.6 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 连接超时 | 网线/端口/IP | ping、telnet | 查物理层、端口号 |
| 节点号交换失败 | 节点号冲突 | 逐台断电排查 | 改节点号 |
| 错误码0x0301 | 地址越界 | 查手册、小批量读 | 分段读或换区 |
| 数据反了 | 字节序 | 检查转换代码 | 手动翻转字节 |
| 间歇中断 | 电磁干扰 | Wireshark抓包 | 屏蔽线、分开走线 |
| 连接被拒绝 | 连接数满 | 查PLC连接状态 | 连接复用、减少重连 |
| 响应慢 | 轮询太频繁 | 查轮询周期 | 降低频率、并发 |
7. 性能优化与长期运行稳定性
7.1 轮询周期与数据量平衡
轮询周期不是越短越好。周期太短,PLC响应不过来,反而丢包;周期太长,数据实时性差。我的经验值是:单台PLC,读100个字,周期100-200ms比较合适。如果数据量大,可以分优先级:关键数据高频读,非关键数据低频读。
CP1H的FINS/TCP响应时间,实测单次读写大约5-15ms(取决于数据量和网络状况)。如果周期200ms,理论上可以读十几次,但实际要考虑PLC的CPU负载,别把PLC逼太紧。
7.2 心跳与断线重连
长连接需要心跳维持。我一般每5秒发一条读指令作为心跳,如果连续3次超时,判定连接断开,触发重连。重连要有退避策略:第一次立即重连,失败等1秒,再失败等2秒,最多等30秒,避免频繁重连把PLC连接数占满。
重连的时候,先关闭旧Socket,再建新Socket。旧Socket不关,PLC侧可能还占着连接,新连接建不上。关闭Socket用Shutdown加Close,确保四次挥手完成。
7.3 日志与故障追溯
日志要记全:发送的帧、接收的帧、时间戳、错误码。但别记太细,否则日志文件爆炸。我的做法是:正常通讯只记摘要(时间、PLC、操作、结果),异常通讯记完整帧(十六进制)。日志按天分割,保留30天。
故障追溯的时候,日志是救命稻草。有一次通讯偶发失败,查日志发现每次失败都发生在整点,后来发现是另一个程序整点做数据备份,占满了网络带宽。没有日志,这种问题根本查不出来。
7.4 长期运行的内存与句柄管理
上位机连续跑几个月,最容易出问题的是内存泄漏和Socket句柄泄漏。每次重连都要确保旧Socket被释放,TcpClient实现了IDisposable,用using或者手动Dispose。缓冲区不要频繁new,用固定大小的数组复用。
我见过一个项目,上位机跑一周就卡死,查到最后是每次读数据都new一个byte数组,GC来不及回收,内存涨到几个G。改成复用缓冲区之后,连续跑半年没问题。
8. 写在最后:一些个人体会
这套FINS/TCP方案,从最初调试到稳定运行,前后折腾了大概两周。踩过的坑主要集中在物理层和字节序上,协议本身反而不复杂。我的体会是:工业通讯,七分靠物理,三分靠协议。物理层做好了,协议层按部就班实现,基本不会有大问题。
另外,调试工具要备齐:Wireshark抓包看TCP层,FINS测试工具验证PLC侧,串口调试助手备用(万一以太网彻底不通,还能用串口救急)。工具齐全,排查效率翻倍。
最后分享一个小技巧:FINS指令的SID字段(服务ID),我习惯用递增的序号,每次发指令SID加1,响应里会原样返回。这样在并发或者乱序响应的时候,能通过SID匹配请求和响应,避免张冠李戴。这个字段手册上说是“随便填”,但用好它能省很多事。