PLCSIM Advanced 简介
PLCSIMAdvanced是西门子推出的一款功能强大的仿真软件,目前新发布的版本为4.0,但鉴于新版本可能存在未知的bug,故本文使用V3.0。
V3.0支持仿真1500PLC及ET200SP,可实现Socket网络通讯功能,也可实现PLC之间、PLC与设备直接的ModbusTCP等通讯。
V3.0安装时需要先安装WinPcap_4_1_3,V4.0则不需要。
以下为两个版本的官网下载链接,下载时需要西门子账号,可以免费注册。
以下为V3.0下载链接:
PLCSIM Advanced V3.0
V3.0的两个升级包(可选安装)
以下为V4.0下载链接
PLCSIM Advanced V4.0
S7 Net Plus 简介
西门子PLC通讯库,支持200、200smart、300、400、1200、1500系列PLC。
说明文档
配置PLCSIM Advanced
打开PLCSIM Advanced V3.0,如下图:
Online Access要选择右边的PLCSIM VirtualEth.Adapter,左侧的PLCSIM不支持外部网络访问。
TCP/IP communication with可选以太网或者是本地虚拟网卡。local即为本地虚拟网卡,是在安装PLCSIMAdvanced时自动安装的网络适配器。打开控制面板-->网络和 Internet-->网络连接,SiemensPLCSIMVirtual Ethernet Adapter就是此虚拟网卡。使用虚拟网卡只能在本机进行通讯仿真,而使用以太网则可以在局域网内进行仿真通讯。
Start Virtual S7-1500PLC为PLC设置,包括IP地址、子网掩码、默认网关及PLC型号。设置完成后点击Start按钮则会生成一个PLC实例。创建成功后就可以开始通讯仿真了。
Virtual SIMATIC Memory Ca为打开保存PLC历史记录的文件夹的按钮。
如下图所示,在Active PLC Instance(s)可以看到已成功创建的PLC。
下载测试DB块
在TIA Protal软件中,添加一个S7-1511的设备,在程序块中添加一个新的DB块,DB号设置为10。
打开设备的属性 --> 防护与安全 -->连接机制,勾选“允许来自远程对象的PUT/GET通讯访问”。
打开设备的属性 --> PROFINET 接口 [X1]-->以太网地址,按需设置PLC的IP地址。
打开DB10的属性,取消勾选“优化块的访问”,并在DB10中新建如下图所示的变量,编译完成后则可以得到每个变量的偏移量,即此变量在DB10上的地址。
设置完成后,下载到刚刚使用PLCSIM Advanced创建的仿真PLC中,需要注意网段要设置成与仿真PLC同一网段。
引用S7NetPlus
创建一个测试程序,此处创建的是一个控制台应用程序。
在NuGet下载S7NetPlus,如下图所示,版本可按需选择
新建一个名为PLCInstance的类,创建PLC单例。
class PLCInstance { private PLCInstance() { plcObj = new Plc(CpuType.S71500, "192.168.10.230", 0, 1); } ////// PLC单例 ///public static PLCInstance Instance { get { return Nested.instance; } } ////// 防止调用此类静态方法时,创建新的实例 ///private class Nested { internal static readonly PLCInstance instance = null; static Nested() { instance = new PLCInstance(); } } ////// 私有PLC单例对象 ///private static Plc plcObj; ////// 连接至PLC并返回连接状态 //////private bool ConnectToPLC() { try { plcObj.Open(); return plcObj.IsConnected ? true : false; } catch (Exception) { return false; } } ////// 关闭连接 ///private void Disconnect() { plcObj.Close(); } }
读写数据
S7NetPlus提供了多种读写的方式,可以读取字节自行解析或者按照指定格式写入字节,也可以指定地址进行读写,还可以使用变量、结构体或者类进行单个或者批量读写。
1、指定地址读写
这种方法可以在Read方法中以字符串形式传入需要读取的地址,返回的是Object类型的值,需要使用者自行做类型转换。Write方法则同理,以字符串的形式指定需要写入的地址,并在第二个参数传入需要写入的值,需要注意西门子PLC内的数据类型与C#的数据类型的对应。以下为读写DB10的0.0地址上的布尔量的值示例,此方式均支持读取与写入。
//读取bool result = (bool)plc.Read("DB10.DBX0.0");//写入plc.Write("DB10.DBX0.0",!result);
这种方式比较简单且方便,它是作者不推荐的方式,文档中原文如下:
This method reads a single variable from the plc, by parsing thestring and returning the correct result. While this is the easiestmethod to get started, is very inefficient because the driver sendsa TCP request for every variable.
意思就是,这种方法会通过解析传入的地址字符串来获取需要读写的地址,对于使用者来说是非常简单的使用方式,S7NetPlus会为每个通过这种方式读写的变量生成一个新的TCP请求,在读写多个变量时,执行效率会比较低。
S7NetPlus使用的通讯本质上是西门子的S7通讯,通过发送七层通讯报文来建立与西门子PLC的TCP连接,后续也是根据S7通讯的通讯协议生成并发送报文来实现PLC的数据读写。当使用这种方式读写多个变量的时候,S7NetPlus内部为每个变量重复建立新的S7连接与发送读写报文的操作,而不是单个连接成功建立后在这个连接上进行批量的读写。
简单理解就是这种方式效率比较低,会占用更多的资源。
2、解析读写
这种方法需要指定DB的类型、DB号、起始地址、PLC数据类型及读取数量。它需要传入的参数变多了,当需要读取多个地址连续且类型相同的变量时,仅需修改后的读取数量,S7NetPlus就会自动读取这一连串的地址,并按照指定的变量类型解析出对应的值,文档中后面说到的多类型变量批量读取也是基于这种方法的。这种方式读取PLC内的字符串类型时,仍存在bug,当需要读写字符串的时候,推荐使用本文后面提及的字节读写的方式。
示例如下:
//读取bool result = (bool)plc.Read(DataType.DataBlock, 10, 0, VarType.Bit, 1);//写入plc.Write(DataType.DataBlock, 10, 0, true);
Read:
个参数是DB的数据类型,可以是DB、定时器、计数器、Merker(内存)、输入、输出。
第二个参数是DB号。
第三个参数是起始地址。
第四个参数是PLC内该变量的类型。
第五个参数是需要读取的个数。
Write:
个参数是DB的数据类型,可以是DB、定时器、计数器、Merker(内存)、输入、输出。
第二个参数是DB号。
第三个参数是起始地址。
第四个参数是需要写入的值。
3、字节读写
这种方法将会读取指定DB块上一段连续的地址上的字节,不做任何解析直接以字节数组的形式返回。
个参数是DB的数据类型,可以是DB、定时器、计数器、Merker(内存)、输入、输出。
第二个参数是DB号。
第三个参数是起始地址。
第四个参数是读取的字节数。
要使用这种方式读写数据,则需要非常熟悉PLC内各类型数据存储的格式,可以自行将读取上来的字节进行解析以获得所需数据。
这种方式理论上能读写任意的数据,解析数据的过程会比较麻烦,若非万不得已,个人建议尽量少用。
此处仅提供PLC内String类型及WString类型的读取示例。
//String读取byte[] data = plc.ReadBytes(DataType.DataBlock, 10, 2, 254);string result = Encoding.Default.GetString(data);//Wstring读取byte[] data = plc.ReadBytes(DataType.DataBlock, 10, 4, 508);string result = Encoding.BigEndianUnicode.GetString(data);
在S7-1500中,一个String类型的变量占用256个字节,个字节是总字符数,第二个字节是当前字符数,真正的字符数据是从第三个字节开始的,共254个字节。
同理,WString类型其实就是双字节的Sring,也就是说一个字符占用两个字节,一个WString类型的变量占用512个字节,、二个字节是总字符数,第三、四个字节是当前字符数,真正的字符数据是从第五个字节开始的,共508个字节。
按照以上示例的方法,读取上来的字符串后面会带很多个"�"的字符,那是因为后面的空字节也读取上来了,正式使用时可以考虑使用.Replace("�","")来去除,或者解析第二个字节来获取字符长度进而转码。
当写入字符串时,则需要根据不同的数据类型来生成对应字符串的字节数组,将该数组写入到指定地址中即可。
需要注意的是,String类型的编码格式对应的是ASCII,而WString的则是C#中的BigEndianUnicode格式。在WString中,由于总长度与当前字符数是都是双字节数,在转换成字节数组的时候存在高低字节顺序问题。在这里就有一个大坑:这两个变量在C#中转换出来的字节数组跟PLC中存储的,高低字节是反过来的。这也就是为什么下面的WString的示例中需要对总字符数和当前字符数的两个字节数组进行反转。
此处提供一种生成String类型和WString的字节数组的方法,可供参考:
////// 获取西门子PLC字符串数组--String ////// ///private byte[] GetPLCStringByteArray(string str) { byte[] value = Encoding.Default.GetBytes(str); byte[] head = new byte[2]; head[0] = Convert.ToByte(254); head[1] = Convert.ToByte(str.Length); value = head.Concat(value).ToArray(); return value; } ////// 获取西门子PLC字符串数组--WString ////// ///private byte[] GetPLCWStringByteArray(string str) { byte[] value = Encoding.BigEndianUnicode.GetBytes(str); byte[] head = BitConverter.GetBytes((short)508); byte[] length = BitConverter.GetBytes((short)str.Length); Array.Reverse(head); Array.Reverse(length); head = head.Concat(length).ToArray(); value = head.Concat(value).ToArray(); return value; }
使用示例如下:
//写入String string str = "Example";plc.Write(DataType.DataBlock, 10, 0, GetPLCStringByteArray(str));//写入WStringstring str = "示例";plc.Write(DataType.DataBlock, 10, 0, GetPLCWStringByteArray(str));
4、旧版本的字节读取注意事项
旧版本的单次字节读取是有字节数限制的,每一次读取的大字节数为200,如果需要读写更多的字节,则需要多次读写并进行拼接,以下提供两种方法,可供参考:
////// 循环读取 ////// 要读取的字节数 /// DB号 /// 起始地址 ///private byte[] CyclicReadMultipleBytes(int numBytes, int db, int startByteAdr = 0) { byte[] resultBytes = new byte[0]; int index = startByteAdr; while (numBytes > 0) { var maxToRead = Math.Min(numBytes, 200); byte[] bytes = plc.ReadBytes(DataType.DataBlock, db, index, maxToRead); if (bytes == null) return null; resultBytes = resultBytes.Concat(bytes).ToArray(); numBytes -= maxToRead; index += maxToRead; } return resultBytes; } ////// 递归读取 ////// 要读取的字节数 /// DB号 /// 起始地址 ///public static byte[] RecursiveReadMultipleBytes(int numBytes, int db, int startByteAdr = 0) { byte[] result = new byte[0]; if (numBytes > 200) { byte[] temp = plc.ReadBytes(DataType.DataBlock, db, startByteAdr, 200); numBytes -= 200; result = temp.Concat(RecursiveReadMultipleBytes(numBytes, db, startByteAdr + 200)).ToArray(); } else { byte[] temp = plc.ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes); result = result.Concat(temp).ToArray(); return result; } return result; }
在读取一两千个字节的情况下,这两种方法速度都差不多,递归会稍微快一点点。新版本没有单次读取限制,正常情况下是不需要这两个方法的。
5、其余读取方式
其它的读取方式可参考文档,本文不再赘述。
读取数据示例
PLCInstance:
using S7.Net;using System;using System.Text;namespace S7NetPlusExample{ class PLCInstance { private PLCInstance() { plcObj = new Plc(CpuType.S71500, "192.168.10.230", 0, 1); } ////// PLC单例 ///public static PLCInstance Instance { get { return Nested.instance; } } ////// 防止调用此类静态方法时,创建新的实例 ///private class Nested { internal static readonly PLCInstance instance = null; static Nested() { instance = new PLCInstance(); } } ////// 私有PLC单例对象 ///private static Plc plcObj; ////// 连接至PLC并返回连接状态 //////private bool ConnectToPLC() { try { plcObj.Open(); return plcObj.IsConnected ? true : false; } catch (Exception) { return false; } } ////// 关闭连接 ///private void Disconnect() { plcObj.Close(); } ////// 读取示例数据 //////public string GetPLCInfo() { if (ConnectToPLC()) { StringBuilder sbr = new StringBuilder(); //读取BOOL值 bool boolResult = (bool)plcObj.Read(DataType.DataBlock, 10, 0, VarType.Bit, 1); //读取Int值 int intResult = (short)plcObj.Read(DataType.DataBlock, 10, 2, VarType.Int, 1); //读取Real值 float realResult = (float)plcObj.Read(DataType.DataBlock, 10, 4, VarType.Real, 1); //读取String值 byte[] stringData = plcObj.ReadBytes(DataType.DataBlock, 10, 10, 254); string stringResult = Encoding.Default.GetString(stringData); //读取WString byte[] wstringData = plcObj.ReadBytes(DataType.DataBlock, 10, 268, 508); string wstringResult = Encoding.BigEndianUnicode.GetString(wstringData); Disconnect(); sbr.AppendLine($"{boolResult}"); sbr.AppendLine($"{intResult}"); sbr.AppendLine($"{realResult}"); sbr.AppendLine($"{stringResult}"); sbr.AppendLine($"{wstringResult}"); return sbr.ToString(); } else { return "连接PLC失败"; } } }}
主程序:
using System;namespace S7NetPlusExample{ class Program { static void Main(string[] args) { Console.WriteLine(PLCInstance.Instance.GetPLCInfo()); Console.ReadKey(); } }}
运行结果:
结尾
本文简单介绍了S7 Net Plus和PLCSIMAdvanced的使用,以上内容均由本人亲自实践得出的结果,但仍有可改进的的地方。S7NetPlus的文档也有非常详细的介绍,如有更复杂的读写需求,可以参考文档。