FINS(factory interface network service)通信协议是欧姆龙公司开发的用于工业自动化控制网络的指令/响应系统。运用 FINS指令可实现各种网络间的无缝通信,包括用于信息网络的 Etherne(以太网),用于控制网络的Controller Link和SYSMAC LINK。
ORMON PLC的FINS协议看起来非常简单,但其中数据内容涉及高低位转换、16进制整数、字符串,有时需要自己写代码来进行通讯处理。
1、PLC的数据类型
数据类型 | 说明 |
布尔型 | 单个位 |
有符号 16 位值 | |
字 | 无符号 16 位值 |
有符号 32 位值 | |
双字型 | 无符号 32 位值 |
32 位实数 | |
BCD | 两个字节封装的 BCD |
四个字节封装的 BCD | |
空终止 ASCII 字符串。 |
短整型、长整型、双字等也可以是BCD码,需要根据PLC的程序设定进行解析。
1、FINS帧定义
FINS/UDP运用的是一种嵌套格式数据包,即Ethernet报头、IP报头、 UDP报头和FINS帧。一个UDP数据段(FINS 帧)超过1472字节将被分成若干个数据包来传送。分开的UDP数据将在UDP/IP协议层自动组合。通常不须要关注运用 层的数据分段,但是在一个多层 IP网络中1427字节的UDP包可能无法 发送。在这种系统中就须要运用 FINS/TCP方式。
ICF为信息控制域,用于标明指令和响应;
RSV为系统保留;
GCT为网关允许数目;
DNA为目的网络号;
DA1为目的节点号;
DA2为目的单元号;
SNA为源网络号;
SA1为源节点号;
SA2为源单元号;
SID为服务和响应的标识号,可任意配置,指令和响应对应相同;
MRC和SRC分别为 FINS指令的主指令和从指令;
参数/数据域,用于标明所操作的数据地址、范围等,在响应帧中前两个字节MRES和SRES构成响应码,用来诊断不正确信息
填充示例:
2、PLC连接、数据读取和解析示例
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PascalMing
{
public class Tcp_FINS_PascalMingTest
{
TcpClient _tcpClient = new TcpClient();
int _port;
string _host;
byte plcAddr = 0;
byte pcAddr = 0;
int headDm = 30;
const int readDMStart = 5000;
const int readDMCount = 60;
EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.AutoReset);
CancellationTokenSource cts = new CancellationTokenSource();
//返回:46 49 4E 53 00 00 00 10 00 00 00 01 00 00 00 00 00 00 00 F0 00 00 00 9B //PC,PLC IP最后一位(4个字节一组)
//不同网络配置返回值有区别,需要根据实际进行替换
byte[] cmdConnect = { 0x46, 0x49, 0x4E, 0x53, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };//最后一位:PC端IP最后一位
//读D5000,连续100个
byte[] cmdReadD5000 = { 0x46, 0x49, 0x4E, 0x53, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x02, 0x00, 0x9B, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0x01, 0x82, (byte)(readDMStart / 0x100), (byte)(readDMStart % 0x100), 0x00, (byte)(readDMCount/0x100), (byte)(readDMCount%0x100) };
public bool ConnectDevice(string ip, int port)
{
_host = ip;
_port = port;
try
{
_tcpClient = new TcpClient();
_tcpClient.Connect(_host, _port);
//_tcpClient.ReceiveTimeout = 5_000;
_tcpClient.Client.Send(cmdConnect);
byte[] rx = new byte[100];
for (int k = 0; k < 100; k++)
{
if (_tcpClient.Available >= 24)
break;
Thread.Sleep(50);
}
int ret = _tcpClient.Client.Receive(rx);
Console.WriteLine($"connect recv Len:{ret},{rx[0]},{rx[1]},{rx[2]},{rx[3]},{rx[7]}");
if (ret == 24 && rx[0] == 0x46 && rx[1] == 0x49 && rx[2] == 0x4E && rx[3] == 0x53)
{
pcAddr = rx[19];
plcAddr = rx[23];
cmdReadD5000[20] = plcAddr;
cmdReadD5000[23] = pcAddr;
Console.WriteLine("connect ok");
Task.Run(() =>
{
myTaskExecute(cts.Token);
});
return true;
}
Console.WriteLine("connect fail,check fail");
return false;
}
catch (Exception ex)
{
Console.WriteLine("connect fail,ex:"+ex.Message);
return false;
}
}
int count = 0;
async Task myTaskExecute(CancellationToken token)
{
Stopwatch sw = Stopwatch.StartNew();
int timeOutMs = 30_000;
List<byte[]> cmdIndex = new List<byte[]>();
ushort txCount = 0;
try
{
while (!token.IsCancellationRequested)
{
int ret = -1;
byte[] rx = new byte[4096];
Console.WriteLine($"{DateTime.Now} send read");
_tcpClient.Client.Send(cmdReadD5000);
for(int k = 0; k < 200; k ++)
{
if (_tcpClient.Available >= headDm+ readDMCount*2)
break;
await Task.Delay(20, token);
}
Console.WriteLine($"{DateTime.Now} send Available:{_tcpClient.Available}");
if (_tcpClient.Available > 0)
{
sw.Restart();
ret = _tcpClient.Client.Receive(rx);
try
{
Console.WriteLine($"{DateTime.Now} DataReceived len:{ret},data:{rx[7]},{rx[19]},{rx[23]}");
if (ret >= 16 && rx[0] == 0x46 && rx[1] == 0x49 && rx[2] == 0x4E && rx[3] == 0x53)
{
//数据解析根据PLC定义进行,包括数据值。此处使用比较简单的测试方案
Console.WriteLine($"D{5000}:{GetInt16(rx,0)}");
Console.WriteLine($"D{5001}:{GetInt16(rx,1)}");
Console.WriteLine($"D{5010}:{GetInt32_2(rx, 10)}");
Console.WriteLine($"D{5012}:{GetInt32_2(rx, 12)}");
Console.WriteLine($"D{5014}:{GetInt16(rx, 14)}");
Console.WriteLine($"D{5015}:{GetString(rx, 15,40)}");
Console.WriteLine();
}
}
catch (Exception ex)
{
Console.WriteLine("DataReceived parse err:" + ex.Message);
}
}
await Task.Delay(2000, token);
}
}
catch (Exception ex)
{
Console.WriteLine("DataReceived ex:" + ex.Message);
}
Console.WriteLine("DataReceived leave");
}
public int GetInt16(byte[]data,int offset)
{
string sV = $"{data[headDm+ offset*2]:X2}{data[headDm+offset*2+1]:X2}";
int iV = int.Parse(sV);
return iV;
}
public int GetInt32(byte[] data, int offset)
{
string sV = $"{data[headDm + offset*2]:X2}{data[headDm + offset*2 + 1]:X2}{data[headDm + offset*2 + 2]:X2}{data[headDm + offset*2 + 3]:X2}";
int iV = int.Parse(sV);
return iV;
}
public int GetInt32_2(byte[] data, int offset)
{
string sV = $"{data[headDm + offset * 2+2]:X2}{data[headDm + offset * 2 + 3]:X2}{data[headDm + offset * 2 + 0]:X2}{data[headDm + offset * 2 + 1]:X2}";
int iV = int.Parse(sV);
return iV;
}
public string GetString(byte[] data, int offset,int len)
{
int headDm = 30;
StringBuilder sb = new StringBuilder();
for(int k = 0; k < len; k ++)
{
if (data[headDm + offset*2 + k] == 0)
break;
sb.Append($"{(char)data[headDm+offset*2+k]}");
}
return sb.ToString();
}
public void DisconnectDevice()
{
_tcpClient?.Close();
}
}
}
验证:
Tcp_FINS_PascalMingTest tcp_fins = new Tcp_FINS_PascalMingTest();
void Do_Tcp_FINS()
{
tcp_FINS_Yinlun.ConnectDevice("192.168.0.1", 9600);
}