一、 HostLink 通讯核心类 (OmronHostLink.cs)
该类封装了 Socket 通讯底层、报文校验码计算以及发送接收的逻辑。
csharp
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace HostLinkDemo
{
public class OmronHostLink : IDisposable
{
private TcpClient _client;
private NetworkStream _stream;
// PLC的IP地址和端口(默认9600)
public string IpAddress { get; set; }
public int Port { get; set; }
// 超时时间(毫秒)
public int Timeout { get; set; } = 3000;
public OmronHostLink(string ip, int port = 9600)
{
IpAddress = ip;
Port = port;
_client = new TcpClient();
}
/// <summary>
/// 连接到PLC
/// </summary>
public async Task<bool> ConnectAsync()
{
try
{
await _client.ConnectAsync(IpAddress, Port);
_stream = _client.GetStream();
Console.WriteLine(" PLC连接成功!");
return true;
}
catch (Exception ex)
{
Console.WriteLine($" PLC连接失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
_stream?.Close();
_client?.Close();
Console.WriteLine("🔌 PLC已断开连接");
}
/// <summary>
/// 发送命令并接收响应
/// </summary>
private async Task<string> SendAndReceiveAsync(string command)
{
// 1. 计算校验码 (FCS)
string fcs = CalculateFcs(command);
// 2. 组装完整报文: @ + 命令 + FCS + *CR (回车符)
string fullCommand = $"@{command}{fcs}\x0D";
byte[] sendBytes = Encoding.ASCII.GetBytes(fullCommand);
// 3. 发送数据
await _stream.WriteAsync(sendBytes, 0, sendBytes.Length);
// 4. 接收数据
byte[] buffer = new byte[1024];
// 根据实际网络情况,可能需要循环读取,这里简化为单次读取
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
string response = Encoding.ASCII.GetString(buffer, 0, bytesRead);
return response;
}
/// <summary>
/// 读取DM区数据 (例如读取 D100 开始的 10 个字)
/// </summary>
/// <param name="startAddress">起始地址 (如 100)</param>
/// <param name="readCount">读取数量 (字数)</param>
public async Task<short[]> ReadDmAreaAsync(int startAddress, int readCount)
{
/*
* HostLink 读内存命令格式示例 (读取 D100 - D109):
* 单元号 命令 存储区代码 起始地址 数量
* 00 RD 82 000100 0010
*/
string command = $"00RD82{startAddress:D6}{readCount:D4}";
string response = await SendAndReceiveAsync(command);
// 解析响应报文
if (response.Contains("OK"))
{
// 提取数据部分 (去除头部 @00RD82 和尾部 FCS/*CR)
// 实际解析需根据具体的响应格式微调截取位置
string dataStr = response.Substring(8, readCount * 4);
short[] values = new short[readCount];
for (int i = 0; i < readCount; i++)
{
string hexValue = dataStr.Substring(i * 4, 4);
values[i] = Convert.ToInt16(hexValue, 16); // Hex转Short
}
return values;
}
else
{
throw new Exception($"读取失败,PLC返回: {response}");
}
}
/// <summary>
/// 写入DM区数据
/// </summary>
public async Task<bool> WriteDmAreaAsync(int startAddress, short[] values)
{
/*
* HostLink 写内存命令格式示例 (写入 D100=1234, D101=5678):
* 00 WD 82 000100 0002 1234 5678
*/
StringBuilder sb = new StringBuilder();
sb.Append($"00WD82{startAddress:D6}{values.Length:D4}");
foreach (var val in values)
{
sb.Append($"{val:X4}"); // 转成4位16进制大写字符串
}
string response = await SendAndReceiveAsync(sb.ToString());
// 检查响应是否包含 "OK"
return response.Contains("OK");
}
/// <summary>
/// 计算校验码 (FCS: Frame Check Sequence)
/// HostLink协议规定对 '@' 和 'CR' 之间的字符进行异或运算
/// </summary>
private string CalculateFcs(string data)
{
byte fcs = 0;
foreach (char c in data)
{
fcs ^= (byte)c;
}
return fcs.ToString("X2"); // 转为两位十六进制大写字符串
}
public void Dispose()
{
Disconnect();
_client?.Dispose();
}
}
}
二、 控制台测试程序 (Program.cs)
展示如何在主程序中调用上述类进行实际的 PLC 数据读写。
csharp
using System;
using System.Threading.Tasks;
namespace HostLinkDemo
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("====== Omron HostLink (FINS/TCP) 通讯测试 ======");
// 1. 创建HostLink实例 (填入PLC的实际IP和端口)
// 注意:HostLink over TCP 的默认端口通常是 9600 或 8501,具体看PLC网络配置
using var plc = new OmronHostLink("192.168.1.100", 9600);
// 2. 建立连接
bool isConnected = await plc.ConnectAsync();
if (!isConnected) return;
try
{
// 3. 写入数据测试 (向 D100, D101 写入数据)
Console.WriteLine("\n[测试] 向 D100-D101 写入数据...");
short[] writeData = { 1234, 5678 };
bool writeResult = await plc.WriteDmAreaAsync(100, writeData);
Console.WriteLine(writeResult ? " 写入成功!" : " 写入失败!");
// 4. 读取数据测试 (从 D100 开始读取 5 个字)
Console.WriteLine("\n[测试] 从 D100 开始读取 5 个字...");
short[] readData = await plc.ReadDmAreaAsync(100, 5);
Console.WriteLine(" 读取成功! 数据如下:");
for (int i = 0; i < readData.Length; i++)
{
Console.WriteLine($"D{100 + i}: {readData[i]} (0x{readData[i]:X4})");
}
}
catch (Exception ex)
{
Console.WriteLine($"\n 发生异常: {ex.Message}");
}
finally
{
// 5. 断开连接
plc.Disconnect();
Console.WriteLine("\n测试完毕,按任意键退出.");
Console.ReadKey();
}
}
}
}
参考代码 C# PLC通讯示例源码(hostlink) www.youwenfan.com/contentcsu/62391.html
三、 知识点解析
- 报文格式差异 :
- 上述代码采用的是 Omron HostLink (ASCII) 模式。
- 如果是其他品牌的 PLC(如三菱、西门子)或者 Omron 的二进制模式,报文的帧头和帧尾会完全不同。如果是二进制模式,发送和接收都需要按
byte[]处理,而不是Encoding.ASCII。
- 网络端口 (Port) :
- 传统的 HostLink 是基于 RS-232/485 串口转 TCP 的,端口常为
9600。 - 如果 PLC 配置的是 FINS/TCP 原生协议,默认端口通常是
9600(部分老型号是8501)。请在 PLC 的网络设置中确认端口号。
- 传统的 HostLink 是基于 RS-232/485 串口转 TCP 的,端口常为
- 存储区代码 :
- 代码中读写的
DM区对应的区域代码是82(十六进制)。 - 如果您需要读写 CIO、WR、HR 等其他区域,需要替换命令中的区域代码(例如 CIO 是
30)。
- 代码中读写的
- 异步与多线程 :
- 在实际的窗体应用(WinForms/WPF)或 Web API 中,绝对不要 使用阻塞式的
.Result或.Wait()调用异步方法,否则极易导致 UI 卡死或线程死锁。请全程保持async/await的调用链。
- 在实际的窗体应用(WinForms/WPF)或 Web API 中,绝对不要 使用阻塞式的