C#通过FINS TCP协议与欧姆龙PLC进行通信

C#通过FINS TCP协议与欧姆龙PLC进行通信。FINS是欧姆龙PLC的专用工业网络协议,基于TCP实现稳定可靠的数据交换。

FINS TCP协议核心要点

协议基础

  • 端口:默认使用9600端口
  • 命令格式:每个FINS指令都遵循特定的二进制帧结构
  • 数据存储区:欧姆龙PLC有不同的内存区域
    • CIO区:输入输出继电器区
    • WR区:工作继电器区
    • DM区:数据存储区
    • HR区:保持继电器区
    • AR区:辅助继电器区

C# FINS TCP通信完整实现

1. 核心通信类

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

public class OmronFinsTcp
{
    private TcpClient _tcpClient;
    private NetworkStream _stream;
    private string _ipAddress;
    private int _port;
    private byte _SA1 = 0;  // PLC网络号(通常为0)
    private byte _SA2 = 0;  // PLC单元号(通常为0)
    private byte _DA1 = 0;  // PC网络号
    private byte _DA2 = 0;  // PC单元号
    private ushort _sid = 0; // 事务ID,每次通信递增
    
    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="ip">PLC IP地址</param>
    /// <param name="port">端口,默认9600</param>
    public OmronFinsTcp(string ip, int port = 9600)
    {
        _ipAddress = ip;
        _port = port;
        _sid = 0;
    }
    
    /// <summary>
    /// 连接到PLC
    /// </summary>
    /// <returns>连接是否成功</returns>
    public bool Connect()
    {
        try
        {
            _tcpClient = new TcpClient();
            _tcpClient.Connect(IPAddress.Parse(_ipAddress), _port);
            _tcpClient.ReceiveTimeout = 3000;
            _tcpClient.SendTimeout = 3000;
            _stream = _tcpClient.GetStream();
            
            // 发送连接请求(FINS头)
            return SendConnectionRequest();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"连接失败: {ex.Message}");
            return false;
        }
    }
    
    /// <summary>
    /// 断开连接
    /// </summary>
    public void Disconnect()
    {
        try
        {
            _stream?.Close();
            _tcpClient?.Close();
        }
        catch { }
    }
    
    /// <summary>
    /// 发送FINS连接请求
    /// </summary>
    private bool SendConnectionRequest()
    {
        // FINS TCP连接请求帧
        byte[] connectionRequest = new byte[24];
        
        // 头部(固定)
        connectionRequest[0] = 0x46; // 'F'
        connectionRequest[1] = 0x49; // 'I'
        connectionRequest[2] = 0x4E; // 'N'
        connectionRequest[3] = 0x53; // 'S'
        
        // 长度(24字节)
        connectionRequest[4] = 0x00;
        connectionRequest[5] = 0x00;
        connectionRequest[6] = 0x00;
        connectionRequest[7] = 0x18;
        
        // 命令代码(连接)
        connectionRequest[8] = 0x00;
        connectionRequest[9] = 0x00;
        connectionRequest[10] = 0x00;
        connectionRequest[11] = 0x02;
        
        // 错误代码
        connectionRequest[12] = 0x00;
        connectionRequest[13] = 0x00;
        connectionRequest[14] = 0x00;
        connectionRequest[15] = 0x00;
        
        // 客户端节点信息
        connectionRequest[19] = 0x00; // 网络号
        connectionRequest[20] = 0x00; // 节点号
        connectionRequest[21] = 0x00; // 单元号
        
        // 发送请求
        _stream.Write(connectionRequest, 0, connectionRequest.Length);
        
        // 接收响应
        byte[] response = new byte[24];
        int bytesRead = _stream.Read(response, 0, response.Length);
        
        return bytesRead == 24 && response[11] == 0x00;
    }
    
    /// <summary>
    /// 构建FINS指令头
    /// </summary>
    private byte[] BuildFinsHeader(byte[] command)
    {
        byte[] header = new byte[16 + command.Length];
        
        // FINS头部标识
        header[0] = 0x46; // 'F'
        header[1] = 0x49; // 'I'
        header[2] = 0x4E; // 'N'
        header[3] = 0x53; // 'S'
        
        // 命令长度
        int length = command.Length + 8; // 命令长度 + 8字节FINS头
        header[4] = (byte)((length >> 24) & 0xFF);
        header[5] = (byte)((length >> 16) & 0xFF);
        header[6] = (byte)((length >> 8) & 0xFF);
        header[7] = (byte)(length & 0xFF);
        
        // 命令代码(内存区读写)
        header[8] = 0x00;
        header[9] = 0x00;
        header[10] = 0x00;
        header[11] = 0x02; // 固定
        
        // 错误代码
        header[12] = 0x00;
        header[13] = 0x00;
        header[14] = 0x00;
        header[15] = 0x00;
        
        // 复制命令数据
        Array.Copy(command, 0, header, 16, command.Length);
        
        return header;
    }
    
    /// <summary>
    /// 发送FINS命令并接收响应
    /// </summary>
    private byte[] SendFinsCommand(byte[] command)
    {
        try
        {
            // 构建完整FINS帧
            byte[] finsFrame = BuildFinsHeader(command);
            
            // 发送命令
            _stream.Write(finsFrame, 0, finsFrame.Length);
            
            // 接收响应头(前16字节)
            byte[] responseHeader = new byte[16];
            int headerBytes = _stream.Read(responseHeader, 0, 16);
            
            if (headerBytes != 16)
                throw new Exception("响应头长度不正确");
            
            // 获取响应数据长度
            int dataLength = (responseHeader[7] << 0) | 
                           (responseHeader[6] << 8) | 
                           (responseHeader[5] << 16) | 
                           (responseHeader[4] << 24);
            dataLength -= 8; // 减去FINS头长度
            
            // 接收响应数据
            byte[] responseData = new byte[dataLength];
            int totalBytesRead = 0;
            
            while (totalBytesRead < dataLength)
            {
                int bytesRead = _stream.Read(responseData, totalBytesRead, dataLength - totalBytesRead);
                if (bytesRead == 0) break;
                totalBytesRead += bytesRead;
            }
            
            return responseData;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"发送命令失败: {ex.Message}");
            return null;
        }
    }
}

2. 数据读写功能类

public class OmronPlcCommunication : OmronFinsTcp
{
    public OmronPlcCommunication(string ip, int port = 9600) : base(ip, port) { }
    
    /// <summary>
    /// 读取DM区数据(16位)
    /// </summary>
    /// <param name="address">DM地址(如100表示D100)</param>
    /// <param name="count">读取字数</param>
    public ushort[] ReadDM(ushort address, ushort count = 1)
    {
        byte[] command = new byte[12];
        
        // 命令头
        command[0] = 0x01; // ICF
        command[1] = 0x00; // RSV
        command[2] = 0x00; // GCT
        command[3] = 0x00; // DNA
        command[4] = 0x00; // DA1
        command[5] = 0x00; // DA2
        command[6] = 0x00; // SNA
        command[7] = 0x00; // SA1
        command[8] = 0x00; // SA2
        command[9] = 0x00; // SID
        
        // 读取命令
        command[10] = 0x01; // MRC(主请求代码)
        command[11] = 0x01; // SRC(子请求代码)
        
        // 内存区域(DM区 = 0x82)
        command[12] = 0x82;
        
        // 起始地址
        command[13] = (byte)((address >> 8) & 0xFF);
        command[14] = (byte)(address & 0xFF);
        command[15] = 0x00; // 位地址(0表示字访问)
        
        // 读取数量
        command[16] = (byte)((count >> 8) & 0xFF);
        command[17] = (byte)(count & 0xFF);
        
        // 发送命令
        byte[] response = SendFinsCommand(command);
        
        if (response == null || response.Length < 2)
            return null;
            
        // 解析响应数据
        ushort[] values = new ushort[count];
        for (int i = 0; i < count; i++)
        {
            int index = 14 + i * 2; // 跳过响应头
            values[i] = (ushort)((response[index] << 8) | response[index + 1]);
        }
        
        return values;
    }
    
    /// <summary>
    /// 写入DM区数据(16位)
    /// </summary>
    /// <param name="address">DM地址</param>
    /// <param name="values">要写入的值</param>
    public bool WriteDM(ushort address, ushort[] values)
    {
        byte[] command = new byte[12 + values.Length * 2];
        
        // 命令头
        command[0] = 0x01; // ICF
        command[1] = 0x00; // RSV
        command[2] = 0x00; // GCT
        command[3] = 0x00; // DNA
        command[4] = 0x00; // DA1
        command[5] = 0x00; // DA2
        command[6] = 0x00; // SNA
        command[7] = 0x00; // SA1
        command[8] = 0x00; // SA2
        command[9] = 0x00; // SID
        
        // 写入命令
        command[10] = 0x01; // MRC
        command[11] = 0x02; // SRC(写命令)
        
        // 内存区域(DM区 = 0x82)
        command[12] = 0x82;
        
        // 起始地址
        command[13] = (byte)((address >> 8) & 0xFF);
        command[14] = (byte)(address & 0xFF);
        command[15] = 0x00; // 位地址
        
        // 写入数量
        ushort count = (ushort)values.Length;
        command[16] = (byte)((count >> 8) & 0xFF);
        command[17] = (byte)(count & 0xFF);
        
        // 写入数据
        for (int i = 0; i < values.Length; i++)
        {
            command[18 + i * 2] = (byte)((values[i] >> 8) & 0xFF);
            command[19 + i * 2] = (byte)(values[i] & 0xFF);
        }
        
        // 发送命令
        byte[] response = SendFinsCommand(command);
        
        return response != null && response.Length >= 2 && response[13] == 0x00;
    }
    
    /// <summary>
    /// 读取CIO区位状态
    /// </summary>
    public bool ReadCIOBit(ushort address, byte bit)
    {
        byte[] command = new byte[12];
        
        // 命令头
        command[0] = 0x01;
        command[1] = 0x00;
        command[2] = 0x00;
        command[3] = 0x00;
        command[4] = 0x00;
        command[5] = 0x00;
        command[6] = 0x00;
        command[7] = 0x00;
        command[8] = 0x00;
        command[9] = 0x00;
        
        // 读取命令
        command[10] = 0x01;
        command[11] = 0x01;
        
        // 内存区域(CIO区 = 0xB0)
        command[12] = 0xB0;
        
        // 起始地址
        command[13] = (byte)((address >> 8) & 0xFF);
        command[14] = (byte)(address & 0xFF);
        command[15] = bit; // 位地址(0-15)
        
        // 读取1位
        command[16] = 0x00;
        command[17] = 0x01;
        
        byte[] response = SendFinsCommand(command);
        
        return response != null && response.Length >= 15 && response[14] == 0x01;
    }
    
    /// <summary>
    /// 写入CIO区位状态
    /// </summary>
    public bool WriteCIOBit(ushort address, byte bit, bool value)
    {
        byte[] command = new byte[12 + 2];
        
        // 命令头
        command[0] = 0x01;
        command[1] = 0x00;
        command[2] = 0x00;
        command[3] = 0x00;
        command[4] = 0x00;
        command[5] = 0x00;
        command[6] = 0x00;
        command[7] = 0x00;
        command[8] = 0x00;
        command[9] = 0x00;
        
        // 写入命令
        command[10] = 0x01;
        command[11] = 0x02;
        
        // 内存区域(CIO区 = 0xB0)
        command[12] = 0xB0;
        
        // 起始地址
        command[13] = (byte)((address >> 8) & 0xFF);
        command[14] = (byte)(address & 0xFF);
        command[15] = bit; // 位地址
        
        // 写入1位
        command[16] = 0x00;
        command[17] = 0x01;
        
        // 写入的数据
        command[18] = value ? (byte)0x01 : (byte)0x00;
        command[19] = 0x00;
        
        byte[] response = SendFinsCommand(command);
        
        return response != null && response.Length >= 2 && response[13] == 0x00;
    }
    
    /// <summary>
    /// 读取保持继电器HR区
    /// </summary>
    public ushort[] ReadHR(ushort address, ushort count = 1)
    {
        return ReadMemoryArea(0xB2, address, count); // HR区代码
    }
    
    /// <summary>
    /// 读取工作继电器WR区
    /// </summary>
    public ushort[] ReadWR(ushort address, ushort count = 1)
    {
        return ReadMemoryArea(0xB1, address, count); // WR区代码
    }
    
    private ushort[] ReadMemoryArea(byte areaCode, ushort address, ushort count)
    {
        byte[] command = new byte[12];
        
        // 命令头
        command[0] = 0x01;
        command[1] = 0x00;
        command[2] = 0x00;
        command[3] = 0x00;
        command[4] = 0x00;
        command[5] = 0x00;
        command[6] = 0x00;
        command[7] = 0x00;
        command[8] = 0x00;
        command[9] = 0x00;
        
        // 读取命令
        command[10] = 0x01;
        command[11] = 0x01;
        
        // 内存区域
        command[12] = areaCode;
        
        // 起始地址
        command[13] = (byte)((address >> 8) & 0xFF);
        command[14] = (byte)(address & 0xFF);
        command[15] = 0x00;
        
        // 读取数量
        command[16] = (byte)((count >> 8) & 0xFF);
        command[17] = (byte)(count & 0xFF);
        
        byte[] response = SendFinsCommand(command);
        
        if (response == null || response.Length < 2)
            return null;
            
        ushort[] values = new ushort[count];
        for (int i = 0; i < count; i++)
        {
            int index = 14 + i * 2;
            values[i] = (ushort)((response[index] << 8) | response[index + 1]);
        }
        
        return values;
    }
}

3. 使用示例

class Program
{
    static void Main(string[] args)
    {
        try
        {
            // 创建PLC通信实例
            var plc = new OmronPlcCommunication("192.168.1.100", 9600);
            
            // 连接到PLC
            if (plc.Connect())
            {
                Console.WriteLine("成功连接到PLC");
                
                // 示例1:读取DM区数据
                ushort[] dmValues = plc.ReadDM(100, 5); // 读取D100-D104
                if (dmValues != null)
                {
                    Console.WriteLine("DM区数据:");
                    for (int i = 0; i < dmValues.Length; i++)
                    {
                        Console.WriteLine($"D{100 + i}: {dmValues[i]}");
                    }
                }
                
                // 示例2:写入DM区数据
                ushort[] writeData = new ushort[] { 100, 200, 300 };
                bool writeSuccess = plc.WriteDM(200, writeData); // 写入D200-D202
                Console.WriteLine($"写入DM区: {writeSuccess}");
                
                // 示例3:读取CIO区位状态
                bool bitStatus = plc.ReadCIOBit(0, 0); // 读取CIO 0.00
                Console.WriteLine($"CIO 0.00状态: {bitStatus}");
                
                // 示例4:写入CIO区位
                bool writeBitSuccess = plc.WriteCIOBit(0, 1, true); // CIO 0.01置为ON
                Console.WriteLine($"写入CIO位: {writeBitSuccess}");
                
                // 示例5:读取HR区
                ushort[] hrValues = plc.ReadHR(0, 10); // 读取HR0-HR9
                if (hrValues != null)
                {
                    Console.WriteLine("HR区数据:");
                    for (int i = 0; i < hrValues.Length; i++)
                    {
                        Console.WriteLine($"HR{i}: {hrValues[i]}");
                    }
                }
                
                // 断开连接
                plc.Disconnect();
                Console.WriteLine("已断开连接");
            }
            else
            {
                Console.WriteLine("连接PLC失败");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"发生错误: {ex.Message}");
        }
        
        Console.ReadLine();
    }
}

PLC端配置关键步骤

在开始通信前,必须确保欧姆龙PLC正确配置:

  1. IP地址设置:通过CX-Programmer设置PLC的IP地址
  2. FINS通信使能
    • 在PLC设置中启用FINS/TCP通信
    • 设置网络号、节点号(通常都为0)
  3. 防火墙:确保9600端口在PLC和PC之间是开放的
  4. PLC工作模式:PLC需处于RUN或MONITOR模式

故障排除指南

问题 可能原因 解决方法
连接超时 IP地址错误/网络不通 检查IP和网络连接
连接被拒绝 PLC未启用FINS TCP 在CX-Programmer中启用FINS
读取数据失败 地址超出范围 检查PLC型号支持的内存范围
响应超时 PLC处于PROGRAM模式 切换到RUN/MONITOR模式
数据不正确 字节顺序问题 确认PLC的字节顺序(大端/小端)

高级功能扩展

对于更复杂的应用,你还可以添加以下功能:

// 批量读取优化
public class BatchReader
{
    private OmronPlcCommunication _plc;
    private Dictionary<string, ushort[]> _tagValues;
    
    public void AddTag(string tagName, ushort address, ushort length)
    {
        // 实现标签批量读取
    }
    
    public bool ReadAll()
    {
        // 优化读取,减少通信次数
    }
}

// 异步通信支持
public async Task<ushort[]> ReadDMAsync(ushort address, ushort count)
{
    return await Task.Run(() => ReadDM(address, count));
}

// 事件通知机制
public event EventHandler<DataChangedEventArgs> DataChanged;

参考代码 C#编写OMRON Fins TCP协议与OMRON PLC通讯 www.3dddown.com/cnb/38791.html

建议

  1. 连接管理:保持长连接,避免频繁连接/断开
  2. 异常处理:所有通信操作都要有try-catch
  3. 超时设置:根据网络状况合理设置超时时间
  4. 资源释放:使用using语句或确保Dispose被调用
  5. 日志记录:记录所有通信过程,便于调试
  6. 心跳机制:定期发送心跳包保持连接
posted @ 2026-01-23 16:39  chen_yig  阅读(1)  评论(0)    收藏  举报