基于西门子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 }
下面开始介绍实现的过程
- 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 }
写入
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 }
- 连接初始化
该部分需要完成步骤: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 }
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 }
数据读取
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 }
数据写入
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 }
测试结果:
完毕!
本文来自博客园,作者:豆腐柠檬,转载请注明原文链接:https://www.cnblogs.com/ToufuLemon/p/15762027.html