基於西門子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

 測試結果:

 

 

 完畢!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM