基于西门子PLC#S7协议上位机通讯(三)-C#通讯模块开发

背景

  在做工控领域系统集成时,由于项目需要跟西门子PLC对接。主要是实现数据的下发及设备状态数据的读取。

  之前采用过两种方式对接:1.采用 OPC UA,但是这个协议对PLC型号有一定要求,1500系列之后PLC才集成了OPC UA服务,前面系列则需要安装西门子内部服务才能使用。2.直接采用Socket TCP/IP通讯,这需要电气PLC工程师与上位机软件人员制定定制化的数据报文格式,对于电气工程师而言就需要写数据解析相关代码,对电气要求高。为了项目的拓展性和通用性,这里就研究起了西门子S7协议。通过S7协议,电气工程师只需要建立对接DB块将对接交互数据放到定义的DB块中就可以实现PLC与上位机对接。

  S7协议是一个通用的协议支持的西门子PLC的系列有:

 1 public enum PlcType
 2     {
 3         S7200 = 1,
 4         S7300 = 2,
 5         S7400 = 3,
 6         S71200 = 4,
 7         S71500 = 5,
 8         S7200Smart = 6
 9 
10     }
View Code

下面开始介绍实现的过程

  1. S7通讯协议

S7协议据说西门子未公布,网上的资料比较少,下面为S7协议结构:

关于S7协议参考文章:

https://www.cnblogs.com/crcce-dncs/p/10659087.html

  2.代码实现

  • 协议

读取

 1  private byte[] GetReadCommand(int dbNum, int startIndex, int len)
 2         {
 3 
 4             //byte[] bLen = GetByteByIntOf3(len);
 5             startIndex = startIndex * 8;
 6             byte[] command = new byte[19 + 12];//31
 7             command[0] = 0x03;
 8             command[1] = 0x00;//[0][1]固定报文头
 9             command[2] = (byte)(command.Length / 256);
10             command[3] = (byte)(command.Length % 256);//[2][3]整个读取请求长度为0x1F= 31 
11             command[4] = 0x02;
12             command[5] = 0xF0;
13             command[6] = 0x80;//COTP
14             command[7] = 0x32;//协议ID
15             command[8] = 0x01;//1  客户端发送命令 3 服务器回复命令
16             command[9] = 0x00;
17             command[10] = 0x00;//[4]-[10]固定6个字节
18             command[11] = 0x00;
19             command[12] = 0x01;//[11][12]两个字节,标识序列号,回复报文相同位置和这个完全一样;范围是0~65535
20             command[13] = (byte)((command.Length - 17) / 256);
21             command[14] = (byte)((command.Length - 17) % 256); //parameter length(减17是因为从[17]到最后属于parameter)
22             command[15] = 0x00;
23             command[16] = 0x00;//data length
24             command[17] = 0x04;//04读 05写
25             command[18] = (byte)1;//读取数据块个数
26 
27             command[19] = 0x12;//variable specification 该字段确定结构的主要类型,对于读/写消息,它总是具有值0x12,代表变量规范
28             command[20] = 0x0A;//Length of following address specification 此项目其余部分的长度
29             command[21] = 0x10;//Syntax Id: S7ANY 此字段确定寻址模式和项结构其余部分的格式。它具有任意类型寻址的常量值0x10 
30             command[22] = 0x02;//bit 0x01,byte 0x02 Variable Type 用于确定变量的类型和长度(使用常用的S7类型,如REAL,BIT,BYTE,WORD,DWORD,COUNTER等)
31             command[23] = (byte)(len / 256);//Count 可以用单个项结构选择整个相似变量数组。这些变量必须具有相同的类型,并且必须在内存中连续,并且count字段确定此数组的大小。对于单变量读或写,它设置为1
32             command[24] = (byte)(len % 256);//Count [23][24]两个字节,访问数据的个数,以byte为单位;
33             command[25] = (byte)(dbNum / 256);//[25][26]DB块的编号 数据库的地址,如果该区域未设置为DB,则忽略该数据库
34             command[26] = (byte)(dbNum % 256);//[25][26]DB块的编号
35             command[27] = 0x84;//选择寻址变量的存储区域,DB块 0x84,I 0x81,Q 0x82,M 0x83 T V...
36 
37             //command[28] = bLen[2];
38             //command[29] = bLen[1];
39             //command[30] = bLen[0];
40 
41             command[28] = (byte)(startIndex / 256 / 256 % 256);//Address 包含所选存储区中寻址变量的偏移量
42             command[29] = (byte)(startIndex / 256 % 256);//Address
43             command[30] = (byte)(startIndex % 256);//Address[28][29][30]访问DB块的偏移量实质上,地址被转换为位偏移并在网络(大端)字节顺序中的3个字节上编码。实际上,由于地址空间小于5位,所以从不使用最重要的5位。作为一个例子,DBX40.3将是0x000143 40 * 8 + 3。
44 
45             return command;
46         }
View Code

写入

 1         private byte[] GetWriteCommand(int dbNum, int startIndex,int len, byte[] inData)
 2         {
 3             startIndex = startIndex * 8;
 4             byte[] command = new byte[35];
 5 
 6             command[0] = 0x03;
 7             command[1] = 0x00;//[0][1]固定报文头
 8             command[2] = (byte)((len + 35) / 256);
 9             command[3] = (byte)((len + 35) % 256);//[2][3]整个读取请求长度
10             command[4] = 0x02;
11             command[5] = 0xF0;
12             command[6] = 0x80;
13             command[7] = 0x32;//protocol Id
14             command[8] = 0x01;//1  客户端发送命令 3 服务器回复命令 Job
15             command[9] = 0x00;
16             command[10] = 0x00;//[9][10] redundancy identification (冗余的识别)
17             command[11] = 0x00;
18             command[12] = 0x01;//[11]-[12]protocol data unit reference
19             command[13] = 0x00;
20             command[14] = 0x0E;//Parameter length
21             command[15] = (byte)((len+4) / 256);
22             command[16] = (byte)((len+4) % 256);//[15][16] Data length
23             //Parameter
24             command[17] = 0x05;//04读 05写 Function Write
25             command[18] = 0x01;//写入数据块个数 Item count
26             command[19] = 0x12;
27             command[20] = 0x0A;
28             command[21] = 0x10;//[19]-[21]固定
29             command[22] = 0x02;//写入方式,1是按位,2是按字
30             command[23] = (byte)(len / 256);
31             command[24] = (byte)(len % 256);//写入数据个数
32             command[25] = (byte)(dbNum / 256);
33             command[26] = (byte)(dbNum % 256);//DB块的编号
34             command[27] = 0x84;//访问数据块的类型,DB块 0x84,I 0x81,Q 0x82,M 0x83 T V...
35 
36             command[28] = (byte)(startIndex / 256 / 256 % 256); ;
37             command[29] = (byte)(startIndex / 256 % 256);
38             command[30] = (byte)(startIndex % 256);//[28][29][30]访问DB块的偏移量      
39 
40             command[31] = 0x00;
41             command[32] = 0x04;// 03bit(位)04 byte(字节)
42             command[33] = (byte)((len * 8) / 256);
43             command[34] = (byte)((len * 8) % 256);//按位计算出的长度
44             return command.Concat(inData).ToArray();
45         }
View Code
  •  连接初始化

  该部分需要完成步骤:1.建立Socket连接;2.根据选择PLC型号创建S7交互报文;3.S7第一次连接交互;4:S7第二次连接交互;5.PDU解析计算最多读取长度

 

 

  1         public bool Open(out string msg)
  2         {
  3             msg = string.Empty;
  4             try
  5             {
  6                 System.Diagnostics.Stopwatch sp = new System.Diagnostics.Stopwatch();
  7                 sp.Start();
  8 
  9                 #region 1.连接
 10                 if (!SocketHelper.PingCheck(Ip, ConnectTimeout))
 11                 {
 12                     msg = "网络故障!";
 13                     return false;
 14                 }
 15                 tcpClient = new TcpClient();
 16                 tcpClient.ReceiveTimeout = ReceiveTimeout;
 17                 tcpClient.SendTimeout = SendTimeout;
 18                 tcpClient.Connect(Ip, Port);
 19                 Thread.Sleep(10);
 20                 if (!tcpClient.Connected)
 21                 {
 22                     throw new ApplicationException($"未连接到{Ip}");
 23                 }
 24                 #endregion
 25 
 26                 #region 2.PLC型号
 27                 var Command1 = SiemensConstant.Command1;
 28                 var Command2 = SiemensConstant.Command2;
 29 
 30                 switch (PlcType)
 31                 {
 32                     case PlcType.S7200:
 33                         Command1 = SiemensConstant.Command1_200;
 34                         Command2 = SiemensConstant.Command2_200;
 35                         break;
 36                     case PlcType.S7200Smart:
 37                         Command1 = SiemensConstant.Command1_200Smart;
 38                         Command2 = SiemensConstant.Command2_200Smart;
 39                         break;
 40                     case PlcType.S7300:
 41                         Command1[21] = (byte)((Rack * 0x20) + Slot); //0x02;
 42                         break;
 43                     case PlcType.S7400:
 44                         Command1[21] = (byte)((Rack * 0x20) + Slot); //0x03;
 45                         Command1[17] = 0x00;
 46                         break;
 47                     case PlcType.S71200:
 48                         Command1[21] = (byte)((Rack * 0x20) + Slot); //0x00;
 49                         break;
 50                     case PlcType.S71500:
 51                         Command1[21] = (byte)((Rack * 0x20) + Slot); //0x00;
 52                         break;
 53                     default:
 54                         Command1[18] = 0x00;
 55                         break;
 56                 }
 57                 #endregion
 58 
 59                 #region 3.一次交互
 60                 if (!SocketHelper.SendData(out msg, tcpClient, Command1))//03 00 00 16 11 E0 00 00 00 01 00 C0 01 0A C1 02 01 02 C2 02 01 01
 61                 {
 62                     msg = $"连接1,数据写入失败:{msg}!";
 63                     return false;
 64                 }
 65 
 66                 //开始读取返回信号1
 67                 byte[] head1 = new byte[Command1.Length];
 68                 if (!SocketHelper.ReceiveData(out msg, tcpClient, head1))//03 00 00 16 11 d0 00 01 00 01 00 c0 01 0a c1 02 01 02 c2 02 01 01 
 69                 {
 70                     msg = $"连接握手[#1]接收失败:{msg}!";
 71                     return false;
 72                 }
 73                 int len1 = PlcDataHelper.GetS16From(head1,2);
 74 
 75                 if(len1!= Command1.Length)
 76                 {
 77                     msg = $"连接握手[#1]失败:接收长度不为[{Command1.Length}]!";
 78                     return false;
 79                 }
 80                 #endregion
 81 
 82                 #region 4.二次交互
 83                 if (!SocketHelper.SendData(out msg, tcpClient, Command2))
 84                 {
 85                     msg = $"连接2,数据写入失败:{msg}!";
 86                     return false;
 87                 }
 88 
 89 
 90                 //开始读取返回信号1
 91                 byte[] head2 = new byte[Command2.Length+2];
 92                 if (!SocketHelper.ReceiveData(out msg, tcpClient, head2))
 93                 {
 94                     msg = $"连接握手信号2接收失败:{msg}!";
 95                     return false;
 96                 }
 97                 int len2 = PlcDataHelper.GetS16From(head2, 2);
 98                 if (len2 != (Command2.Length + 2))
 99                 {
100                     msg = $"连接握手[#2]失败:接收长度不为[{Command2.Length + 2}]!";
101                     return false;
102                 }
103                 #endregion
104 
105                 #region 5.PDU计算
106                 //PDU ->240、480、960
107                 PDU = PlcDataHelper.GetS16From(head2, head2.Length - 2);
108                 MAXCOUNT = PDU - 18;
109                 #endregion
110 
111                 msg = $"连接[{Ip}]成功,耗时{sp.Elapsed.TotalMilliseconds.ToString()}ms";
112                 return true;
113 
114             }
115             catch (Exception ex)
116             {
117                 Close(out string _msg);//连接断开,重试
118                 msg = $"连接失败:{ex.Message}";
119                 return false;
120             }
121         }
View Code

Socket连接数据接收与发送在C#中有两种方式,见前面文章所写。

  • 断开销毁连接

在读取异常时尽量断开及销毁

 1         public bool Close(out string msg)
 2         {
 3             msg = string.Empty;
 4             try
 5             {
 6                 tcpClient?.Close();
 7                 tcpClient = null;
 8                 return true;
 9             }
10             catch (Exception ex)
11             {
12                 msg = $"关闭失败:{ex.Message}";
13                 tcpClient = null;
14                 return false;
15             }
16         }
View Code

数据读取

 1 public bool ReadDataBytes(out string msg,int dbNum, int startIndex, int len, out byte[] reData)
 2         {
 3             msg = string.Empty; reData = new byte[0];
 4             try
 5             {
 6                 System.Diagnostics.Stopwatch sp = new System.Diagnostics.Stopwatch();
 7                 sp.Start();
 8 
 9                 #region 连接状态
10                 if (tcpClient == null || !tcpClient.Connected)
11                 {
12                     if (!Open(out msg))
13                     {
14                         Thread.Sleep(40);
15                         if (!Open(out msg)) return false;
16                     }
17                 }
18                 #endregion
19                 int i = 0;
20                 for (int index = startIndex; index < startIndex + len; index += MAXCOUNT)
21                 {
22                     int _newLen = len + startIndex - index;
23                     if (_newLen > MAXCOUNT) _newLen = MAXCOUNT;
24                     i++;
25 
26                     #region 读取
27                     byte[] command = GetReadCommand(dbNum, index, _newLen);
28 
29                     if (!SocketHelper.SendData(out msg, tcpClient, command))
30                     {
31                         msg = $"发送读取指令失败:{msg}!";
32                         return false;
33                     }
34                     //B[12]~B[13] = 0x001C = 序列号
35                     //B[16]~B[17] = 0x0015 = 21 = 读取请求count(17) + 4
36                     //B[24]~B[25] = 0x0088 = 17 * 8 = 请求数据长度(bit为单位)
37                     //B[26]~最后 = 数据值)
38                     byte[] nData = new byte[_newLen + 25];
39 
40                     if (!SocketHelper.ReceiveData(out msg, tcpClient, nData))
41                     {
42                         msg = $"接收读取数据1失败:{msg}!";
43                         return false;
44                     }
45                     #endregion
46                     #region 校验
47                     //0x04 读 0x01 读取一个长度 //如果是批量读取,批量读取方法里面有验证
48                     if (nData[19] == 0x04 && nData[20] == 0x01)
49                     {
50                         if (nData[21] == 0x0A && nData[22] == 0x00)
51                         {
52                             msg = $"读取失败,请确认地址是否正确!";
53                             return false;
54                         }
55                         else if (nData[21] == 0x05 && nData[22] == 0x00)
56                         {
57                             msg = $"读取失败,请确认地址是否正确!";
58                             return false;
59                         }
60                         else if (nData[21] != 0xFF)
61                         {
62                             msg = $"读取失败,异常代码[21]:{nData[21]}";
63                             return false;
64                         }
65                     }
66                     #endregion
67 
68                     byte[] bytes = new byte[_newLen];
69                     Array.Copy(nData, 25, bytes, 0, _newLen);
70                     reData = reData.Concat(bytes).ToArray();
71           
72                 }
73                 msg = $"读取({reData.Length})字节数据成功,耗时{sp.Elapsed.TotalMilliseconds.ToString()}ms,{i}次读取";
74                 return true;
75             }
76             catch (Exception ex)
77             {
78                 Close(out string _msg);
79                 msg = $"读取数据失败:{ex.Message}.{_msg}";
80                 return false;
81             }
82         }
View Code

数据写入

 1  public bool WriteDataBytes(out string msg, int dbNum, int startIndex, byte[] inData)
 2         {
 3             msg = string.Empty;
 4             try
 5             {
 6                 System.Diagnostics.Stopwatch sp = new System.Diagnostics.Stopwatch();
 7                 sp.Start();
 8                 #region 连接状态
 9                 if (tcpClient == null || !tcpClient.Connected)
10                 {
11                     if (!Open(out msg))
12                     {
13                         Thread.Sleep(40);
14                         if (!Open(out msg)) return false;
15                     }
16                 }
17                 #endregion
18                 //奇数补零,写入数据必须为一个字
19                 if ((inData.Length % 2) > 0)
20                 {
21                     inData = inData.Concat(new byte[1] { 0 }).ToArray();
22                 }
23                 int len = inData.Length;
24                 int i = 0;
25                 for (int index = startIndex; index < startIndex + len; index += SiemensConsts.MAXRWRIDATE)
26                 {
27                     int _newLen = len + startIndex - index;
28                     if (_newLen > SiemensConsts.MAXRWRIDATE) _newLen = SiemensConsts.MAXRWRIDATE;
29                     i++;
30 
31                     byte[] nData = new byte[_newLen];
32                     Array.Copy(inData, index - startIndex, nData, 0, _newLen);
33                     //写入
34                     byte[] command = GetWriteCommand(dbNum, index, _newLen, nData);
35 
36                     if (!SocketHelper.SendData(out msg, tcpClient, command))
37                     {
38                         msg = $"发送写入指令失败:{msg}!";
39                         return false;
40                     }
41                     byte[] content = new byte[22];
42 
43                     if (!SocketHelper.ReceiveData(out msg, tcpClient, content))
44                     {
45                         msg = $"写入数据接收失败:{msg}!";
46                         return false;
47                     }
48                     #region 校验
49                     var offset = content.Length - 1;
50                     if (content[offset] == 0x0A)
51                     {
52                         msg = $"写入失败,异常代码[{offset}]:{content[offset]}!";
53                         return false;
54                     }
55                     else if (content[offset] == 0x05)
56                     {
57                         msg = $"写入失败,异常代码[{offset}]:{content[offset]}!";
58                         return false;
59                     }
60                     else if (content[offset] != 0xFF)
61                     {
62                         msg = $"写入失败,异常代码[{offset}]:{content[offset]}!";
63                         return false;
64                     }
65                     #endregion
66                 }
67                 msg = $"写入({inData.Length})字节数据成功,耗时{sp.Elapsed.TotalMilliseconds.ToString()}ms,{i}次写入";
68                 return true;
69             }
70             catch (Exception ex)
71             {
72                 Close(out string _msg);
73                 msg = $"写入数据失败:{ex.Message}.{_msg}";
74                 return false;
75             }
76         }
View Code

 测试结果:

 

 

 完毕!

posted @ 2022-01-06 11:11  豆腐柠檬  阅读(1056)  评论(2编辑  收藏  举报