简介:这份资源面向在VB.NET环境下开发工业自动化上位机的工程师与学习者,聚焦OMRON CP系列PLC的通讯实现,重点讲解FINES协议在IO、DM、H位和W位等资源读写中的应用。包内共37个文件,以vb源码、exe可执行程序、resx与resources资源文件、dll类库及vbproj工程文件为主,另含sln解决方案、xml配置与pdb调试文件,压缩包约145KB,结构紧凑,便于直接打开工程对照调试。资源中附带的FCS校验计算器与通讯示例代码,可帮助读者理解FINS命令的字节构造、Socket收发流程及响应解析方式,并针对网络中断、超时、数据校验错误等异常提供排错思路。目前已有474人学习下载,适合希望掌握PLC通讯底层实现、提升工业控制系统开发能力的中级开发者参考。
1. 上位机与 CP 系列握手的真实门槛
车间里一台老设备要接入 MES,PLC 是 OMRON CP 系列,上位机用 VB.net 写。很多人第一反应是「串口发几个字节不就完了」,真上手才发现:Host Link 协议有帧头帧尾、FCS 校验、应答超时,CP 系列还分 CP1E/CP1L/CP1H,串口和以太网两种物理层,命令格式还不完全一样。标题里这个组合,本质是让 VB.net 通过 Host Link(串口)或 FINS(以太网)跟 CP 系列交换 DM 区、CIO 区的数据。它解决的是产线数据采集、配方下发、设备状态监控这类刚需,适合做上位机、工控集成、设备联网的从业者。下面按「先跑通串口最小闭环,再上以太网,最后讲坑」的路径拆开讲,代码可直接抄。
2. Host Link 协议拆帧:VB.net 串口通讯的最小闭环
2.1 为什么先啃 Host Link 而不是直接上 FINS
CP 系列里 CP1E 很多型号只有 RS232/RS485 口,没有以太网口,Host Link 是绕不开的。Host Link 是 OMRON 的字符型协议,命令以@开头,后面跟单元号、命令码、数据区、读写长度,最后是 FCS 校验和*+ 回车。它的好处是纯 ASCII,用 VB.net 的SerialPort类就能收发,不需要额外协议栈;坏处是每帧都要算 FCS,算错 PLC 直接不回,调试时容易怀疑人生。
FCS 是帧内@到数据末尾所有字符的异或,取低 8 位转成两位十六进制。这个校验不复杂,但新手常犯的错是把@也算进去却漏了单元号,或者把回车算进去。我一般写一个独立函数,输入是@到数据末尾的字符串,输出两位大写十六进制。
2.2 串口参数与最小读写命令
CP 系列默认 Host Link 串口参数是 9600、7 位数据位、2 位停止位、偶校验(7E2),单元号默认 0。VB.net 里SerialPort要显式设Parity = Parity.Even、DataBits = 7、StopBits = StopBits.Two,少一个都收不到应答。读 DM 区的命令格式是@00RD00000001+ FCS +*+ CR,意思是读 DM0 开始的 1 个字。写 DM 区是@00WD00000001+ 数据 + FCS +*+ CR。
Imports System.IO.Ports Module HostLinkDemo Dim port As New SerialPort("COM3") ' 计算 FCS:对 @ 到数据末尾的字符逐字节异或 Function CalcFCS(cmd As String) As String Dim fcs As Byte = 0 For Each c As Char In cmd fcs = fcs Xor Asc(c) Next Return fcs.ToString("X2") ' 两位大写十六进制 End Function ' 读 DM 区:addr 为起始地址,count 为字数 Function ReadDM(addr As Integer, count As Integer) As String Dim body As String = "@00RD" & addr.ToString("D6") & count.ToString("D4") Dim frame As String = body & CalcFCS(body) & "*" & vbCr port.Write(frame) System.Threading.Thread.Sleep(100) ' 等 PLC 应答 Return port.ReadExisting() End Function Sub Main() port.PortName = "COM3" port.BaudRate = 9600 port.DataBits = 7 port.Parity = Parity.Even port.StopBits = StopBits.Two port.ReadTimeout = 1000 port.Open() Console.WriteLine(ReadDM(0, 1)) ' 期望返回 @00RD00 + 数据 + FCS + * port.Close() End Sub End Module这段代码里CalcFCS是核心,ReadDM拼帧后直接写串口。注意Thread.Sleep(100)是偷懒做法,实际项目应该用DataReceived事件或轮询BytesToRead,否则高速采集时会丢帧。addr.ToString("D6")保证地址是 6 位,count.ToString("D4")保证长度是 4 位,位数错了 PLC 会返回IC格式错误。应答帧里@00RD00后面跟数据,最后两位是 FCS,解析时按固定偏移取就行。
2.3 应答解析与超时重试
PLC 正常应答格式是@00RD00+ 数据 + FCS +*+ CR,异常应答是@00IC+ 错误码 + FCS +*+ CR。错误码里01是命令不支持,02是地址越界,03是数据长度错。VB.net 里不要用ReadLine,因为 Host Link 的结束符是 CR,但中间可能夹杂回显,稳妥做法是读到*+ CR 再截断。
超时重试我一般设 3 次,间隔 200ms。如果 3 次都失败,先查物理层:串口线是不是交叉线、PLC 的 DIP 开关是不是 Host Link 模式、波特率有没有被改过。血泪经验是 CP1E 的 DIP 开关 4 号位控制串口协议,拨错就是死活不通,查半天代码最后发现是硬件开关。
3. 以太网 FINS 通讯:CP1L/CP1H 的 UDP 落地
3.1 FINS 帧结构与 UDP 选型理由
CP1L/CP1H 带以太网口的型号支持 FINS 协议,走 UDP 或 TCP。UDP 无连接、开销小,适合周期采集;TCP 可靠但有连接状态,适合配方下发。FINS 帧分两层:FINS 命令帧(ICF、RSV、GCT、DNA、DA1、DA2、SNA、SA1、SA2、SID + 命令码 + 数据)和 UDP/TCP 头。命令码0101是读 CIO 区,0102是写 CIO 区,0201是读 DM 区,0202是写 DM 区。
FINS 的地址编码跟 Host Link 不同,DM 区地址直接是 6 位十六进制,CIO 区也是。读 DM0 的 FINS 帧里,命令码0201,后面跟起始地址000000和长度0001。UDP 端口默认 9600,目标 IP 是 PLC 的 IP。
3.2 VB.net 用 UdpClient 发 FINS 读 DM
Imports System.Net Imports System.Net.Sockets Module FinsDemo ' 构造 FINS 读 DM 命令帧 Function BuildFinsReadDM(addr As Integer, count As Integer) As Byte() Dim frame As New List(Of Byte) ' FINS 头 frame.AddRange({&H80, &H0, &H2, &H0, &H0, &H0, &H0, &H0, &H0, &H0}) ' ICF~SID frame.AddRange({&H2, &H1}) ' 命令码 0201 读 DM frame.AddRange({0, 0, 0, 0}) ' 起始地址占位 frame.AddRange({0, 0}) ' 长度占位 ' 填地址和长度(大端) Dim a As Integer = addr frame(12) = (a >> 8) And &HFF frame(13) = a And &HFF frame(14) = 0 frame(15) = 0 frame(16) = (count >> 8) And &HFF frame(17) = count And &HFF Return frame.ToArray() End Function Sub Main() Dim plcIP As IPAddress = IPAddress.Parse("192.168.1.10") Dim udp As New UdpClient() udp.Connect(plcIP, 9600) Dim cmd As Byte() = BuildFinsReadDM(0, 1) udp.Send(cmd, cmd.Length) Dim remote As New IPEndPoint(IPAddress.Any, 0) Dim resp As Byte() = udp.Receive(remote) ' 响应前 10 字节是 FINS 头,后面是命令码+数据 Console.WriteLine("响应长度: " & resp.Length) udp.Close() End Sub End Module这里BuildFinsReadDM拼的是最简 FINS 帧,ICF 用0x80表示需要应答,DA1 是目标网络号,DA2 是目标节点号,SA1/SA2 是源节点号。实际项目里节点号要跟 PLC 的 IP 末位对应,比如 IP 是192.168.1.10,节点号就是0x0A。地址填充用大端,frame(12)是高字节,frame(13)是低字节,写反了读出来就是错地址。UDP 接收要设超时,否则Receive会一直阻塞。
3.3 批量读写与字节序处理
批量读 DM 时,长度字段填字数,响应里数据按字为单位、大端排列。比如读 DM0 到 DM1,响应数据是 4 字节,前两字节是 DM0,后两字节是 DM1。VB.net 里用BitConverter要注意大小端,x86 是小端,FINS 是大端,所以BitConverter.ToInt16之前要Array.Reverse。我一般写一个FinsToInt16函数,先反转再转换,避免玄学数值。
写 DM 区命令码0202,数据段直接跟要写的字,同样大端。批量写的时候长度字段是字数,数据段长度是字数乘 2。如果写的数据长度跟长度字段不匹配,PLC 会返回0x0005错误码,意思是数据长度错。
4. 避坑与排查:CP 系列通讯的 5 个高频翻车点
4.1 串口收不到任何应答
现象:ReadExisting返回空字符串,PLC 无反应。原因:DIP 开关没拨到 Host Link 模式,或者串口线是直连线而非交叉线。解决:查 CP1E 手册的 DIP 开关定义,4 号位 ON 是 Host Link;用万用表测 2、3 脚是否交叉。这个坑我踩过两次,代码没问题,硬件开关没拨。
4.2 FCS 校验错误导致 PLC 返回 IC
现象:PLC 返回@00IC+ 错误码。原因:FCS 计算范围错了,把*或 CR 算进去了,或者大小写没统一。解决:FCS 只算@到数据末尾,不含*和 CR;输出统一大写。用串口助手手动发一帧对比,能快速定位。
4.3 以太网 FINS 节点号不对
现象:UDP 发出去没响应,或者响应来自错误节点。原因:FINS 头里 DA1/DA2 填错,DA2 是目标节点号,必须跟 PLC IP 末位一致。解决:IP192.168.1.10对应节点号0x0A,IP192.168.1.20对应0x14。填错时 PLC 会丢弃帧,不返回错误。
4.4 批量读长度超限
现象:读 100 个字时 PLC 返回错误。原因:Host Link 单帧最多读 30 个字,FINS 单帧最多读 990 个字,超了要分帧。解决:Host Link 按 30 字分片,FINS 按 990 字分片,循环拼接。分片时地址要递增,别重复读同一段。
4.5 线程阻塞导致界面卡死
现象:VB.net 窗体点采集按钮后无响应。原因:串口ReadExisting或 UDPReceive在 UI 线程同步调用,阻塞消息循环。解决:把通讯逻辑放Task.Run或BackgroundWorker,UI 线程只更新控件。用Invoke回主线程刷数据,别在子线程直接改控件属性。
5. 进阶:把通讯层封装成可复用的 VB.net 类库
5.1 抽象出 IPlcClient 接口
串口和以太网两套实现,上层业务不该关心物理层。我一般定义一个IPlcClient接口,方法有Connect、Disconnect、ReadDM(addr, count)、WriteDM(addr, values)、ReadCIO(addr, count)。串口实现类内部持有SerialPort,以太网实现类持有UdpClient。业务层只调接口,换 PLC 型号时只换实现类,不动业务代码。
Public Interface IPlcClient Function Connect() As Boolean Sub Disconnect() Function ReadDM(addr As Integer, count As Integer) As Short() Sub WriteDM(addr As Integer, values As Short()) End Interface Public Class HostLinkClient Implements IPlcClient Private port As SerialPort Public Function Connect() As Boolean Implements IPlcClient.Connect ' 打开串口,设 7E2 Return True End Function Public Function ReadDM(addr As Integer, count As Integer) As Short() Implements IPlcClient.ReadDM ' 拼 Host Link 帧,解析应答 Return New Short() {} End Function ' 其余成员省略 End Class接口的好处是单元测试时可以写一个MockPlcClient,不连硬件就能测业务逻辑。ReadDM返回Short()而不是String,把解析逻辑收在实现类里,业务层拿到的是数值。
5.2 用配置文件管理连接参数
连接参数不要硬编码。我一般用App.config或 JSON 存 PLC 类型、IP、端口、串口号、波特率、单元号。启动时读配置,工厂方法根据PlcType创建对应实现类。这样现场调试时改配置文件就行,不用重新编译。
<configuration> <appSettings> <add key="PlcType" value="HostLink"/> <add key="PortName" value="COM3"/> <add key="BaudRate" value="9600"/> <add key="UnitNo" value="0"/> <add key="PlcIP" value="192.168.1.10"/> <add key="PlcNode" value="10"/> </appSettings> </configuration>读配置用ConfigurationManager.AppSettings,工厂方法里Select Case判断PlcType。注意PlcNode是十进制,填 FINS 头时要转十六进制。
5.3 心跳与断线重连
产线环境网络抖动是常态,通讯层要有心跳。我一般每 5 秒读一次 DM 区的固定地址(比如 DM100),读成功说明链路正常,失败计数加一,连续 3 次失败就触发重连。重连时先Disconnect再Connect,串口要Close再Open,UDP 要重新Connect。重连成功后在日志里记一笔,方便排查断线频率。
验证方法很简单:拔掉网线或串口线,看程序是否在 15 秒内触发重连,插回后是否自动恢复。这个测试能暴露大部分超时和重连逻辑的 bug。我习惯在ReadDM里加一个Stopwatch,记录每次通讯耗时,超过 500ms 就在日志里标警告,长期看能发现链路劣化的趋势。
这套东西做完,VB.net 跟 CP 系列的通讯就算落地了。从 Host Link 的 FCS 到 FINS 的节点号,从串口 7E2 到 UDP 大端,每个参数都对应一个具体的坑。希望帮到你。
本文还有配套的精品资源,点击获取