PLCSIM Advanced 简介
PLCSIM Advanced是西门子推出的一款功能强大的仿真软件,目前最新发布的版本为4.0,但鉴于新版本可能存在未知的bug,故本文使用V3.0。
V3.0支持仿真1500PLC及ET 200SP,可实现Socket网络通讯功能,也可实现PLC之间、PLC与设备直接的ModbusTCP等通讯。
V3.0安装时需要先安装WinPcap_4_1_3,V4.0则不需要。
以下为V3.0下载链接:
以下为V4.0下载链接
S7 Net Plus 简介
西门子PLC通讯库,支持200、200smart、300、400、1200、1500系列PLC。
配置PLCSIM Advanced
打开PLCSIM Advanced V3.0,如下图:
Online Access要选择右边的PLCSIM Virtual Eth.Adapter,左侧的PLCSIM不支持外部网络访问。
TCP/IP communication with 可选以太网或者是本地虚拟网卡。local即为本地虚拟网卡,是在安装PLCSIM Advanced时自动安装的网络适配器。打开控制面板-->网络和 Internet-->网络连接,Siemens PLCSIM Virtual Ethernet Adapter就是此虚拟网卡。使用虚拟网卡只能在本机进行通讯仿真,而使用以太网则可以在局域网内进行仿真通讯。
Start Virtual S7-1500 PLC为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);
}
/// <summary>
/// PLC单例
/// </summary>
public static PLCInstance Instance
{
get
{
return Nested.instance;
}
}
/// <summary>
/// 防止调用此类静态方法时,创建新的实例
/// </summary>
private class Nested
{
internal static readonly PLCInstance instance = null;
static Nested()
{
instance = new PLCInstance();
}
}
/// <summary>
/// 私有PLC单例对象
/// </summary>
private static Plc plcObj;
/// <summary>
/// 连接至PLC并返回连接状态
/// </summary>
/// <returns></returns>
private bool ConnectToPLC()
{
try
{
plcObj.Open();
return plcObj.IsConnected ? true : false;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 关闭连接
/// </summary>
private void Disconnect()
{
plcObj.Close();
}
}
读写数据
S7NetPlus提供了多种读写的方式,可以读取字节自行解析或者按照指定格式写入字节,也可以指定地址进行读写,还可以使用变量、结构体或者类进行单个或者批量读写。
指定地址读写
以下为读写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 the string and returning the correct result. While this is the easiest method to get started, is very inefficient because the driver sends a TCP request for every variable.
简单理解就是这种方式效率比较低,会占用更多的资源。
解析读写
当需要读取多个地址连续且类型相同的变量时,这种方式是最好的。但是这种方式读取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号。
第三个参数是起始地址。
第四个参数是需要写入的值
字节读写
使用这种方式读写数据,需要非常熟悉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个字节。
按照以上示例的方法,读取上来的字符串后面会带很多个"\0"的字符,那是因为后面的空字节也读取上来了,正式使用时可以考虑使用.Replace("\0", "")来去除,或者解析第二个字节来获取字符长度进而转码。
当写入字符串时,则需要根据不同的数据类型来生成对应字符串的字节数组,然后将该数组写入到指定地址中即可。
此处提供一种生成String类型和WString的字节数组的方法,可供参考:
/// <summary>
/// 获取西门子PLC字符串数组--String
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 获取西门子PLC字符串数组--WString
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
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));
//写入WString
string str = "示例";
plc.Write(DataType.DataBlock, 10, 0, GetPLCWStringByteArray(str));
旧版本的字节读取注意事项:
旧版本的单次字节读取是有字节数限制的,每一次读取的最大字节数为200,如果需要读写更多的字节,则需要多次读写并进行拼接,以下提供两种方法,可供参考:
/// <summary>
/// 循环读取
/// </summary>
/// <param name="numBytes">要读取的字节数</param>
/// <param name="db">DB号</param>
/// <param name="startByteAdr">起始地址</param>
/// <returns></returns>
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;
}
/// <summary>
/// 递归读取
/// </summary>
/// <param name="numBytes">要读取的字节数</param>
/// <param name="db">DB号</param>
/// <param name="startByteAdr">起始地址</param>
/// <returns></returns>
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;
}
其余读取方式
其它的读取方式可参考文档,本文不再赘述。
读取数据示例
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);
}
/// <summary>
/// PLC单例
/// </summary>
public static PLCInstance Instance
{
get
{
return Nested.instance;
}
}
/// <summary>
/// 防止调用此类静态方法时,创建新的实例
/// </summary>
private class Nested
{
internal static readonly PLCInstance instance = null;
static Nested()
{
instance = new PLCInstance();
}
}
/// <summary>
/// 私有PLC单例对象
/// </summary>
private static Plc plcObj;
/// <summary>
/// 连接至PLC并返回连接状态
/// </summary>
/// <returns></returns>
private bool ConnectToPLC()
{
try
{
plcObj.Open();
return plcObj.IsConnected ? true : false;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 关闭连接
/// </summary>
private void Disconnect()
{
plcObj.Close();
}
/// <summary>
/// 读取示例数据
/// </summary>
/// <returns></returns>
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和PLCSIM Advanced的使用,以上内容均由本人亲自实践得出的结果,但仍有可改进的的地方。S7NetPlus的文档也有非常详细的介绍,如有更复杂的读写需求,可以参考文档。