C#与西门子PLC通讯——熟手快速入门

本文档详细介绍了如何使用C#与西门子PLC进行通讯,包括基础数据类型对应、读写操作、自定义数据类型处理,以及WinForm项目示例。读者将学习到如何处理数据类型、自定义数据类型的读写,以及在WinForm项目中实现PLC的连接和交互功能。
摘要由CSDN通过智能技术生成

提示1:参照本文,你可以快速搭建一个通讯交互实例,并完成一个项目演示用例。 提示2:如果你第一次来,请跳转到C#与西门子PLC通讯——新手快速入门了解背景信息。

关键词1:C#,.Net Core,S7 Net Plus,TIA Portal V17,PLCSIM Advanced V4,S7-1500。 关键词2:数据类型对照,DBX,DBB,DBW,DBD,面向对象编程,WinForm程序。

代码已同步至: Gitee:https://gitee.com/lukailin/sim-s71500 Github:https://github.com/Millance/SimS71500

https://i-blog.csdnimg.cn/blog_migrate/ffd323518bedd5b8d5571e6416e275b6.png#pic_center

文章目录

前言

翌日,斯电气之士大喜,言已成通讯之试,访吾欲构一物。余默思片刻,书此以为之。

本文基于C# .Net Core和西门子博图TIA Portal V17搭建。由于手边没有西门子PLC实物,所以采用S7-PLCSIM Advanced V4.0作为模拟PLC,以实现0成本完成通讯测试实例。 在实际通讯中,往往需要先确定地址,数据类型和读写规则。因此本文将侧重分析数据类型的读写,以及处理读写过程中容易出现的问题,并且扩展了在交互过程中遇到陌生数据类型的处理方式。 最后本文以一个桌面小程序抛砖引玉,重点实现了熟手需要学习的面向对象编程、设计模式和界面设计。

一、PLC与C# 基础数据类型

1.1 数据类型对照表

这个对应关系主要取决于PLC与C#之间进行数据交换或通信时,确保数据的一致性和正确性。因此需要定义一种映射关系,以便在两个系统之间传递数据时能够正确地解释和处理数据。 常用的对照关系如下:
PLC 数据类型C# 数据类型字节数Boolbool1/8Bytebyte1Charchar1Intshort2Wordushort2DIntint4DWorduint4Realfloat4LIntlong8LRealdouble8LWordulong8Stringstring256Array[0…n] of TypeType[n]n × \times × Type

不同数据类型在内存中占据不同的字节数。为了确保数据在两个系统之间传递时不会出现字节对齐、数据截断或者正负符号等问题,需要定义字节数对应关系。例如,一个PLC的Int类型在C#中被映射为short,因为它们都占据2个字节的内存空间。 Array[0…n] of Type中,需要根据Type的实际类型和数组长度n进行计算。 另外,其他的数据类型对照可以从字节数和有无符号的角度进行思考,字节数接近的可以进行尝试。

详细的PLC数据类型请参考西门子的在线帮助文档:基本数据类型以及char 和 string 的定义等。 或博图自带的帮助文档:
https://i-blog.csdnimg.cn/blog_migrate/6d1296b40d785d9aa601bc80654d5338.png

1.2 C# 读写PLC数据

1.2.1 Plc.Read方法源码浅析

先来扒一下S7 Net Plus源码。 调用plc.Read("DB1.DBX0.0")方法,会进行入下面的源码。

/// <summary>
/// 从PLC读取单个变量,接受输入字符串如"DB1.DBX0.0","DB20.DBD200","MB20","T45"等。
/// 如果读取不成功,请检查LastErrorCode或LastErrorString。
/// </summary>
/// <param name="variable">输入字符串如"DB1.DBX0.0","DB20.DBD200","MB20","T45"等。</param>
/// <returns>返回包含值的对象。必须根据需要对该对象进行类型转换。如果没有读取到数据,将返回null。</returns>
/// </summary>
public object? Read(string variable)
{
   
    var adr = new PLCAddress(variable);
    return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
}

其中PLCAddress方法的代码如下:

namespace S7.Net
{
   
    internal class PLCAddress
    {
   
        ...

        public PLCAddress(string address)
        {
   
            Parse(address, out dataType, out dbNumber, out varType, out startByte, out bitNumber);
        }

        public static void Parse(string input, out DataType dataType, out int dbNumber, out VarType varType, out int address, out int bitNumber)
        {
   
            ...

            switch (input.Substring(0, 2))
            {
   
                case "DB":
                    string[] strings = input.Split(new char[] {
    '.' });
                    if (strings.Length < 2)
                        throw new InvalidAddressException("To few periods for DB address");

                    dataType = DataType.DataBlock;
                    dbNumber = int.Parse(strings[0].Substring(2));
                    address = int.Parse(strings[1].Substring(3));

                    string dbType = strings[1].Substring(0, 3);
                    switch (dbType)
                    {
   
                        case "DBB":
                            varType = VarType.Byte;
                            return;
                        case "DBW":
                            varType = VarType.Word;
                            return;
                        case "DBD":
                            varType = VarType.DWord;
                            return;
                        case "DBX":
                            bitNumber = int.Parse(strings[2]);
                            if (bitNumber > 7)
                                throw new InvalidAddressException("Bit can only be 0-7");
                            varType = VarType.Bit;
                            return;
                        default:
                            throw new InvalidAddressException();
                    }
              ...
            }
        }
    }
}

从PLCAddress的Parse方法可以看到,类似plc.Read("DB1.DBX0.0")plc.Read("DB1.DBW2")这些读取指定地址的方法只能准确支持特定数据类型,如:Bit(DBX)、Byte(DBB)、Word(DBW)、DWord(DBD)。当数据类型的长度相同时,也可以支持相同长度的其他数据类型,但无法满足所有可能的情况。

Read方法还可以重载为:

/// <summary>
/// 读取并解码提供的“VarType”指定字节数的数据。
/// 可用于读取同一类型(如Word、DWord、Int等)的多个连续变量。
/// 如果读取不成功,请检查LastErrorCode或LastErrorString。
/// </summary>
/// <param name="dataType">存储区域的数据类型,可以是DB、Timer、Counter、Merker(内存)、Input、Output。</param>
/// <param name="db">存储区域的地址(如果要读取DB1,设置为1)。对于其他存储区域类型(计数器、定时器等),也必须设置此参数。</param>
/// <param name="startByteAdr">起始字节地址。如果要读取DB1.DBW200,设置为200。</param>
/// <param name="varType">要读取的变量类型</param>
/// <param name="bitAdr">比特地址。如果要读取DB1.DBX200.6,将此参数设置为6。</param>
/// <param name="varCount">变量的数量</param>
/// </summary>
/// <returns>返回包含值的对象。必须根据需要对该对象进行类型转换。</returns>
public object? Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
{
   
    int cntBytes = VarTypeToByteLength(varType, varCount);
    byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);

    return ParseBytes(varType, bytes, varCount, bitAdr);
}

同时,在public object? Read(string variable)中也可以看到,经过PLCAddress解析之后,也是调用的这个重载的Read方法。方法中的VarType可以选择众多类型,请自行探索。因此,一些无法用DBX、DBB、DBW和DBD进行读写的数据类型,可以用重载方法进行读写。

1.2.2 PLC 中增加数据类型

在博图软件中增加一些测试数据类型,如下:
https://i-blog.csdnimg.cn/blog_migrate/7380712cd9443087e495a35ccaa4278f.png

配置信息拷贝出来,方便参考:
名称数据类型偏移量起始值监视值布尔量Bool0.0falseFALSE整形量Int2.000数组字Array[0…9] of Word4.0读写BoolBool24.0falseTRUE读写ByteByte25.016#016#01读写CharChar26.0’ ’‘a’读写IntInt28.003读写WordWord30.016#016#0004读写DIntDInt32.005读写DWordDWord36.016#016#0000_0006读写RealReal40.00.07.7读写LIntLInt44.008读写LRealLReal52.00.09.9读写LWordLWord60.016#016#0000_0000_0000_0010读写StringString68.0‘’‘你好!Hello PLC!’

这一步不会的,请跳转到C#与西门子PLC通讯——新手快速入门

1.2.3 C# 读取PLC中不同类型的数据

我们继续改造新手入门的演示程序。

using S7.Net;
using System.Text;

namespace SimS71500
{
   
    internal class Program
    {
   
        static void Main(string[] args)
        {
   
            // 解决:“'GBK' is not a supported encoding name.”的方法
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

            Plc plc = new Plc(CpuType.S71500, "192.168.0.100", 0, 1);
            plc.Open();

            // 接收键入的值
            string inputKey = "";

            //存储区域的地址
            int dbArea = 1;

            Task readPLCTask = Task.Factory.StartNew(() =>
            {
   
                while (plc.IsConnected && inputKey != "q")
                {
   
                    Console.Clear();
                    // plc.Read 参数分别为数据块类型,数据块,偏移量,读取类型,读取长度
                    // 布尔量
                    Console.WriteLine("布尔量\t" + plc.Read(DataType.DataBlock, dbArea, 0, VarType.Bit, 1));
                    // 整形量
                    Console.WriteLine("整形量\t" + plc.Read(DataType.DataBlock, dbArea, 2, VarType.Int, 1));
                    // 数组字中的第一个元素
                    Console.WriteLine("数组字中的第一个元素\t" + plc.Read(DataType.DataBlock, 1, 4, VarType.Word, 1));
                    // 数组字中的剩余元素
                    short[] remainArr = (short[])plc.Read(DataType.DataBlock, 1, 6, VarType.Word, 9);
                    Console.Write("数组字中的剩余元素\t");
                    for (int i = 0; i < remainArr.Length; i++)
                    {
   
                        Console.Write(remainArr[i] + "\t");
                    }
                    Console.WriteLine();
                    Console.WriteLine("**************************************************************************************************");
                    // 读取Bool
                    Console.WriteLine("读取Bool\t" + plc.Read(DataType.DataBlock, dbArea, 24, VarType.Bit, 1));
                    // 读取Byte
                    Console.WriteLine("读取Byte\t" + plc.Read(DataType.DataBlock, dbArea, 25, VarType.Byte, 1));
                    // 读取Char
                    Console.WriteLine("读取Char\t" + plc.Read(DataType.DataBlock, dbArea, 26, VarType.String, 1));
                    // 读取Int
                    Console.WriteLine("读取Int \t" + plc.Read(DataType.DataBlock, dbArea, 28, VarType.Int, 1));
                    // 读取Word
                    Console.WriteLine("读取Word\t" + plc.Read(DataType.DataBlock, dbArea, 30, VarType.Word, 1));
                    // 读取DInt
                    Console.WriteLine("读取DInt\t" + plc.Read(DataType.DataBlock, dbArea, 32, VarType.DInt, 1));
                    // 读取DWord
                    Console.WriteLine("读取DWord\t" + plc.Read(DataType.DataBlock, dbArea, 36, VarType.DWord, 1));
                    // 读取Real
                    Console.WriteLine("读取Real\t" + plc.Read(DataType.DataBlock, dbArea, 40, VarType.Real, 1));
                    // 读取LInt
                    byte[] dataLInt = plc.ReadBytes(DataType.DataBlock, dbArea, 44, 8);
                    if (BitConverter.IsLittleEndian)
                    {
   
                        Array.Reverse(dataLInt); // 如果系统是小端序(Little Endian),需要反转字节数组
                    }
                    Console.WriteLine("读取LInt\t" + BitConverter.ToInt64(dataLInt, 0));
                    // 读取LReal
                    Console.WriteLine("读取LReal\t" + plc.Read(DataType.DataBlock, dbArea, 52, VarType.LReal, 1));
                    // 读取LWord
                    byte[] dataLWord = plc.ReadBytes(DataType.DataBlock, dbArea, 60, 8);
                    if (BitConverter.IsLittleEndian)
                    {
   
                        Array.Reverse(dataLWord); // 如果系统是小端序(Little Endian),需要反转字节数组
                    }
                    Console.WriteLine("读取LWord\t" + BitConverter.ToInt64(dataLWord, 0));
                    // 读取String
                    byte[] dataS = plc.ReadBytes(DataType.DataBlock, dbArea, 68, 256);
                    int stringLen = dataS[1];
                    string gbkString = Encoding.GetEncoding("GBK").GetString(dataS, 2, stringLen);
                    Console.WriteLine("读取String\t" + gbkString);

                    Task.Delay(200).Wait();
                }
            });

            inputKey = Console.ReadLine();

            plc.Close();
            Task.WaitAll(readPLCTask);
        }
    }
}


下面是读取到的效果:
https://i-blog.csdnimg.cn/blog_migrate/f032980e77d7fd40d2d41725ae50e4e9.png

1.2.4 读取的代码解析
1.2.4.1 基本数据类型的读取方法
//存储区域的地址
int dbArea = 1;
plc.Read(DataType.DataBlock, dbArea, 0, VarType.Bit, 1);

其中,dbArea表示读取的数据块编号,即plc.Read("DB1.DBX0.0")中的DB11VarType.Bit表示读取类型为Bit。 最后的1表示读取1个类型为VarType.Bit对应的长度。在比如:下面的9表示读取9个VarType.Word

plc.Read(DataType.DataBlock, 1, 6, VarType.Word, 9);

1.2.4.2 大端存储和小端存储的问题
// 读取LInt
byte[] dataLInt = plc.ReadBytes(DataType.DataBlock, dbArea, 44, 8);
if (BitConverter.IsLittleEndian)
{
   
    Array.Reverse(dataLInt); // 如果系统是小端序(Little Endian),需要反转字节数组
}
Console.WriteLine("读取LInt\t" + BitConverter.ToInt64(dataLInt, 0));
// 读取LWord
byte[] dataLWord = plc.ReadBytes(DataType.DataBlock, dbArea, 60, 8);
if (BitConverter.IsLittleEndian)
{
   
    Array.Reverse(dataLWord); // 如果系统是小端序(Little Endian),需要反转字节数组
}
Console.WriteLine("读取LWord\t" + BitConverter.ToInt64(dataLWord, 0));

读取LInt和读取LWord比较特殊,VarType中没有对应的类型,因此需要手写byte[]转换方法。 同时,由于PLC采用大端存储,但是上位机一般采用小端存储,因此还需要反转一下byte数组。

1.2.4.3 C# 读取中文乱码的问题
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

解决:“System.ArgumentException:“‘GBK’ is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. Arg_ParamName_Name”” 的方法。 在Encoding.GetEncoding("GBK").GetString()前任意位置执行即可。 它允许注册和使用其他字符编码提供程序,以便支持其他字符编码,例如 “GBK” 或 “GB2312”,这些编码不是.NET默认支持的。 一旦注册了字符编码提供程序,程序就可以使用所需的编码,例如 “GBK”,而不会遇到编码不支持的问题。这对于处理非标准字符编码的数据非常有用。

byte[] dataS = plc.ReadBytes(DataType.DataBlock, dbArea, 68, 256);
int stringLen = dataS[1];
string gbkString = Encoding.GetEncoding("GBK").GetString(dataS, 2, stringLen);
Console.WriteLine("读取String\t" + gbkString);

其中,GetString(dataS, 2, stringLen)忽略前两个无法识别的字节。如果不使用2, stringLen这两个参数,则会在字符串前显示一个“?”。
https://i-blog.csdnimg.cn/blog_migrate/f694ccbd743b02b093fa5c8aff8a392f.png
具体解释参考西门子官方文档中关于string 在西门子 PLC 中的格式的解析。
https://i-blog.csdnimg.cn/blog_migrate/4598a7d54a7a397cb321e4547616c1bc.png
在我们的案例中,获取的byte数组为: {254, 16, 196, 227, 186, 195, 163, 161, 72, 101, 108, 108, 111, 32, 80, 76, 67, 33, 0, 0, 0, 0, 0,…} 其中254表示String的长度,16表示字符数量(中文表示两个字符)。这两个字节是非文本数据或称为控制信息,而不是有效的字符数据。 因此,可以简化为直接从有效字符开始的偏移量读取中文,前提是要知道自己在干什么:

// 读取String
Console.WriteLine("读取String\t" + Encoding.GetEncoding("GBK").GetString(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值