简介:这是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-422 | COM 口 | 老型号、近距离直连、现场调试 | 中,需处理断帧和超时 |
| 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 0008<CR><LF>其中RD是读寄存器指令,DM0100表示从 DM0100 这个数据寄存器地址开始,0008表示连续读 8 个元素。响应帧会先回正常/异常状态,再跟着数据字段,数据字段里每个元素通常用十六进制表达,元素之间用空格分隔。协议本身不复杂,但最容易出问题的是地址写法:同样是 100 号寄存器,有的系列写DM0100,有的写D100,甚至地址位数都可能不同。因此构帧时必须把设备类型、地址编号和补齐位数单独做成可配置项,不要直接写死在程序里。
2.3 寄存器映射与数据类型对照
基恩士 Memory Link 能读的地址分为字和位两类,字地址常见的有 DM、LR、CR 等,位地址常见的有 MR。上位机需要知道每个 PLC 地址在内存里对应的数据类型,否则读到的数字会被解析成完全错误的值。
| PLC 地址类型 | 常见含义 | 上位机对应数据 |
|---|---|---|
| DM | 数据寄存器,字访问 | short / ushort / byte[] |
| MR | 内部继电器,位访问 | bool / BitArray |
| LR | 链接继电器,字或位 | short / bool |
| CR | 控制寄存器 | 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.Empty<short>(); List<short> values = new List<short>(); 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, IProgress<short[]> 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; } } }IProgress<T>是 C# 里比较推荐的 UI 刷新方式,它会在创建定时器的线程上下文中执行回调,不需要手动 Invoke。Task.Run把阻塞式 Socket 读放到线程池,让循环不会卡住界面。间隔intervalMs不宜小于 50,基恩士 PLC 的以太网处理优先级不一定高,太频繁的读请求会拖慢 PLC 的程序扫描周期。
3.4 C# 通信参数与异常处理对照表
| 参数 | 推荐值 | 说明 |
|---|---|---|
| ConnectTimeout | 3000 ms | 防止 PLC 掉线时界面长时间无响应 |
| ReceiveTimeout | 2000 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 |
|---|---|---|
| 分组名 | Console | My.Computer.Console 或 Console |
| 字符串格式化 | $"RD {addr}" | "RD " & addr |
| 字符串拼接 | string.Join | String.Join |
| For 循环 | for (int i = 0; i < n; i++) | For i As Integer = 0 To n - 1 |
| 捕获异常 | catch (Exception ex) | Catch ex As Exception |
| 异步方法 | async Task | Async Function |
| 空判断 | item == null | item Is Nothing |
| 取类型 | typeof(string) | GetType(String) |
| 数组长度 | arr.Length | arr.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 地址的读取结果和触摸屏实际显示值对比,确认最大值、负数和浮点数解析正确。最后在停机维护窗口把自检清单完整跑一遍,把同步采集日志和每条指令的截图存档,作为后续排错的基线。
本文还有配套的精品资源,点击获取