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正确配置:
- IP地址设置:通过CX-Programmer设置PLC的IP地址
- FINS通信使能:
- 在PLC设置中启用FINS/TCP通信
- 设置网络号、节点号(通常都为0)
- 防火墙:确保9600端口在PLC和PC之间是开放的
- 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
建议
- 连接管理:保持长连接,避免频繁连接/断开
- 异常处理:所有通信操作都要有try-catch
- 超时设置:根据网络状况合理设置超时时间
- 资源释放:使用using语句或确保Dispose被调用
- 日志记录:记录所有通信过程,便于调试
- 心跳机制:定期发送心跳包保持连接
浙公网安备 33010602011771号