基于 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 的调用链。
相关推荐
qq_422828622 小时前
android图形学之SurfaceControl和Surface的关系 五
android·开发语言·python
如竟没有火炬2 小时前
用队列实现栈
开发语言·数据结构·python·算法·leetcode·深度优先
火星papa3 小时前
C# 任务(Task)的基础实现
c#·任务·task
折哥的程序人生 · 物流技术专研3 小时前
《Java 100 天进阶之路》第17篇:Java常用包装类与自动装箱拆箱深入
java·开发语言·后端·面试
C+++Python3 小时前
C 语言 动态内存分配:malloc /calloc/realloc /free
c语言·开发语言
水木流年追梦3 小时前
大模型入门-应用篇3-Agent智能体
开发语言·python·算法·leetcode·正则表达式
凯瑟琳.奥古斯特4 小时前
假脱机技术原理详解
开发语言·职场和发展
敲代码的瓦龙4 小时前
Java?枚举!!!
java·开发语言
NiceCloud喜云4 小时前
IntelliJ IDEA 保姆级安装 + ClaudeAPI 配置教程
java·开发语言·前端·ide·chrome·docker·intellij-idea