[西门子] PLC遇见IT:C#+S7Net+PLCSIM实现西门子PLC仿真通讯

[复制链接]
查看124 | 回复0 | 2024-4-16 08:02:26 | 显示全部楼层 |阅读模式
↑ 点击上方
“智能制造之家”
关注我们




会员可进入会员下载专区获取相关资料,非会员请按文末规则获取~

写在前面
前面我们已经分享了很多西门子PLC的干货文章与资料:
西门子S7-1500控制9台V90伺服 | 附源代码下载
基于S7-1500的AGV与MES数据集成的智能仓储系统设计与实施

使用S7-1200和1500 轻松连接物联网MQTT 消息服务器
OT遇见IT—西门子S7-1200/1500官方库直连SQL Sever

TIAV17+S7-1200:解析最新西门子S7CommPlus协议
S7-1200+SCADA:详解西门子S7协议与数据读写
也分享了很多SCADA相关的干货:
从PLC、SCADA到IIoT:讲透工业数据采集!

XX数字化工厂SCADA与工业网络建设规划方案

XX汽车焊装智能工厂控制系统与信息系统(PLC+SCADA+MES)规划设计

数字化工厂SCADA组成、网络结构及系统典型架构解析

XX集团工厂集成改造MES+SCADA数字化项目实施方案

今天分享当PLC遇见IT:C#+S7Net+PLCSIM实现西门子PLC仿真通讯~

文章转载自:CSDN 虚梦年华
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下载链接:
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 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提供了多种读写的方式,可以读取字节自行解析或者按照指定格式写入字节,也可以指定地址进行读写,还可以使用变量、结构体或者类进行单个或者批量读写。
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 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.
意思就是,这种方法会通过解析传入的地址字符串来获取需要读写的地址,对于使用者来说是非常简单的使用方式,但是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个字节。
按照以上示例的方法,读取上来的字符串后面会带很多个"\0"的字符,那是因为后面的空字节也读取上来了,正式使用时可以考虑使用.Replace("\0", "")来去除,或者解析第二个字节来获取字符长度进而转码。
当写入字符串时,则需要根据不同的数据类型来生成对应字符串的字节数组,然后将该数组写入到指定地址中即可。
需要注意的是,String类型的编码格式对应的是ASCII,而WString的则是C#中的BigEndianUnicode格式。在WString中,由于总长度与当前字符数是都是双字节数,所以在转换成字节数组的时候存在高低字节顺序问题。在这里就有一个大坑:这两个变量在C#中转换出来的字节数组跟PLC中存储的,高低字节是反过来的。这也就是为什么下面的WString的示例中需要对总字符数和当前字符数的两个字节数组进行反转。
此处提供一种生成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));




//写入WStringstring str = "示例";plc.Write(DataType.DataBlock, 10, 0, GetPLCWStringByteArray(str));4、旧版本的字节读取注意事项

旧版本的单次字节读取是有字节数限制的,每一次读取的最大字节数为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;        }在读取一两千个字节的情况下,这两种方法速度都差不多,递归会稍微快一点点。不过新版本没有单次读取限制,所以正常情况下是不需要这两个方法的。
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);        }
        /// <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的文档也有非常详细的介绍,如有更复杂的读写需求,可以参考文档。
会员可进入会员下载专区获取相关资料~


硬核专辑


ERP/PLM/MES/SCADA/PLC/工业软件研究报告




信息化、数字化、智能制造、工业互联网解决方案

自动化、信息化、数字化、工业网络、仿真与虚拟调试入门

WinCC技术 | 工业网络 | MES技术相关| 工业巨头战略布局 | 工业通讯案例
仿真与虚拟调试 | 职业感悟、认知提升 | 自动化控制标准合集


XX集团工厂集成改造MES+SCADA数字化项目实施方案
2021-06-21
西门子、施耐德、三菱、汇川、中控:2021年中国工控自动化行业纵览
2021-06-19
391页物联网产业链全梳理:详述全球物联网平台厂商与四大层级
2021-06-24
C#与WinCC通讯之西门子S7-1200数据读取详解
2021-06-23
中国工业机器人行业产业链深度研究报告
2021-06-28
工业软件报告之CAX(CAD/CAE/CAM)篇:全球主流玩家与产品格局
2021-06-22
某外资工业软件巨头三维数字化工厂完整解决方案
2021-06-15
企业信息化MES/MOM技术规范(设计、架构、信息流与行业参考)
2021-06-16
某自动化、信息化各大系统整体解决方案与案例
2021-06-20
102页阿里巴巴数字智能工厂完整解决方案
2021-06-07
系统解读数字化工厂的五大核心系统(PLM\ERP\MES\WMS\DCS)
2021-06-03
中国工业互联网Top20城市榜单及产业链结构解析
2021-06-08


欢迎关注"智能制造之家
免责申明:本公众号所载文章为本公众号原创或根据网络搜索编辑整理,文章版权归原作者所有。因转载众多,无法找到真正来源,如标错来源,或对于文中所使用的图片,资料,下载链接中所包含的软件,资料等,如有侵权,请联系删除~




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册哦

x
您需要登录后才可以回帖 登录 | 注册哦

本版积分规则