
简介这是PC与基恩士PLC通信的源码包内含C#和VB两套完整工程面向需要实现上位机与基恩士PLC数据读写交互的工控开发人员既适合新手入门也适合有经验的开发者直接参考。资源共98个文件压缩包仅718KB包含C#/VB源文件、可执行程序、通信组件动态库、界面资源、配置文件和PDF版通讯组件说明文档工程目录清晰方便按需查找。目前已有1038人学习适用于通过TCP/IP方式完成PC与PLC之间的数据通讯。借助示例工程可以了解TcpClient客户端封装、PLC数据读写调用流程以及VB与C#两种语言的实现差异配合说明文档和可运行程序能够快速移植到实际工控项目中并在调试中掌握常见问题的排错思路。1. PC与基恩士PLC通信先想清楚协议边界设备能 ping 通上位机却读不到基恩士 PLC 的数据这是 PC 与基恩士 PLC 通信项目里最典型的开局。很多人把 Ethernet 当成一根“通了就行”的线但基恩士 KV 系列走的是私有指令帧不是 Modbus TCP也不是西门子 S7 协议随便用网络调试助手发几个字节不会有任何响应。标题里同时出现 C# 与 VB 源码说明这套方案的实际落地场景是拿到厂家示例、改一改就能跑到自己的产线上。这个话题适合做上位机开发、设备数据采集、MES 对接的工程师也适合被 PLC 厂家的示例代码带偏而反复卡壳的人。本文会把协议选型、Memory Link 帧结构、C# 和 VB 两套源码、以及排错手段一次讲清楚让你从“能连上”推进到“能稳定读数据”。2. 通信方式选型串口、Memory Link 与组件 DLL 怎么选2.1 基恩士 PLC 上位机通信的常见路径基恩士 PLC 型号跨度很大从老款 KV-1000 到现在的 KV-7500、KV-X 系列上位机通信路径不完全一样。我一般会先按物理接口和协议类型分成四种避免一上来就陷进某一份示例源码里。通信路径物理接口适用场景跨语言难度串口 RS-232C / RS-422COM 口老型号、近距离直连、现场调试中需处理断帧和超时Ethernet Memory Link以太网KV-5000/KV-7000/KV-X 等主流型号低TCP 裸协议通信厂家提供的 DLL 组件以太网或 USB需要高价服务、不想自己解析协议高依赖 SDK 和授权KV EtherNet/IP以太网与支持 EtherNet/IP 的上位机或 PLC 联动中需要 EDS 配置文件多数设备数据采集项目走第二种也就是 Ethernet Memory Link。原因很直接上位机只需要一个 TCP Socket 就能收发帧不需要安装专用驱动C# 和 VB 都能实现而厂家 DLL 虽然封装得省事但版本更新慢还要考虑 32 位/64 位进程兼容问题。串口的坑在波特率、停止位和数据长度设错时很隐蔽所以我只在没有以太网接口的老机型上才考虑。2.2 Memory Link 的请求帧与响应帧结构Memory Link 是基恩士 PLC 提供的一种文本指令型通信方式请求和响应都以 ASCII 字符为主。不同系列的手册上指令细节会有差异但整体结构非常稳定。读操作通常由三部分构成指令码 起始地址 元素个数末尾跟回车换行响应则包含状态和实际数据。以读寄存器为例一个典型请求帧可能长这样RD DM0100 0008CRLF其中RD是读寄存器指令DM0100表示从 DM0100 这个数据寄存器地址开始0008表示连续读 8 个元素。响应帧会先回正常/异常状态再跟着数据字段数据字段里每个元素通常用十六进制表达元素之间用空格分隔。协议本身不复杂但最容易出问题的是地址写法同样是 100 号寄存器有的系列写DM0100有的写D100甚至地址位数都可能不同。因此构帧时必须把设备类型、地址编号和补齐位数单独做成可配置项不要直接写死在程序里。2.3 寄存器映射与数据类型对照基恩士 Memory Link 能读的地址分为字和位两类字地址常见的有 DM、LR、CR 等位地址常见的有 MR。上位机需要知道每个 PLC 地址在内存里对应的数据类型否则读到的数字会被解析成完全错误的值。PLC 地址类型常见含义上位机对应数据DM数据寄存器字访问short / ushort / byte[]MR内部继电器位访问bool / BitArrayLR链接继电器字或位short / boolCR控制寄存器short / ushort字型地址默认读取 16 位整数32 位整数和浮点数需要连续读两个字再拼接。拼接顺序取决于 PLC 侧的字节序设置这是后面排错时最容易看漏的地方。位地址如果以字为单位返回则每个 bit 代表一个继电器状态C# 里可以把读取结果转成 BitArray 后按位取。2.4 裸 Socket 选型C# 与 VB 都能跑的通信骨架既然要覆盖 C# 和 VB 两套源码最稳妥的做法是直接写一个基于 TcpClient 的通信类把连接、发送、接收、断开四件事封装好。下面的 C# 代码是一个最小可用骨架也是后面两个章节共同打底的部分。using System; using System.Net.Sockets; using System.Text; public class PclLinkClient { private TcpClient _client; private NetworkStream _stream; public int ConnectTimeout { get; set; } 3000; public int ReceiveTimeout { get; set; } 2000; public bool Connect(string ip, int port) { _client new TcpClient(); IAsyncResult ar _client.BeginConnect(ip, port, null, null); if (!ar.AsyncWaitHandle.WaitOne(ConnectTimeout)) { throw new TimeoutException(连接基恩士PLC超时); } _client.EndConnect(ar); _stream _client.GetStream(); _stream.ReadTimeout ReceiveTimeout; return _client.Connected; } public byte[] WriteAndRead(byte[] frame) { _stream.Write(frame, 0, frame.Length); _stream.Flush(); byte[] buffer new byte[4096]; int len _stream.Read(buffer, 0, buffer.Length); byte[] data new byte[len]; Array.Copy(buffer, data, len); return data; } public void Close() { if (_stream ! null) { _stream.Close(); _stream null; } if (_client ! null) { _client.Close(); _client null; } } }这段代码有两个关键点。连接时用 BeginConnect 配合 WaitOne是为了避免上位机界面在 PLC 掉线时卡死几十秒ReceiveTimeout 控制的是读取响应的最长等待时间。注意 WriteAndRead 里只用了一次 Read流式 Socket 在响应较长时可能一次读不完真正用于生产环境时必须按结束符累积数据这一点会在后面的稳定运行章节单独处理。这个类不包含任何具体的 Memory Link 指令所以 C# 和 VB 项目都能复用同一套设计思想。3. C# 源码实现一个可运行的 PC 与基恩士 PLC 通信类3.1 用 C# 构造 Memory Link 请求帧并解析响应有了 TcpClient 骨架下一步就是构帧和解析。这里把读请求单独抽成一个方法地址和数量用参数传入便于以后对不同机型的地址格式做适配。public static byte[] BuildReadFrame(string device, int startAddress, int count) { string deviceAddress ${device}{startAddress:D4}; string frame $RD {deviceAddress} {count:D4}\r\n; return Encoding.ASCII.GetBytes(frame); }startAddress:D4的作用是把十进制地址补成至少 4 位数字例如 100 变成 0100。count:D4同样把读取个数补成 4 位。如果你的 PLC 手册要求 6 位地址就把格式改成D6这也是把协议做成可配置项的原因。响应解析则是把返回的 ASCII 文本按空格拆开再按十六进制转成短整型数组。public static short[] ParseWords(byte[] response) { string text Encoding.ASCII.GetString(response).Trim(); string[] parts text.Split(new[] { \r, \n, }, StringSplitOptions.RemoveEmptyEntries); // 去掉开头的状态字段 if (parts.Length 0) return Array.Emptyshort(); Listshort values new Listshort(); for (int i 1; i parts.Length; i) { values.Add(Convert.ToInt16(parts[i], 16)); } return values.ToArray(); }这里假设响应帧里的第一个字段是状态码后面的字段都是数据。如果异常响应和正常响应格式不同那么parts[0]就会暴露问题读出来不是数字时应该直接抛出异常而不是继续解析。很多“数据不对”的问题其实发生在这一层把十六进制字符串当十进制转或者把字符串当作原始字节处理。3.2 用 C# 连接基恩士 PLC 并读取一个数据区域把上面的方法拼在一起一个最小可用的 C# 上位机读取流程就可以运行了。以下代码从 DM0100 地址开始读 8 个数。static void Main(string[] args) { PclLinkClient pcl new PclLinkClient(); try { pcl.Connect(192.168.0.20, 8501); byte[] frame BuildReadFrame(DM, 100, 8); byte[] response pcl.WriteAndRead(frame); short[] data ParseWords(response); for (int i 0; i data.Length; i) { Console.WriteLine($DM{100 i}: {data[i]}); } } finally { pcl.Close(); } }端口号我在这里用了 8501这是基恩士以太网模块常见默认端口但不同系列可能不同。上线前一定要在 KV STUDIO 或 PLC 网络配置页面确认实际端口不要想当然。IP 地址192.168.0.20也要和 PLC 以太网单元配置成同一网段。程序如果抛超时异常先检查网线和防火墙再检查 PLC 是否运行在允许远程访问的模式。3.3 循环数据采集与 UI 刷新卡顿的解法上位机只读一次数据没有实际意义生产环境通常要循环采集比如每 100 毫秒读一次。直接在 UI 线程里调用WriteAndRead是性能灾难因为Read会阻塞当前线程一旦 PLC 响应慢界面立刻卡死。热搜里经常出现的“C# 循环数据采集和 UI 刷新卡顿”就是这个问题。更合理的做法是用后台循环采集再用进度回传机制刷新界面。private async Task PollLoop(PclLinkClient pcl, IProgressshort[] progress, CancellationToken ct, int intervalMs 100) { while (!ct.IsCancellationRequested) { try { byte[] frame BuildReadFrame(DM, 0, 32); byte[] response await Task.Run(() pcl.WriteAndRead(frame)); short[] data ParseWords(response); progress?.Report(data); } catch (Exception ex) { Console.WriteLine($采集异常: {ex.Message}); } try { await Task.Delay(intervalMs, ct); } catch (TaskCanceledException) { break; } } }IProgressT是 C# 里比较推荐的 UI 刷新方式它会在创建定时器的线程上下文中执行回调不需要手动 Invoke。Task.Run把阻塞式 Socket 读放到线程池让循环不会卡住界面。间隔intervalMs不宜小于 50基恩士 PLC 的以太网处理优先级不一定高太频繁的读请求会拖慢 PLC 的程序扫描周期。3.4 C# 通信参数与异常处理对照表参数推荐值说明ConnectTimeout3000 ms防止 PLC 掉线时界面长时间无响应ReceiveTimeout2000 ms根据 PLC 扫描周期调整扫描慢要放大重试次数3 次网络抖动导致读取失败时自动重试采集间隔100 ms常规设备监控足够过高会消耗 PLC 通信资源异常处理分三层连接阶段捕获SocketException和TimeoutException发送阶段捕获IOException解析阶段捕获FormatException和ArgumentException。每一层异常都建议记录当时的 IP、端口、请求帧内容而不是只记录ex.Message。这能省掉后面大量抓包时间。4. VB 源码实现同样的通信逻辑如何写出 VB 版4.1 VB.NET 版本的通信类骨架VB 和 C# 编译到同一个 .NET 运行时所以 Socket 通信的逻辑可以几乎逐行对应。VB 的完整类代码如下。Imports System.Net.Sockets Imports System.Text Public Class VbPclLinkClient Private _client As TcpClient Private _stream As NetworkStream Public Property ConnectTimeout As Integer 3000 Public Property ReceiveTimeout As Integer 2000 Public Function Connect(ip As String, port As Integer) As Boolean _client New TcpClient() Dim ar As IAsyncResult _client.BeginConnect(ip, port, Nothing, Nothing) If Not ar.AsyncWaitHandle.WaitOne(ConnectTimeout) Then Throw New TimeoutException(连接基恩士PLC超时) End If _client.EndConnect(ar) _stream _client.GetStream() _stream.ReadTimeout ReceiveTimeout Return _client.Connected End Function Public Function WriteAndRead(frame As Byte()) As Byte() _stream.Write(frame, 0, frame.Length) _stream.Flush() Dim buffer(4095) As Byte Dim len As Integer _stream.Read(buffer, 0, buffer.Length) Dim data(len - 1) As Byte Array.Copy(buffer, data, len) Return data End Function Public Sub CloseConnection() If _stream IsNot Nothing Then _stream.Close() _stream Nothing End If If _client IsNot Nothing Then _client.Close() _client Nothing End If End Sub End ClassVB 的中等复杂度项目里最容易踩的坑不是协议而是数组长度。C# 的new byte[4096]声明了 4096 个元素VB 的Dim buffer(4095) As Byte也是 4096 个元素底标是 0上限是 4095。如果照搬 C# 的写法写成Dim buffer(4096)就会在读取时多一个字节的容量虽然不致命但会让后续解析下标错位。调用端的 VB 写法如下。这里用Console.WriteLine直接打印响应的 ASCII 文本方便先确认协议通不通。Module Program Sub Main() Dim pcl As New VbPclLinkClient() Try pcl.Connect(192.168.0.20, 8501) Dim frame As Byte() Encoding.ASCII.GetBytes(RD DM0100 0008 vbCrLf) Dim response As Byte() pcl.WriteAndRead(frame) Console.WriteLine(Encoding.ASCII.GetString(response)) Finally pcl.CloseConnection() End Try End Sub End Module4.2 C# 与 VB 源码迁移时的关键字对照表在两套源码之间迁移时语言差异比协议差异更容易消耗时间。我总结了一张高频对照表适合现场改代码时快速定位。行为C#VB分组名ConsoleMy.Computer.Console 或 Console字符串格式化$RD {addr}RD addr字符串拼接string.JoinString.JoinFor 循环for (int i 0; i n; i)For i As Integer 0 To n - 1捕获异常catch (Exception ex)Catch ex As Exception异步方法async TaskAsync Function空判断item nullitem Is Nothing取类型typeof(string)GetType(String)数组长度arr.Lengtharr.Length十六进制转整数Convert.ToInt16(s, 16)Convert.ToInt16(s, 16)VB 的And和Or在布尔运算里会短路在位运算中要用AndAlso和OrElse否则迁移位运算逻辑会得到错误结果。处理 PLC 返回状态时如果涉及掩码判断优先把值转成Integer再运算避免Short在 VB 中溢出的处理差异。4.3 扫码枪触发事件与 PLC 联动事件驱动做法设备采集场景里PC 经常要同时处理扫码枪和 PLC 写入。C# 和 VB 的处理方式一致都是把扫码枪的数据到达做成事件在事件回调里组织写入帧。下面用 VB 演示一个事件处理函数。Private Sub OnBarcodeReceived(code As String) If String.IsNullOrWhiteSpace(code) Then Return Dim data As String code.PadRight(8).Substring(0, 8) Dim frame As String WD DM0200 0001 data vbCrLf Try pcl.WriteAndRead(Encoding.ASCII.GetBytes(frame)) Catch ex As Exception LogError(扫码写入失败: ex.Message) End Try End Sub事件回调里直接做 Socket 读写只适合低频场景。若产线节拍高扫码枪几十毫秒就能出一次结果而 PLC 写入指令可能还在等待响应并发调用WriteAndRead会把请求帧和响应帧混在一起。我一般会把扫码结果先放进ConcurrentQueue(Of String)再由独立采集线程消费这个队列。这样既能保证条码不丢也避免多个线程同时操作同一个 NetworkStream。4.4 没有 PLC 时用模拟器跑通通信代码没有实机调试条件时可以写一个极简的 TCP 模拟器监听 8501 端口收到RD开头的请求后回复一段固定数据。这样能在写界面和测试逻辑时不依赖 PLC。C# 模拟器代码如下。TcpListener listener new TcpListener(System.Net.IPAddress.Any, 8501); listener.Start(); TcpClient client await listener.AcceptTcpClientAsync(); byte[] buffer new byte[1024]; int n await client.GetStream().ReadAsync(buffer, 0, buffer.Length); string request Encoding.ASCII.GetString(buffer, 0, n).Trim(); if (request.StartsWith(RD)) { byte[] response Encoding.ASCII.GetBytes(OK 0001 0002 0003 0004\r\n); await client.GetStream().WriteAsync(response, 0, response.Length); }注意这个模拟器一次只能处理一个客户端收到一条请求就退出只用于验证上位机的连接和解析。真实调试时可以用网络调试助手或者写一个循环 Accept 的版本但制造响应时要保留回车换行否则上位机会一直等不到完整帧。5. 稳定运行技巧日志、超时与字节序的实际处理5.1 日志里必须有请求帧和响应帧通信调试的第一个动作是把上下行报文完整记录下来。不要只记“读取失败”要记发出去了什么、收到什么。下面这行 C# 日志效果就很直接。Console.WriteLine($[{DateTime.Now:HH:mm:ss.fff}] TX: {BitConverter.ToString(frame)} | {Encoding.ASCII.GetString(frame)});日志里同时保留 HEX 和 ASCII 两种形式能快速区分“不可见字符被过滤”和“数据本身错误”。排查顺序永远是请求帧对不对响应帧有没有解析代码有没有错。能在日志里直接看到RD DM0100 0008变成RD LM0100 0008的情况比看十遍代码更有效。5.2 超时与粘包的处理细节Memory Link 的响应以回车换行结束所以收到\n就可以认为一帧结束。TCP 流里的粘包问题表现为一次 Read 同时读到了两条响应或者一条响应被分到两次 Read。处理办法是维护一个接收缓冲区每次 Read 后按\n切分完整帧。生产环境里不要依赖一次Read拿全数据这一点是最容易在测试时被忽视的。5.3 字节序和位运算在通信处理中的技巧读取 32 位整数或浮点数时要把连续两个 16 位字拼起来。下面是以小端序拼接的示例。int raw (data[0] 0xFFFF) | (data[1] 16); float value BitConverter.ToSingle(BitConverter.GetBytes(raw), 0);如果高位在前就把data[1]放在低 16 位。判断依据是 PLC 侧的数据格式设置不统一时宁可先读一个已知变量验证。位地址的读取则用掩码操作例如(words[0] 0x0001) ! 0表示第 0 位为真。5.4 上线前的自检清单连接超时、响应超时、请求帧地址、端口号、字节序、PLC 侧通信使能这六项每个都要有验证记录。具体做法是把每个 DM 地址的读取结果和触摸屏实际显示值对比确认最大值、负数和浮点数解析正确。最后在停机维护窗口把自检清单完整跑一遍把同步采集日志和每条指令的截图存档作为后续排错的基线。本文还有配套的精品资源点击获取