基于 C# 实现的 Omron HostLink (FINS) 协议 PLC 通讯

该类封装了 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

三、 知识点解析

  1. 报文格式差异
    • 上述代码采用的是 Omron HostLink (ASCII) 模式
    • 如果是其他品牌的 PLC(如三菱、西门子)或者 Omron 的二进制模式,报文的帧头和帧尾会完全不同。如果是二进制模式,发送和接收都需要按 byte[] 处理,而不是 Encoding.ASCII
  2. 网络端口 (Port)
    • 传统的 HostLink 是基于 RS-232/485 串口转 TCP 的,端口常为 9600
    • 如果 PLC 配置的是 FINS/TCP 原生协议,默认端口通常是 9600(部分老型号是 8501)。请在 PLC 的网络设置中确认端口号。
  3. 存储区代码
    • 代码中读写的 DM区 对应的区域代码是 82(十六进制)。
    • 如果您需要读写 CIO、WR、HR 等其他区域,需要替换命令中的区域代码(例如 CIO 是 30)。
  4. 异步与多线程
    • 在实际的窗体应用(WinForms/WPF)或 Web API 中,绝对不要 使用阻塞式的 .Result.Wait() 调用异步方法,否则极易导致 UI 卡死或线程死锁。请全程保持 async/await 的调用链。
相关推荐
Chris _data1 小时前
并发单词频率统计器 - 从零到完整实现(C# 实战)
开发语言·c#
idolao1 小时前
Oligo 7.60 安装教程:引物设计+Java 环境配置
java·开发语言
不知名的老吴1 小时前
Lambda表达式与新的Streams API相结合
开发语言·python
石山代码8 小时前
ArrayList / HashMap / ConcurrentHashMap
java·开发语言
程序大视界8 小时前
【Python系列课程】Python正则表达式(下):环视、命名分组与日志实战
开发语言·python·正则表达式
枫叶v.9 小时前
Agent 分层存储架构设计:从记忆方法到中间件选型
开发语言·python
sleven fung10 小时前
MinerU与BabelDOC与KTransformers与OpenAI API库
开发语言·python·ai·langchain
萤萤七悬10 小时前
【Python笔记】AI帮实现CLI工具-使用argparse.ArgumentParser接收命令参数
开发语言·笔记·python
iCxhust11 小时前
C# 命令行指令 查看二进制文件
开发语言·单片机·嵌入式硬件·c#·proteus·微机原理·8088单板机
csdn_aspnet11 小时前
Java 霍尔分区算法(Hoare‘s Partition Algorithm)
java·开发语言·算法