Fins TCP协议理解及C Sharp实现思路

news/2024/11/16 10:37:08/文章来源:https://www.cnblogs.com/susume/p/18383230
 
假设本文中使用到设备的ip地址,用于后续内容的理解:
客户端(本机电脑 windows系统)IP: 192.168.1.101
服务端(PLC omron CJ2M系列)IP 和 端口号 : 192.168.1.10 : 9600
 
注意:
①本文中的 FINS TCP 报文都是以16进制(Hex)发送出去的,所以对应的转换也都会转成16进制的形式。
②16进制Hexadecimal (Hex),10进制Decimal (Dec),8进制Octal (Oct),2进制Binary (Bin),下面是10进制 10在C#代码中其他进制的表示。
//16进制 前缀0x 范围(0~F) 
var a = 0xA; 
//2进制 前缀0b 范围(0~1) 
var c = 0b1010; 
//8进制 C#中没有关于8进制的前缀表示,不过可以使用Convert的类型转换,将指定的值转换为指定的进制的字符串表示 
var b = Convert.ToString(10,8);//b的值为 8进制的"12" 
③大端字节序(Big-Endian),小端字节序(Little-Endian)指的是数据存储在以字节为单元的可寻址存储器中的存储模式。
 
假装是个目录:
一、FINS/TCP 头命令 (握手信号,FINS/TCP Header)
  ①Request构成
  ②Response构成
  ③大端、小端
二、FINS帧发送命令 (FINS frame)
  ①完整的命令构成
  ②FINS header
  ③FINS command
  ④FINS parameter/data
  ⑤读,写操作命令
  ⑥强制操作命令
 
一、握手命令 (FINS/TCP 头命令)
Request构成:客户端(电脑)发送给服务端(PLC)报文的构成为:
  头(固定为 FINS 字母ASCII码的16进制)+
  长度(这里的长度指的是,后面全部组加起来的字节数的16进制显示) +
  命令码 +
  错误码 +
  客户端节点地址(电脑IP的最后一部分转为16进制)

举例:
这是一条由客户端(电脑)向服务端(PLC)发送命令 00000000 的报文 46494E53 0000000C 00000000 00000000 00000065,这条报文的总长度为20个字节,分成5组,每组4个字节
  46494E53 固定声明为FINS 头,表明这是一条FINS/TCP命令
  0000000C 长度 、表明 [命令码 错误码 客户端节点地址] 加起来的字节数为 12,转为16进制为C(Hex)
  00000000 命令码
  00000000 错误码
  00000065 电脑的IP地址最后一部分,也就是192.168.1.101 中的 101。转换成16进制是65(Hex)
 
Response构成:服务端(PLC)发送给客户端(电脑)的报文构成为:
  Request构成 +
  服务端节点地址(PLC地址的最后一部分转为16进制)

举例:
然后从服务端(PLC)回复客户端(电脑)的命令 00000001 的报文 46494E53 00000010 00000001 00000000 00000065 0000000A,这条报文的总长度为24个字节,分成6组,每组4个字节
  46494E53 固定声明位FINS头,表明这是一条FINS/TCP命令
  00000010 长度 、表明 [命令码 错误码 客户端节点地址 服务端节点地址]加起来的字节数为16,转为16进制为10(Hex)
  00000001 命令码
  00000000 错误码
  00000065 客户端节点地址
  0000000A 服务端节点地址(PLC IP地址的最后一部分转为16进制,也就是192.168.1.10中的10,。转换成16进制是A(Hex))
 
了解了基本的Fins TCP握手命令的结构后,那么要如何以C#代码的形式呈现呢?这里对如何拼接 Request报文构成 进行实现,用以加深协议命令结构与具体代码实现之间联系的理解。
class DemoFinsTcpHeader
{//完整命令是20个字节static byte[] arrSend = new byte[20];static int cmdIndex = 0;static int clientIpNode = 101;static void Main(){//因为是用于FINS TCP协议使用的协议头,所以直接固定为0x46494E53 即"FINS"的ASCIIbyte[] Header = new byte[4]{0x46, 0x49, 0x4E, 0x53};//FinsTCP命令头    updateArrSend(Header, arrSend);  //长度 通过上面的内容我们知道命令长度为12int cmdLen = 12;updateArrSend(BitConverter.GetBytes(cmdLen).Reverse().ToArray(), arrSend);        //命令码int cmdCode = 0;updateArrSend(BitConverter.GetBytes(cmdCode).Reverse().ToArray(), arrSend);        //错误码int errCode = 0;updateArrSend(BitConverter.GetBytes(errCode).Reverse().ToArray(), arrSend);        //端口节点地址    updateArrSend(BitConverter.GetBytes(clientIpNode).Reverse().ToArray(), arrSend);                }//拼接FINS/TCP 头命令static void updateArrSend(byte[] eachGroupBuf, byte[] curSendArr){for (int i = 0; i < eachGroupBuf.Length; i++){curSendArr[cmdIndex+i] = eachGroupBuf[i];       }   cmdIndex += 4;}
}
虽然上面的代码实现了握手信号的命令的拼接,但是在实际项目的使用中通常不能这样子写,需要考虑到命令拼接方法的可复用型,以及在拼接过程中可能出现的异常情况的预先处理。
关于代码中的 BitConverter.GetBytes(变量名)方法,然后使用了Reverse()对数组进行了反转。之所以需要这么做的原因是因为在C#中使用BitConverter.GetBytes()方法来转换数值类型时,生成的byte数组默认是以本地字节序排列的。而计算机的处理器一般是x86 架构,使用的本地字节序就是我们通常所说的小端存储模式。
 
大端模式、小端模式:
  大端存储模式的特点是,将最高有效字节(MSB)存储在低位地址中,将最低有效字节(LSB)存储在高位地址中。
  小端存储模式的特点是,将最高有效字节(MSB)存储在高位地址中,将最低有效字节(LSB)存储在低位地址中。
大致上可以这样想,位权高的便是高有效字节,位权低的则是低有效字节。比如0x10000064中,4的位权是0, 1的位权是7。

在上图中可以看到,0x0C也就是12的16进制表示在byte数组的索引0位置中。而在C#中,通常声明的数组其内存地址是连续,递增的。所以我们可以看到最低有效字节的0x0C被放到了索引0中。
而根据上面的握手信号指令,显然是不满足的。所以需要将小端字节序改为大端字节序,就需要调用Reverse()方法进行数组的反转。

又因为调用Reverse()方法返回的是IEnumerable类型,所以还需要转换成byte数组类型,调用ToArray()后正确得到 0x0000000C 的长度码。对于其他的如命令码也是相同的操作。
 
二、FINS帧发送命令 (FINS frame)
FINS帧命令实际上就是在握手信号发送之后,与PLC建立了正常的连接,此时再进行与PLC的数据交互,我们就需要使用指定的结构拼接FINS帧命令发送给PLC。
从官方文档中可以看到FINS帧命令的结构是在握手信号命令的基础上继续扩展的。
完整命令的构成:
  FINS头 + 长度码 + 命令码 + 错误码 + FINS frame(大小范围 12 ~ 2012 个字节)

对于FINS frame的构成来说,它有着自己的header, command, parameter/data
FINS header:
  ICF: 显示帧的主要操作信息,其存储的数据2进制表现形式如下。可以看到在图中的描述对于最低位 0的规定,如果需要回复请求则置0,如果不需要回复请求则置1。对于第7位 6的规定则是如果是请求命令则置0,相应则置1。
  那么换算成16进制表示则是,如果请求需要回复为0x80,如果请求不需要回复为0x81,如果是响应请求为0xC0。(0xC1应该没有吧,有知道的可以告诉我谢谢)

  RSV: 表示这是一个由系统进行操作的字节,固定值为0x00;
  GCT:这个含义是说通讯被允许经过的网关的最大数量。通常我们服务端与客户端是小于2个网关进行连接的为了保证通讯的可靠性,所以一般都设置0x02。但是如果需要增加也可以手动修改这个值。
  DNA:目标网络号, 网络号的值与设备路由表相关,一般固定为0x00,如果设置过路由表则需要根据实际情况调整。
  DA1:目标节点号,就是ip地址最后一部分转为16进制表示,假设目标为PLC那么在本文其目标节点号就是0x0A,如果目标为客户端那么在本文其目标节点号则是0x65
  DA2:目标单元号 ,根据官方文档的描述,如果目标是PLC,则固定为0x00。因为直接与CPU通讯。但若是PLC的扩展模块进行通讯,则需要修改对应的值。
  SNA:源网络号,网络号的值与设备路由表相关,一般固定为0x00,如果设置过路由表则需要根据实际情况调整。
  SA1:源节点号,假设源设备为客户端,那么在本文中其源节点号为0x65。
  SA2:源单元号,如果源设备为客户端,在绝大部分情况下其值也仍是0x00。,
  SID:服务ID,固定为0x00
FINS command:
  MRC:主请求码
  SRC:副请求码
其中MRC与SRC一起构成完整的操作命令标识码,用来指定帧命令的作用。包含(0101读操作,0102写操作,2301强制操作),并且从ICF 一直到SRC,每个位置都是占1个字节,这两部分加起来总共12个字节。这就是为什么FINS frame的大小最小为12,因为作为帧命令的组成部分这些是必须的。
FINS parameter/data:
对于帧命令中的数据部分parameter/data,也有其特定的结构。在不同的操作(即主请求码与副请求码组合后的不同命令标识码)中,其每部分所含的字节数,或者各部分的组成顺序也有些许差异。
读操作,与读响应中Parameter/data的构成:
读操作(0x0101):
  Area(寄存器中的区域标识,其中DM为0x82,W字为0xB1, W位为0x31)+
  起始地址(由2个字节的起始地址即整数部分, 加上 1个字节的位地址也就是小数部分,又可以说是偏移量) +
  读取的长度(比如往后面读取1个数据就是 0x0001,读取两个数据就是0x0002)

举例:如下面这条完整的由客户端向服务端的D100地址读1个数据的命令
  46494E53 0000001A 00000002 00000000 (FINS/TCP命令头,其中0x00000002在读写指令中是固定的:FINS + 长度码 + 命令码 + 错误码)
  80 00 02 000A00 006500 00 (FINS frame:ICF + RSV + GCT + DNA+DA1+DA2 + SNA+SA1+SA2 + SID )
  01 01 (FINS command:MRC + SRC)
  82 006400 0001(FINS parameter:area + 起始地址 + 读取的长度)
读响应:
  结束码(或者说错误码, 2个字节) +
  读到的值(长度由需要读的数据个数 * 2个字节,即1个数据占2个字节。比如发送读指令中读取长度是0x0002,那么读到的值的长度就是 2 * 2 = 4个字节,如果读取长度是0x000B,那么读到的值长度就是 11 * 2 = 22个字节)

举例:如通过上面的读指令,服务端响应客户端的完整指令如下:
  46494E53 00000018 00000002 00000000 (FINS/TCP命令头,其中0x00000002在读写指令中是固定的:FINS + 长度码 + 命令码 + 错误码)
  C0 00 02 006500 000A00 00 (FINS frame:ICF + RSV + GCT + DNA+DA1+DA2 + SNA+SA1+SA2 + SID ,需要注意ICF由于是响应读请求,所以它2进制表示中的位权为6的值是1)
  01 01(FINS command:MRC + SRC)
  0000 1122(FINS data:结束码 + 读到的值,由于正常读取所以结束码为0x0000)
写操作,与写响应中Parameter/data的构成:

写操作(0x0102):
  AREA + 起始地址 + 写入长度 + 写入的数据内容
举例:如下面这条完整的由客户端向服务端的D100地址写1个数据的命令
  46494E53 0000001C 00000002 00000000
  80 00 02 000A00 006500 00
  01 02
  82 006400 0001 1000 (FINS parameter:area + 起始地址 + 写入长度 + 写入数据)
写响应:
  结束码(又或者说是错误码)
举例:按照写操作的响应完整命令
  46494E53 00000016 00000002 00000000
  C0 00 02 006500 000A00 00
  01 02
  0000 (FINS data:结束码,写入正常为0x0000)
 
上面的完整指令,为了方便理解所以用空格和换行符进行分隔,在实际中是没有的。
强制操作parameter/data的构成:
强制操作:
  写入的长度 (如0x0001)+
  强制操作命令 (包括0x0001强制置位,0x0000强制复位,0xFFFF强制取消) +
  Area +
  起始地址
举例:由客户端向服务端W101.01写一个强制复位的指令
  46494E53 00000016 00000002 00000000
  80 00 02 00A000 006500 00
  23 01
  0001 0000 31 006501 (FINS parameter:写入的数量 + 强制操作命令 + Area + 起始地址)
 
大致上介绍完了FINS 帧命令的基本结构(如果有漏了哪里请在评论区告诉我,谢谢),那么现在需要考虑到如何在代码中实现。这里给一个简单的读命令拼接示例。
class DemoFinsTcpRead
{static void CreateArrSend(){//要拼接完整的命令,我们需要拿到 FINS/TCP头代码,但是里面需要拿到长度码//长度码指的是,从命令码开始一直到最后的所有字节数,//而在完整的读指令结构中我们已经知道parameter的长度是1 + 3 + 2 = 6个字节//又因为 FINS header + FINS command 的字节数加起来等于  10 + 2 = 12 个字节//再与前面的命令码4个字节 错误码4个字节相加,得到长度应是26个字节,也就是0x1Abyte[] FinsTcpCmd = new byte[50];int cmdIndex = 0;        Array.Copy(new byte[]{0x46, 0x49, 0x4E, 0x53}, FinsTcpCmd, 4);cmdIndex += 4;Buffer.BlockCopy(BitConverter.GetBytes(26).Reverse().ToArray(),0,FinsTcpCmd,cmdIndex,4);cmdIndex += 4;Buffer.BlockCopy(BitConverter.GetBytes(2).Reverse().ToArray(),0,FinsTcpCmd,cmdIndex,4);//读写命令中,固定为0x00000002cmdIndex += 4;Buffer.BlockCopy(BitConverter.GetBytes(0).Reverse().ToArray(),0,FinsTcpCmd,cmdIndex,4);cmdIndex += 4;//本文中规定的客户端节点101,服务端节点10int clientNode = 101;int serviceNode = 10;        //那么现在对FINS frame进行拼接。假设我们对D100地址读取1个数据Buffer.BlockCopy(new byte[]{0x80,     //ICF0x00,     //RSV0x02,     //GCT0x00,     //DNABitConverter.GetBytes(serviceNode)[0],     //DA10x00,     //DA20x00,     //SNABitConverter.GetBytes(clientNode)[0],     //SA10x00,     //SA20x00      //SID}, 0, FinsTcpCmd, cmdIndex, 10);cmdIndex += 10;//然后是对FINS command进行拼接Buffer.BlockCopy(new byte[]{0x01, 0x01}, 0, FinsTcpCmd, cmdIndex, 2);cmdIndex += 2;//最后是对FINS parameter进行拼接        Buffer.BlockCopy(new byte[]{0X82,     //DM区字地址0x00,     //起始地址为2个字节,100取16进制是0x0064,分开存储0x64,     0x00,     //位地址00x00,     //读取1个数量的数据0x01}, 0, FinsTcpCmd, cmdIndex, 6);cmdIndex += 6;//最后拿到的也就是完整命令的FinsTcpCmd的总长度 为34}}

 

为了用于加深对协议的理解,很多地方都做了预设处理。在实际项目中,还需要考虑到命令的复杂多变以及对命令的发送,接收和异常处理机制,等以后有机会再展开吧。
至此,本文就到这里结束了。谢谢你们能看完,如果有发现哪里写错了,请告诉我让我改正谢谢!
 
 
 
参考:
欧姆龙PLC的FINS TCP协议 https://blog.csdn.net/weixin_37700863/article/details/120536223
C#实现16进制与10进制转换 https://www.cnblogs.com/shiyh/p/18186488
进制英文缩写的含义 https://www.runoob.com/w3cnote/hex-dec-oct-bin.html
PLC中的大端小端 https://www.cnblogs.com/depend-wind/articles/13380830.html
上位发送FINS/TCP命令通讯欧姆龙PLC官方文档 https://www.fa.omron.com.cn/upload/faqfiles/201806200316173668.pdf
上位机与欧姆龙PLC的FINS TCP通讯 https://blog.csdn.net/yu_fujiang/article/details/126831440
欧姆龙FInsTcp通讯详解(一)https://blog.csdn.net/sgmcumt/article/details/87435778
 
 
 
 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/788085.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

poc电路

POC电路概念: POC(Power Over Coaxia)一种基于同轴线缆传输的视频信号、同轴控制,电源叠加的技术。在叠加过程中,难度最大的是解决直流电源与高频视频信号叠加传输的问题,保证高频视频信号不失真,低频控制信号不出现乱码。 POC工作原理:POC设计要点:选择电感时的关键参数…

不劳而获?

天雷无妄卦二爻爻辞:不耕获,不菑畲…… 这六个字啥意思?不耕种而有收获不开荒而有熟田?所以就是说可以,不劳而获?嗯? 醒醒吧! 不劳,怎么可能有获?! 当寄生虫还得劳呢,否则只能饿死!其实,换一种理解,还真是可以做到不劳而获,关键就看“劳”与“获”是怎样的一种…

CF1630F-最小割、Dilworth定理

link:https://codeforces.com/contest/1630/problem/F 给你一个由 \(n\) 个顶点组成的无向图,编号从 \(1\) 到 \(n\) ,其中顶点 \(i\) 的值为 \(a_i\) ,所有值 \(a_i\) 都是不同的。如果 \(a_u\) 整除 \(a_v\) ,则两个顶点 \(u\) 和 \(v\) 之间存在一条边。当删除一个顶点…

DocKylin: A Large Multimodal Model for Visual Document Understanding with Efficient Visual Slimming

DocKylin: A Large Multimodal Model for Visual Document Understanding with Efficient Visual Slimming arxiv:http://arxiv.org/abs/2406.19101 视觉处理器+LLM:视觉处理器:Swin Transformer 创新点:通过:1、去除图片冗余像素;2、去除冗余token。来减小模型中的视觉处…

Lab 2: Key/Value Server

6.5840 Lab 2: Key/Value Server 1.Introduction 本次Lab将构建一个单机的键值服务器,该服务器保证即使存在网络故障,每个操作也都只执行一次,并且这些操作线性化执行。后续Lab中,将复制这样的服务器来处理服务器崩溃的情况。 键值服务器支持三种RPC(远程过程调用)操作:Put…

esp-idf vscode debug command espIdf.getXtensaGdb not found

esp32 idf vscode debug错误 vscode中配置文件采用的是正点原子的,调用gdb的时候,提示报错,找不到相应的命令 launch.json文件中gdb的配置如下 {"version": "0.2.0","configurations": [ { "name": "GDB", "type&qu…

Debian12、Ubuntu22安装英特尔wifi驱动

1、打开英特尔无线适配器的 Linux* 支持查看wifi所需的内核版本 以AX200为例,需要Linux内核版本为5.1,Debian12默认内核版本为6.1,Ubuntu24默认内核版本为6..8,因此不需要更新内核。2、打开适用于 Linux* 的英特尔 无线 Wi-Fi 驱动程序下载内核(非必要步骤)和驱动。3、安…

牛客周赛 Round 57

B 可以直接统计每条边两个点的情况即可,不用DFS。 F 写法和这个差不多。可以用map、set、统计这些方法,计算动态的一个数组的最大数。 可以直接用map统计就行,map已经自动给你排好序了(从小到大)。1 #include <bits/stdc++.h>2 using namespace std;3 #define LL lo…

RMQ

RMQ - OI Wiki (oi-wiki.org) 这么说构建和查询,时间复杂度最小的是线段树。最好写的是ST表,emmm,其实线段树也很好写,就是代码量相对多一点。