欧姆龙CP1H以太网通讯实战:FINS/TCP协议上位机开发与调试
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.10255.255.255.0上位机FINS客户端CP1H-1192.168.1.11255.255.255.01号机FINS服务端CP1H-2192.168.1.12255.255.255.02号机CP1H-3192.168.1.13255.255.255.03号机节点号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协议的分层理解FINSFactory 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指令。节点号交换命令的格式客户端到服务端偏移长度内容说明040x46494E53ASCII FINS44长度后续字节数840x00000000命令码0表示节点号交换124错误码客户端填0164客户端节点号客户端自己的FINS节点号服务端回应偏移长度内容说明040x46494E53FINS44长度后续字节数840x00000001命令码1表示节点号交换响应124错误码0表示成功164服务端节点号PLC的FINS节点号节点号交换完成后就可以发FINS指令了。FINS指令帧的FINS/TCP头偏移长度内容说明040x46494E53FINS44长度后续字节数840x00000002命令码2表示FINS指令124错误码客户端填0164长度后续FINS帧长度4.3 FINS指令帧的核心字段FINS指令帧也叫FINS命令帧结构如下字段长度说明ICF1信息控制字段一般填0x80RSV1保留填0GCT1网关计数填2DNA1目标网络号单网段填0DA11目标节点号PLC的节点号DA21目标单元地址CPU单元填0CIF41填1SNA1源网络号填0SA11源节点号上位机的节点号SA21源单元地址填0SID1服务ID随便填响应会原样返回MRC1主命令码SRC1子命令码数据变长命令参数以读DM区为例MRC0x01SRC0x01。数据部分包含起始地址2字节和读取字数2字节。比如读D100开始的10个字起始地址0x0064100读取字数0x000A10响应帧里MRC和SRC会加上0x80比如0x01变成0x81表示响应数据部分就是读到的内容。4.4 常用命令码速查操作MRCSRC说明读DM区0x010x01读数据存储区写DM区0x010x02写数据存储区读CIO区0x010x01读CIO区地址不同写CIO区0x010x02写CIO区读HR区0x010x01读保持继电器区读定时器0x010x01读定时器当前值运行/停止0x040x01/0x02控制PLC运行模式地址区的区分靠“数据区代码”DM区是0x82CIO区是0xB0HR区是0xB2等等。这个代码在FINS指令的数据部分里读DM区时数据部分第一个字节就是0x82。提示FINS指令帧里所有多字节数值都是大端序高位在前跟x86的小端序相反写代码的时候要注意字节序转换。我一开始没注意读出来的数据全是反的查了半天才发现是字节序问题。5. 上位机代码实现从Socket到FINS封装5.1 技术选型与整体架构上位机用C#写.NET Framework 4.7.2Socket用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 连接超时先查物理层再查协议第一次调试上位机连PLCSocket.Connect直接超时。排查顺序先ping PLC的IPping不通查网线发现水晶头压得不好有一根线没压到位重压水晶头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-D32767D100100D200没越界啊。后来查手册发现CP1H的DM区虽然标称D0-D32767但实际可用的连续区域有限某些区域被系统占用。具体哪些区域可用要看型号和系统设置。解决办法是分段读或者换到CIO区。这个坑让我意识到手册上的地址范围是理论值实际可用范围要实测。调试的时候先用小批量读确认地址可用再扩大范围。6.4 数据字节序错误读出来全是反的读D100的值PLC里设的是12340x04D2上位机读出来是0xD20453764。这是典型的字节序问题。FINS协议是大端序C#的BitConverter是小端序转换的时候要手动翻转ushort value (ushort)((data[0] 8) | data[1]);所有多字节数值都要注意字节序包括地址、长度、数据。我后来写了一个SwapBytes函数统一处理避免遗漏。6.5 通讯间歇性中断电磁干扰产线一开变频器通讯就断。用Wireshark抓包发现大量TCP重传。查物理层网线是非屏蔽的而且跟变频器动力线走同一个线槽。换成屏蔽网线单独走线槽屏蔽层单端接地问题解决。工业现场通讯稳定性物理层占七成。协议层再健壮物理层丢包严重也白搭。我的经验是能用屏蔽线就用屏蔽线能分开走线就分开走线接地一定要做好。6.6 常见问题速查表现象可能原因排查方法解决连接超时网线/端口/IPping、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匹配请求和响应避免张冠李戴。这个字段手册上说是“随便填”但用好它能省很多事。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →