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

提示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://img-blog.csdnimg.cn/7f7e2d69dadc4271b49064464367a980.png?x-oss-process=image/resize,w_800#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://img-blog.csdnimg.cn/f02ba39b7f1043c08c55aca1483c459e.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://img-blog.csdnimg.cn/90c3c136ce374579910ce75bee5ea991.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://img-blog.csdnimg.cn/51f06bfd01ba4942afd89ad0913fb251.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://img-blog.csdnimg.cn/1082fa904dd6436ebfb93d14879d2a57.png
具体解释参考西门子官方文档中关于string 在西门子 PLC 中的格式的解析。
https://img-blog.csdnimg.cn/aa258cd873b54a648f78f08a6ee6d6d6.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(plc.ReadBytes(DataType.DataBlock, dbArea, 70, 254)));

1.2.5 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 = "";
            bool boolFlag = false;
            short iCount = 1;

            //存储区域的地址
            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();
                }
            });

            Random random = new Random();
            Task writePLCTask = Task.Factory.StartNew(() =>
            {
                while (plc.IsConnected && inputKey != "q")
                {

                    // 布尔量
                    plc.Write(DataType.DataBlock, dbArea, 0, boolFlag);//false表示写入的值
                                                                       // 整形量
                    plc.Write(DataType.DataBlock, dbArea, 2, iCount);
                    // 数组字中的第一个元素
                    plc.Write(DataType.DataBlock, dbArea, 4, (short)(iCount * 2));
                    // 数组字中的剩余元素
                    short[] arrValues = new short[9]; // 你要写入的新值数组
                    for (int i = 0; i < arrValues.Length; i++)
                    {
                        arrValues[i] = (short)(iCount * 2 + 1 + i);
                    }
                    plc.Write(DataType.DataBlock, dbArea, 6, arrValues);

                    // 写入Bool
                    plc.Write(DataType.DataBlock, dbArea, 24, boolFlag);
                    // 写入Byte
                    plc.Write(DataType.DataBlock, dbArea, 25, (byte)(iCount + 2));
                    // 写入Char
                    plc.Write(DataType.DataBlock, dbArea, 26, ((char)random.Next(97, 123)).ToString());
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 28, (short)(iCount + 4));
                    // 写入Word
                    plc.Write(DataType.DataBlock, dbArea, 30, (ushort)(iCount + 5));
                    // 写入DInt
                    plc.Write(DataType.DataBlock, dbArea, 32, iCount + 6);
                    // 写入DWord
                    plc.Write(DataType.DataBlock, dbArea, 36, iCount + 7);
                    // 写入Real
                    plc.Write(DataType.DataBlock, dbArea, 40, (float)(random.NextDouble() * 10));
                    // 写入LInt
                    // 生成两32位整数并合并为一个long
                    long randomLInt = ((long)random.Next(int.MinValue, int.MaxValue) << 32) | (uint)random.Next(int.MinValue, int.MaxValue);
                    byte[] dataLInt = BitConverter.GetBytes(randomLInt);
                    if (BitConverter.IsLittleEndian)
                    {
                        Array.Reverse(dataLInt); // 如果系统是小端序(Little Endian),需要反转字节数组
                    }
                    plc.WriteBytes(DataType.DataBlock, dbArea, 44, dataLInt);
                    // 写入LReal
                    plc.Write(DataType.DataBlock, dbArea, 52, random.NextDouble() * 100);
                    // 写入LWord
                    // 生成两32位整数并合并为一个long
                    long randomLWord = ((long)random.Next(int.MinValue, int.MaxValue) << 32) | (uint)random.Next(int.MinValue, int.MaxValue);
                    byte[] dataLWord = BitConverter.GetBytes(randomLWord);
                    if (BitConverter.IsLittleEndian)
                    {
                        Array.Reverse(dataLWord); // 如果系统是小端序(Little Endian),需要反转字节数组
                    }
                    plc.WriteBytes(DataType.DataBlock, dbArea, 60, dataLWord);
                    // 写入String
                    string chineseString = boolFlag ? "你好!Hello PLC!" : "Hello PLC!";
                    // 编码为字节数组
                    byte[] chineseBytes = Encoding.GetEncoding("GBK").GetBytes(chineseString);
                    // 构建带控制信息的字节数组
                    byte[] dataString = new byte[chineseBytes.Length + 2]; // 加2是为了存储控制信息
                    // 添加控制信息
                    dataString[0] = 254; // 第一个字节固定为254
                    dataString[1] = (byte)(chineseBytes.Length); // 第二个字节表示字符长度
                    // 复制字符串数据到字节数组
                    Array.Copy(chineseBytes, 0, dataString, 2, chineseBytes.Length);
                    // 将字节数组写入PLC
                    plc.WriteBytes(DataType.DataBlock, dbArea, 68, dataString);

                    iCount++;
                    boolFlag = !boolFlag;
                    Task.Delay(200).Wait();
                }
            });

            inputKey = Console.ReadLine();

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

https://img-blog.csdnimg.cn/06cc485b42764acd937750c6ea29684e.png

1.2.6 写入的代码解析

其中字符串的处理较为麻烦,需要在字符串字节数组前增加两个控制信息。

// 写入String
string chineseString = boolFlag ? "你好!Hello PLC!" : "Hello PLC!";
// 编码为字节数组
byte[] chineseBytes = Encoding.GetEncoding("GBK").GetBytes(chineseString);
// 构建带控制信息的字节数组
byte[] dataString = new byte[chineseBytes.Length + 2]; // 加2是为了存储控制信息
// 添加控制信息
dataString[0] = 254; // 第一个字节固定为254
dataString[1] = (byte)(chineseBytes.Length); // 第二个字节表示字符长度
// 复制字符串数据到字节数组
Array.Copy(chineseBytes, 0, dataString, 2, chineseBytes.Length);
// 将字节数组写入PLC
plc.WriteBytes(DataType.DataBlock, dbArea, 68, dataString);

二、自定义数据类型

2.1 自定义数据类型的创建

一些厉害的电气工程师还会增加自定义的数据类型,以下是随意写的示例,不当之处欢迎批评。
https://img-blog.csdnimg.cn/d4c05b3212824e75a3f23503615e2830.png

https://img-blog.csdnimg.cn/3a95adb2d4ab48e3b527a6e9b25440a5.png

将两个自定义的数据类型加入到新的DB块中。
https://img-blog.csdnimg.cn/1cdd4e0561a3403994a5a306456ad4ca.png

配置信息拷贝出来,方便参考:
名称数据类型偏移量起始值电机001电机0任务号DWord016#0任务状态Int40当前状态Int60电机正转Bool8FALSE电机反转Bool8.1FALSE故障码Int100一维移动设备001一维移动设备12任务号DWord1216#0任务状态Int160任务起始位置DWord1816#0任务终止位置DWord2216#0当前状态Int260当前坐标Int280故障码Int300

2.2 C#读写自定义数据类型

如果看懂了第一章的内容,这个地方看似简单,实则不难,直接贴代码:

using S7.Net;

namespace SimS71500
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Plc plc = new Plc(CpuType.S71500, "192.168.0.100", 0, 1);
            plc.Open();

            // 接收键入的值
            string inputKey = "";
            bool boolFlag = false;
            short iCount = 1;

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

            Task readPLCTask = Task.Factory.StartNew(() =>
            {
                while (plc.IsConnected && inputKey != "q")
                {
                    Console.Clear();
                    // plc.Read 参数分别为数据块类型,数据块,偏移量,读取类型,读取长度
                    Console.WriteLine("*********************电机001*********************");
                    // 读取DWord
                    Console.WriteLine("电机001-任务号  \t" + plc.Read(DataType.DataBlock, dbArea, 0, VarType.DWord, 1));
                    // 读取Int
                    Console.WriteLine("电机001-任务状态\t" + plc.Read(DataType.DataBlock, dbArea, 4, VarType.Int, 1));
                    // 读取Int
                    Console.WriteLine("电机001-当前状态\t" + plc.Read(DataType.DataBlock, dbArea, 6, VarType.Int, 1));
                    // 读取Bool
                    Console.WriteLine("电机001-电机正转\t" + plc.Read(DataType.DataBlock, dbArea, 8, VarType.Bit, 1));
                    // 读取Bool
                    Console.WriteLine("电机001-电机反转\t" + plc.Read(DataType.DataBlock, dbArea, 8, VarType.Bit, 1, 1));
                    // 读取Int
                    Console.WriteLine("电机001-故障码  \t" + plc.Read(DataType.DataBlock, dbArea, 10, VarType.Int, 1));
                    Console.WriteLine("****************一维移动设备001******************");
                    // 读取DWord
                    Console.WriteLine("移动设备001-任务号  \t" + plc.Read(DataType.DataBlock, dbArea, 12, VarType.DWord, 1));
                    // 读取Int
                    Console.WriteLine("移动设备001-任务状态\t" + plc.Read(DataType.DataBlock, dbArea, 16, VarType.Int, 1));
                    // 读取DWord
                    Console.WriteLine("移动设备001-起始位置\t" + plc.Read(DataType.DataBlock, dbArea, 18, VarType.DWord, 1));
                    // 读取DWord
                    Console.WriteLine("移动设备001-终止位置\t" + plc.Read(DataType.DataBlock, dbArea, 22, VarType.DWord, 1));
                    // 读取Int
                    Console.WriteLine("移动设备001-当前状态\t" + plc.Read(DataType.DataBlock, dbArea, 26, VarType.Int, 1));
                    // 读取Int
                    Console.WriteLine("移动设备001-当前坐标\t" + plc.Read(DataType.DataBlock, dbArea, 28, VarType.Int, 1));
                    // 读取Int
                    Console.WriteLine("移动设备001-故障码  \t" + plc.Read(DataType.DataBlock, dbArea, 30, VarType.Int, 1));

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

            Task writePLCTask = Task.Factory.StartNew(() =>
            {
                while (plc.IsConnected && inputKey != "q")
                {
                    // 写入DWord
                    plc.Write(DataType.DataBlock, dbArea, 0, iCount + 1);
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 4, (short)(iCount + 2));
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 6, (short)(iCount + 3));
                    // 写入Bool
                    plc.Write(DataType.DataBlock, dbArea, 8, boolFlag);
                    // 写入Bool
                    plc.Write(DataType.DataBlock, dbArea, 8, !boolFlag, 1);
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 10, (short)(iCount + 4));

                    // 写入DWord
                    plc.Write(DataType.DataBlock, dbArea, 12, iCount + 5);
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 16, (short)(iCount + 6));
                    // 写入DWord
                    plc.Write(DataType.DataBlock, dbArea, 18, iCount + 7);
                    // 写入DWord
                    plc.Write(DataType.DataBlock, dbArea, 22, iCount + 8);
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 26, (short)(iCount + 9));
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 28, (short)(iCount + 10));
                    // 写入Int
                    plc.Write(DataType.DataBlock, dbArea, 30, (short)(iCount + 11));

                    iCount++;
                    boolFlag = !boolFlag;
                    Task.Delay(200).Wait();
                }
            });

            inputKey = Console.ReadLine();

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


https://img-blog.csdnimg.cn/73fa6a3cf8924c9785af937e283af213.png

别忘了修改存储区域的地址!

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

2.3 C#面向对象的模式读写PLC的自定义数据类型

现在学到了“一”是一横,“二”是二横,“三”是三横。请问“四”是亖横吗? 那么来挑战一下“四”怎么写。

using S7.Net;

namespace SimS71500
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Plc plc = new Plc(CpuType.S71500, "192.168.0.100", 0, 1);
            plc.Open();

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

            int dbArea = 2;
            Motor motor = new Motor("电机001", plc, dbArea);
            MobileDevice mobileDevice = new MobileDevice("一维移动设备001", plc, dbArea);

            // 使用 AutoResetEvent 进行任务协调
            AutoResetEvent readComplete = new AutoResetEvent(false);
            AutoResetEvent writeComplete = new AutoResetEvent(true);
            bool needReadLock = false; // 控制是否需要读锁

            Task readPLCTask = Task.Factory.StartNew(() =>
            {
                while (plc.IsConnected && inputKey != "q")
                {
                    Console.Clear();
                    motor.ReadValues();
                    Console.WriteLine($"*********************{motor.Name}*********************");
                    Console.WriteLine($"{motor.TaskNumber} - {motor.TaskStatus} - {motor.CurrentStatus} - {motor.MotorForward} - {motor.MotorReverse} - {motor.FaultCode}");
                    mobileDevice.ReadValues();
                    Console.WriteLine($"****************{mobileDevice.Name}******************");
                    Console.WriteLine($"{mobileDevice.TaskNumber} - {mobileDevice.TaskStatus} - {mobileDevice.StartPosition} - {mobileDevice.EndPosition} - {mobileDevice.CurrentStatus} - {mobileDevice.CurrentCoordinate} - {mobileDevice.FaultCode}");
                    Task.Delay(200).Wait();

                    if (needReadLock)
                    {
                        writeComplete.Set(); // 通知写任务可以执行
                        readComplete.WaitOne(); // 等待写任务完成
                    }
                }
            });

            bool boolFlag = false;
            short iCount = 1;
            Task writePLCTask = Task.Factory.StartNew(() =>
            {
                while (plc.IsConnected && inputKey != "q")
                {
                    if (!needReadLock)
                    {
                        writeComplete.WaitOne(); // 等待读任务完成
                        needReadLock = true; // 设置需要读锁
                    }
                    iCount++;
                    motor.WriteValues((uint)(iCount + 1), (short)(iCount + 2), (short)(iCount + 3), boolFlag, !boolFlag, (short)(iCount + 4));
                    mobileDevice.WriteValues((uint)(iCount + 5), (short)(iCount + 6), (uint)(iCount + 7), (uint)(iCount + 8), (short)(iCount + 9), (short)(iCount + 10), (short)(iCount + 11));
                    boolFlag = !boolFlag;
                    needReadLock = false; // 取消读锁
                    readComplete.Set(); // 通知读任务可以执行
                    Task.Delay(2000).Wait();
                }
            });

            inputKey = Console.ReadLine();

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

        internal class Motor
        {
            private Plc plc;
            private int dbArea;

            public Motor(string name, Plc plc, int dbArea)
            {
                this.Name = name;
                this.plc = plc;
                this.dbArea = dbArea;
            }

            // 字段属性
            public string Name { get; private set; }
            public uint TaskNumber { get; private set; }      // 任务号
            public short TaskStatus { get; private set; }     // 任务状态
            public short CurrentStatus { get; private set; }  // 当前状态
            public bool MotorForward { get; private set; }   // 电机正转
            public bool MotorReverse { get; private set; }   // 电机反转
            public short FaultCode { get; private set; }     // 故障码

            // 读取PLC数据
            public void ReadValues()
            {
                TaskNumber = (uint)plc.Read(DataType.DataBlock, dbArea, 0, VarType.DWord, 1);
                TaskStatus = (short)plc.Read(DataType.DataBlock, dbArea, 4, VarType.Int, 1);
                CurrentStatus = (short)plc.Read(DataType.DataBlock, dbArea, 6, VarType.Int, 1);
                MotorForward = (bool)plc.Read(DataType.DataBlock, dbArea, 8, VarType.Bit, 1);
                MotorReverse = (bool)plc.Read(DataType.DataBlock, dbArea, 8, VarType.Bit, 1, 1);
                FaultCode = (short)plc.Read(DataType.DataBlock, dbArea, 10, VarType.Int, 1);
            }

            // 写入PLC数据
            public void WriteValues(uint taskNumber, short taskStatus, short currentStatus, bool motorForward, bool motorReverse, short faultCode)
            {
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 0, taskNumber);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 4, taskStatus);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 6, currentStatus);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 8, motorForward);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 8, motorReverse, 1);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 10, faultCode);
            }
        }

        internal class MobileDevice
        {
            private Plc plc;
            private int dbArea;

            public MobileDevice(string name, Plc plc, int dbArea)
            {
                this.Name = name;
                this.plc = plc;
                this.dbArea = dbArea;
            }
            // 字段属性
            public string Name { get; private set; }
            public uint TaskNumber { get; private set; }           // 任务号
            public short TaskStatus { get; private set; }          // 任务状态
            public uint StartPosition { get; private set; }        // 起始位置
            public uint EndPosition { get; private set; }          // 终止位置
            public short CurrentStatus { get; private set; }       // 当前状态
            public short CurrentCoordinate { get; private set; }   // 当前坐标
            public short FaultCode { get; private set; }           // 故障码

            // 读取PLC数据
            public void ReadValues()
            {
                TaskNumber = (uint)plc.Read(DataType.DataBlock, dbArea, 12, VarType.DWord, 1);
                TaskStatus = (short)plc.Read(DataType.DataBlock, dbArea, 16, VarType.Int, 1);
                StartPosition = (uint)plc.Read(DataType.DataBlock, dbArea, 18, VarType.DWord, 1);
                EndPosition = (uint)plc.Read(DataType.DataBlock, dbArea, 22, VarType.DWord, 1);
                CurrentStatus = (short)plc.Read(DataType.DataBlock, dbArea, 26, VarType.Int, 1);
                CurrentCoordinate = (short)plc.Read(DataType.DataBlock, dbArea, 28, VarType.Int, 1);
                FaultCode = (short)plc.Read(DataType.DataBlock, dbArea, 30, VarType.Int, 1);
            }

            // 写入PLC数据
            public void WriteValues(uint taskNumber, short taskStatus, uint startPosition, uint endPosition, short currentStatus, short currentCoordinate, short faultCode)
            {
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 12, taskNumber);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 16, taskStatus);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 18, startPosition);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 22, endPosition);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 26, currentStatus);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 28, currentCoordinate);
                Thread.Sleep(100); // 添加一个延迟以验证效果
                plc.Write(DataType.DataBlock, dbArea, 30, faultCode);
            }
        }
    }
}

这段代码中将两个PLC自定义数据类型转为了C#中的对象,通过对象的操作实现数据的读取。同时为了避免读的时候出现数据还没来得及写完的情况,加上了线程同步锁。
https://img-blog.csdnimg.cn/2191e78382bf4ecfa299610b88006dab.png

// 使用 AutoResetEvent 进行任务协调
AutoResetEvent readComplete = new AutoResetEvent(false);
AutoResetEvent writeComplete = new AutoResetEvent(true);

// 读锁
writeComplete.Set(); // 通知写任务可以执行
readComplete.WaitOne(); // 等待写任务完成

// 写锁
writeComplete.WaitOne(); // 等待读任务完成
readComplete.Set(); // 通知读任务可以执行

如果将上面的线程同步锁去掉,会有几率读到还没写入完成的数据信息。
https://img-blog.csdnimg.cn/1f12bac3a925471691d5ae284b9e5d97.png

三、WinForm项目示例

尝试做一个WinForm程序测试程序。在这一章,你将体验到做成一个WinForm项目的完整流程,并且完成从新手到熟手的转变。

3.1 新建项目

3.1.1 新建窗体项目

右击【解决方案】- 点击【添加】- 点击【新建项目】。
https://img-blog.csdnimg.cn/a226257aac274bf6b281bec6b5e7d285.png?x-oss-process=image/resize,w_800
选择【桌面】- 选择【Windows窗体应用】-【下一步】。
https://img-blog.csdnimg.cn/18c2b153097f4991adb3751b98b70383.png?x-oss-process=image/resize,w_800
输入【项目名称】-【下一步】。
https://img-blog.csdnimg.cn/47f21b7d7dae47c3a4fcdcd25237411f.png?x-oss-process=image/resize,w_800
点击【创建】。
https://img-blog.csdnimg.cn/a08dd0ceaf3d4f4eba1f59ea09e66d4e.png?x-oss-process=image/resize,w_800
右击【新建的项目】-【设为启动项目】。
https://img-blog.csdnimg.cn/bd8c7da393054edba57995ea36785ae7.png?x-oss-process=image/resize,h_800

3.1.2 项目中添加S7NetPlus NuGet包
  • 右击项目中的【依赖项】-【管控NuGet程序包】。
    https://img-blog.csdnimg.cn/51b5581fccd642b289c5cafc381da326.png?x-oss-process=image/resize,w_500
  • 找到【S7netplus】-【安装】。
    https://img-blog.csdnimg.cn/e3346c2a7dc84b1190a12dcfee0288f8.png?x-oss-process=image/resize,w_800
  • 【OK】。
    https://img-blog.csdnimg.cn/afafcf2b08d64936a0e643a196a63801.png?x-oss-process=image/resize,w_500
  • 可以看到【依赖项】中,【包】增加了【S7netplus】库。
    https://img-blog.csdnimg.cn/549614a041444d3591104e31194d7afb.png

3.2 PLC连接测试功能

3.2.1 添加PLC连接用的控件

【可选】:如果没有在设计界面找到工具箱,请点击【视图】-【工具箱】。
https://img-blog.csdnimg.cn/d0c9bb424e13421fb6540395937a1535.png?x-oss-process=image/resize,h_700
随便选几个控件排版一下。
https://img-blog.csdnimg.cn/2d3e62e20e754a59a82b633292d3d437.png?x-oss-process=image/resize,h_700
最终效果如下图所示:
https://img-blog.csdnimg.cn/a7deea74643140f78a6add14e1aafdfe.png?x-oss-process=image/resize,w_500

3.2.2 创建一个单例模式的PLC控制对象

创建PLC单例模式的主要目的是确保在应用程序中只有一个PLC连接实例。

以下是一些原因为什么要使用单例模式:

  • 资源共享:在一个应用程序中,通常只需要一个PLC连接,多次创建连接实例会导致资源浪费,例如内存和网络资源。通过使用单例模式,可以确保只有一个连接实例,并共享这个实例。+ 避免重复连接和断开:如果多个部分需要访问PLC,每次都创建和断开连接可能会导致不必要的连接和断开操作,这不仅浪费时间,还可能引发错误。单例模式确保在需要的时候可以共享已连接的PLC,而不需要重复连接和断开。+ 维护一致性:PLC连接状态和属性应该在应用程序中保持一致。使用单例模式,可以确保应用程序中的所有部分都使用相同的连接实例,从而保持一致性。+ 简化管理:通过将连接和相关的操作封装在单例类中,可以更容易地管理和维护PLC连接。在应用程序中只需关注一个连接点,而不是多个。+ 线程安全性:单例模式可以用于确保在多线程环境中只有一个连接实例,从而避免竞态条件和其他与多线程相关的问题。

总之,单例模式有助于管理资源、确保一致性、简化应用程序的结构和提高性能,特别是在需要共享和管理单个资源实例的情况下。

  • 添加【类】
    https://img-blog.csdnimg.cn/72a734b1934148478787e44deaad16cd.png?x-oss-process=image/resize,h_700
  • 命名为【PlcController】。
    https://img-blog.csdnimg.cn/63b1d92f1f79414681788b84c49bc113.png?x-oss-process=image/resize,w_800
  • 添加单例模式的PLC控制对象。
using S7.Net;

namespace SimS71500Form
{
    public class PlcController
    {
        private static readonly PlcController myPlc = new PlcController();
        private Plc? plc;

        // 饿汉式单例模式的私有构造函数

        public static PlcController MyPlc
        {
            get { return myPlc; }
        }

		public async Task Connect(CpuType cpuType, string ipAddress, short rack, short slot)
		{
		    // 检查PLC是否为null或未连接
		    if (plc == null || plc.IsConnected == false)
		    {
		        try
		        {
		            // 创建Ping对象以检查PLC的可达性
		            Ping ping = new Ping();
		
		            // 使用异步方式发送Ping请求
		            PingReply reply = await ping.SendPingAsync(ipAddress);
		
		            // 如果Ping请求失败或PLC不可达,抛出异常
		            if (reply == null || reply.Status != IPStatus.Success)
		            {
		                throw new Exception("PLC通讯失败");
		            }
		
		            // 初始化PLC连接
		            plc = new Plc(cpuType, ipAddress, rack, slot);
		
		            // 异步方式打开PLC连接
		            await plc.OpenAsync();
		        }
		        catch (Exception ex)
		        {
		            // 处理连接错误并抛出异常
		            throw new Exception("PLC连接错误: " + ex.Message);
		        }
		    }
		}


        public void Disconnect()
        {
            // 断开PLC连接
            if (plc != null && plc.IsConnected)
            {
                plc.Close();
            }
        }

        public bool IsConnected
        {
            get
            {
                if (plc != null)
                {
                    return plc.IsConnected;
                }
                return false;
            }
        }
    }
}


3.2.3 窗体中的PLC控制类的调用
using S7.Net;

namespace SimS71500Form
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            string[] cpuTypeArr = Enum.GetNames(typeof(CpuType));
            foreach (var item in cpuTypeArr)
            {
                cmbPLcType.Items.Add(item);
            }
        }

        private async void btnConnPlc_Click(object sender, EventArgs e)
        {
            try
            {
                // 使用异步方法,防止WinForm界面卡顿(假死)
                await MyPlc.Connect(
                      (CpuType)Enum.Parse(typeof(CpuType), cmbPLcType.Text),
                      txtPlcIp.Text,
                      Convert.ToInt16(cmbPlcRack.Text),
                      Convert.ToInt16(cmbPlcSlot.Text)
                      );
                if (MyPlc.IsConnected)
                {
                    txtConnLog.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}  连接成功!\r\n");
                }
            }
            catch (Exception ex)
            {
                // 处理读取错误
                txtConnLog.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}  {ex.Message}\r\n");
            }
        }

        private void btnDisConnPlc_Click(object sender, EventArgs e)
        {

            try
            {
                PlcController.MyPlc.Disconnect();
                if (PlcController.MyPlc.IsConnected == false)
                {
                    txtConnLog.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}  断开成功!\r\n");
                }
            }
            catch (Exception ex)
            {
                // 处理读取错误
                txtConnLog.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}  {ex.Message}\r\n");
            }
        }
    }
}

成功!运行效果如下图所示:
https://img-blog.csdnimg.cn/a644fab1d93c42c0bfeeb730bcaba743.png?x-oss-process=image/resize,w_500

3.3 读写PLC指定地址功能

3.3.1 继续添加控件

需要注意的是,位偏移只有在类型为Bit时才会使用。 最终效果如下图所示:
https://img-blog.csdnimg.cn/5e05df4b99be41a4b7054a37042858db.png?x-oss-process=image/resize,w_500

3.3.2 增加读写反馈类

这个类的主要目的是提供统一的反馈结果,以便在与PLC通信的过程中更容易处理结果和错误。

具体来说,这个代码段定义了以下类和属性: PLCResponce类: IsSuccess:布尔属性,指示操作是否成功。默认值为false,可以在操作成功时将其设置为true。 ReaponceMsg:字符串属性,用于存储操作的响应消息。默认为空字符串,可以在操作完成后将其设置为相关消息。 ReadResponce类(继承自PLCResponce): Data:对象属性,用于存储从PLC读取的数据。这是一个通用属性,可以存储各种类型的数据,具体数据类型取决于从PLC读取的内容。 WriteResponce类(继承自PLCResponce): 这个类没有额外的属性,它继承了IsSuccess和ReaponceMsg属性,用于表示写入操作的结果和响应消息。

这些类的作用是将PLC操作的结果进行封装,包括成功与否的状态以及相关的响应消息。这可以有助于系统更容易地处理与PLC的通信,以及提供一致的反馈格式,以便进一步的错误处理和日志记录等操作。在实际应用中,使用这些类来封装PLC读取和写入操作的结果,并根据需要检查IsSuccess属性来确定操作是否成功,并访问Data属性来获取读取的数据。

namespace SimS71500Form
{
    public class Responses
    {
        public class PLCResponce
        {
            public bool IsSuccess { get; set; } = false;
            public string ReaponceMsg { get; set; } = "";
        }

        /// <summary>
        /// 读取PLC后返回的数据结构
        /// </summary>
        public class ReadResponce : PLCResponce
        {
            public object? Data { get; set; }
        }

        /// <summary>
        /// 写入PLC后返回的数据结构
        /// </summary>
        public class WriteResponce : PLCResponce
        {

        }
    }
}


3.3.3 PLC控制类中增加读写操作
using static SimS71500Form.Responses;

/// <summary>
/// 读取指定地址的数据
/// </summary>
/// <param name="dataType">存储区域的数据类型</param>
/// <param name="dbArea">存储区域的地址</param>
/// <param name="startByteAdr">起始字节地址</param>
/// <param name="varType">要读取的变量类型</param>
/// <param name="varCount">变量的数量</param>
/// <param name="bitAdr">比特地址</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public ReadResponce ReadVariable(DataType dataType, int dbArea, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
{
    ReadResponce responce = new ReadResponce();
    try
    {
        if (plc == null || !plc.IsConnected)
        {
            responce.ReaponceMsg = "PLC未连接";
            return responce;
        }

        switch (varType)
        {
            case VarType.Bit:
                responce.Data = plc.Read(dataType, dbArea, startByteAdr, VarType.Bit, varCount, bitAdr);
                responce.IsSuccess = true;
                break;

            case VarType.Byte:
            case VarType.Word:
            case VarType.DWord:
            case VarType.Int:
            case VarType.DInt:
            case VarType.Real:
            case VarType.LReal:
                responce.Data = plc.Read(dataType, dbArea, startByteAdr, varType, varCount);
                responce.IsSuccess = true;
                break;

            case VarType.String:
                byte[] dataS = plc.ReadBytes(dataType, dbArea, 68, 256);
                int stringLen = dataS[1];
                string gbkString = Encoding.GetEncoding("GBK").GetString(dataS, 2, stringLen);
                responce.Data = gbkString;
                responce.IsSuccess = true;
                break;

            case VarType.S7String:
            case VarType.S7WString:
            case VarType.Timer:
            case VarType.Counter:
            case VarType.DateTime:
            case VarType.DateTimeLong:
                responce.ReaponceMsg = "未处理相关逻辑,请自行探索";
                break;
            default:
                break;
        }

        return responce;
    }
    catch (Exception ex)
    {
        // 处理读取错误
        responce.ReaponceMsg = "PLC读取错误: " + ex.Message;
        return responce;
    }
}

/// <summary>
/// 写入指定地址的数据
/// </summary>
/// <param name="dataType">存储区域的数据类型</param>
/// <param name="dbArea">存储区域的地址</param>
/// <param name="startByteAdr">起始字节地址</param>
/// <param name="varType">要读取的变量类型</param>
/// <param name="value">需要写入的值</param>
/// <param name="bitAdr">比特地址</param>
/// <returns></returns>
public WriteResponce WriteVariable(DataType dataType, int dbArea, int startByteAdr, VarType varType, string value, int bitAdr = -1)
{
    WriteResponce responce = new WriteResponce();

    try
    {
        if (plc == null || !plc.IsConnected)
        {
            responce.ReaponceMsg = "PLC未连接";
            return responce;
        }


        switch (varType)
        {
            case VarType.Bit:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToBoolean(value), bitAdr);
                responce.IsSuccess = true;
                break;

            case VarType.Byte:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToByte(value));
                responce.IsSuccess = true;
                break;

            case VarType.Word:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToUInt16(value));
                responce.IsSuccess = true;
                break;

            case VarType.DWord:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToUInt32(value));
                responce.IsSuccess = true;
                break;

            case VarType.Int:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToInt16(value));
                responce.IsSuccess = true;
                break;

            case VarType.DInt:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToInt32(value));
                responce.IsSuccess = true;
                break;

            case VarType.Real:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToSingle(value));
                responce.IsSuccess = true;
                break;

            case VarType.LReal:
                plc.Write(dataType, dbArea, startByteAdr, Convert.ToDouble(value));
                responce.IsSuccess = true;
                break;

            case VarType.String:
                // 编码为字节数组
                byte[] stringBytes = Encoding.GetEncoding("GBK").GetBytes(value);
                // 构建带控制信息的字节数组
                byte[] dataString = new byte[stringBytes.Length + 2]; // 加2是为了存储控制信息
                // 添加控制信息
                dataString[0] = 254; // 第一个字节固定为254
                dataString[1] = (byte)(stringBytes.Length); // 第二个字节表示字符长度
                // 复制字符串数据到字节数组
                Array.Copy(stringBytes, 0, dataString, 2, stringBytes.Length);
                // 将字节数组写入PLC
                plc.WriteBytes(dataType, dbArea, startByteAdr, dataString);
                responce.IsSuccess = true;
                break;

            case VarType.S7String:
            case VarType.S7WString:
            case VarType.Timer:
            case VarType.Counter:
            case VarType.DateTime:
            case VarType.DateTimeLong:
                responce.ReaponceMsg = "未处理相关逻辑,请自行探索";
                break;
            default:
                break;
        }

        return responce;
    }
    catch (Exception ex)
    {
        // 处理读取错误
        responce.ReaponceMsg = "PLC读取错误: " + ex.Message;
        return responce;
    }
}

3.3.4 窗体功能中调用PLC控制类的读写操作
private void btnReadPlc_Click(object sender, EventArgs e)
{
    ReadResponce responce = MyPlc.ReadVariable(
         (DataType)Enum.Parse(typeof(DataType), cmbDataType.Text),
         Convert.ToInt16(numDbArea.Value),
         Convert.ToInt16(numStartByteAdr.Value),
         (VarType)Enum.Parse(typeof(VarType), cmbVarType.Text),
         Convert.ToInt16(numVarCount.Value),
         Convert.ToByte(numBitAdr.Value)
        );
    if (responce == null)
    {
        txtReadWriteLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  读取失败!\r\n");
    }
    else if (responce.IsSuccess == false)
    {
        txtReadWriteLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  读取失败!{responce.ReaponceMsg}\r\n");
    }
    else
    {
        txtReadWriteLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  {responce.Data}\r\n");
    }

}

private void btnWritePlc_Click(object sender, EventArgs e)
{
    WriteResponce responce = MyPlc.WriteVariable(
        (DataType)Enum.Parse(typeof(DataType), cmbDataType.Text),
        Convert.ToInt16(numDbArea.Value),
        Convert.ToInt16(numStartByteAdr.Value),
        (VarType)Enum.Parse(typeof(VarType), cmbVarType.Text),
        txtInputData.Text,
        Convert.ToByte(numBitAdr.Value)
        );
    if (responce == null)
    {
        txtReadWriteLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  写入失败!\r\n");
    }
    else if (responce.IsSuccess == false)
    {
        txtReadWriteLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  写入失败!{responce.ReaponceMsg}\r\n");
    }
    else
    {
        txtReadWriteLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  写入成功!\r\n");
    }
}

运行效果如下图所示:
https://img-blog.csdnimg.cn/a4d7d2eba1d94a50865b43bd80a0ef18.png?x-oss-process=image/resize,w_500

3.4 读写PLC自定义数据类型功能

3.4.1 增加有立体效果的自定义控件

既然数据类型都自定义了,那么控件也可以自定义一个。 右击【项目】-【添加】-【用户控件】。
https://img-blog.csdnimg.cn/59e3f99f815d435b85af6694b558d568.png?x-oss-process=image/resize,h_500
输入【名称】-【添加】。
https://img-blog.csdnimg.cn/0369d8b541e54742b30580718020d122.png?x-oss-process=image/resize,w_800
如果自定义控件类库没在工具箱出现,请右击【项目】-【重新生成】。
https://img-blog.csdnimg.cn/bfdb46a92e1b47ab9c59b326a766e604.png
在自定义控件中添加代码。

using System.Drawing.Drawing2D;

namespace SimS71500Form
{
    public partial class CustomCircleControl : UserControl
    {
        private Color statusColor = Color.Gray;

        public Color StatusColor
        {
            get { return statusColor; }
            set
            {
                statusColor = value;
                Invalidate(); // 强制重新绘制以显示新的颜色
            }
        }

        public CustomCircleControl()
        {
            InitializeComponent();
            DoubleBuffered = true;
            Size = new Size(50, 50); // 设置控件的大小
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            Color darkerColor = ControlPaint.Dark(statusColor, 0.2f); // 调整 0.2f 以控制加深的程度

            int circleDiameter = Math.Min(Width, Height) - 10;
            int circleX = (Width - circleDiameter) / 2;
            int circleY = (Height - circleDiameter) / 2;

            // 创建一个线性渐变画刷
            LinearGradientBrush gradientBrush = new LinearGradientBrush(
                new Rectangle(circleX, circleY, circleDiameter, circleDiameter),
                statusColor,   // 渐变起始颜色
                darkerColor, // 渐变结束颜色
                LinearGradientMode.ForwardDiagonal); // 斜向45度的渐变
            e.Graphics.FillEllipse(gradientBrush, circleX, circleY, circleDiameter, circleDiameter);

            // 创建一个灰色边框的画笔
            Pen borderPen = new Pen(Color.Gray, circleDiameter / 15);
            e.Graphics.DrawEllipse(borderPen, circleX, circleY, circleDiameter, circleDiameter);
        }
    }
}

重新生成项目后,在工具栏中找到这个自定义控件,加入到窗体中。
https://img-blog.csdnimg.cn/f2a89fcf156c45f48d6a865e077f4cdb.png?x-oss-process=image/resize,w_800

3.4.2 继续添加其他控件

最后布局如下图所示。 注意:这里的自动读写的功能通过timer控件来实现。
https://img-blog.csdnimg.cn/e7861735cd914cfea6719bb594373e49.png?x-oss-process=image/resize,w_500

注意,timer控件需要设置如下属性,否则不会自动运行。
https://img-blog.csdnimg.cn/9280f7f5c1134dcf8709ddd980bff3eb.png

3.4.3 添加自定义数据类型相应的对象

创建一个Device类,代码中进行了简化,只读取当前状态,其他信息可以自行补充。

using S7.Net;
using static SimS71500Form.Responses;

namespace SimS71500Form
{
    public abstract class Device
    {
        public Device(string name, int dbArea)
        {
            this.Name = name;
            this.dbArea = dbArea;
        }
        public int dbArea { get; private set; }
        public string Name { get; private set; }
        public uint TaskNumber { get; private set; }      // 任务号
        public short TaskStatus { get; private set; }     // 任务状态
        public short CurrentStatus { get; private set; }  // 当前状态
        public short FaultCode { get; private set; }     // 故障码

        public void SetCurrentStatus(short newStatus)
        {
            // 提供一个方法在外部更改 CurrentStatus
            this.CurrentStatus = newStatus;
        }

        // 抽象的 Read 方法,返回ReadResponce
        public abstract ReadResponce Read();

        // 抽象的 Write 方法,返回WriteResponce
        public abstract WriteResponce Write(short status);
    }

    public class Motor : Device
    {
        public Motor(string name, int dbArea) : base(name, dbArea)
        {

        }
        public bool MotorForward { get; private set; }   // 电机正转
        public bool MotorReverse { get; private set; }   // 电机反转

        public override ReadResponce Read()
        {
            ReadResponce responce = PlcController.MyPlc.ReadVariable(DataType.DataBlock, dbArea, 6, VarType.Int, 1);
            if (responce.IsSuccess && responce.Data != null)
            {
                SetCurrentStatus((short)responce.Data);
            }
            return responce;
        }

        public override WriteResponce Write(short status)
        {
            WriteResponce responce = PlcController.MyPlc.WriteVariable(DataType.DataBlock, dbArea, 6, VarType.Int, status.ToString());
            return responce;
        }

    }

    public class MobileDevice : Device
    {
        public MobileDevice(string name, int dbArea) : base(name, dbArea)
        {

        }
        public uint StartPosition { get; private set; }        // 起始位置
        public uint EndPosition { get; private set; }          // 终止位置
        public short CurrentCoordinate { get; private set; }   // 当前坐标

        public override ReadResponce Read()
        {
            ReadResponce responce = PlcController.MyPlc.ReadVariable(DataType.DataBlock, dbArea, 26, VarType.Int, 1);
            if (responce.IsSuccess && responce.Data != null)
            {
                SetCurrentStatus((short)responce.Data);
            }
            return responce;
        }

        public override WriteResponce Write(short status)
        {
            WriteResponce responce = PlcController.MyPlc.WriteVariable(DataType.DataBlock, dbArea, 26, VarType.Int, status.ToString());
            return responce;
        }


    }

}


3.4.5 窗体中自定义类的读写操作
Motor motor;
private void btnCreateMotor_Click(object sender, EventArgs e)
{
    motor = new Motor("电机001", 2);
}
private void btnReadMotor_Click(object sender, EventArgs e)
{
    if (motor != null)
    {
        ReadResponce responce = motor.Read();

        if (responce == null)
        {
            txtDeviceLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  {motor.Name}读取失败!\r\n");
        }
        else if (responce.IsSuccess == false)
        {
            txtDeviceLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  {motor.Name}读取失败!{responce.ReaponceMsg}\r\n");
        }
        else
        {
            if (motor.CurrentStatus == 0)
            {
                ledMotorState.StatusColor = Color.Gray;
            }
            else if (motor.CurrentStatus == 1)
            {
                ledMotorState.StatusColor = Color.LimeGreen;
            }
            else
            {
                ledMotorState.StatusColor = Color.Red;
            }
        }
    }
    else
    {
        txtDeviceLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  电机未创建!\r\n");
    }
}

MobileDevice mobileDevice;
private void btnCreateMobile_Click(object sender, EventArgs e)
{
    mobileDevice = new MobileDevice("一维移动设备001", 2);
}

private void btnReadMobile_Click(object sender, EventArgs e)
{
    if (mobileDevice != null)
    {
        ReadResponce responce = mobileDevice.Read();

        if (responce == null)
        {
            txtDeviceLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  {mobileDevice.Name}读取失败!\r\n");
        }
        else if (responce.IsSuccess == false)
        {
            txtDeviceLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  {mobileDevice.Name}读取失败!{responce.ReaponceMsg}\r\n");
        }
        else
        {
            if (mobileDevice.CurrentStatus == 0)
            {
                ledMobileDeviceState.StatusColor = Color.Gray;
            }
            else if (mobileDevice.CurrentStatus == 1)
            {
                ledMobileDeviceState.StatusColor = Color.LimeGreen;
            }
            else
            {
                ledMobileDeviceState.StatusColor = Color.Red;
            }
        }
    }
    else
    {
        txtDeviceLog.AppendText($"{DateTime.Now.ToString("HH:mm:ss")}  移动设备未创建!\r\n");
    }
}

bool bAutoReadMotor = false;
bool bAutoReadMobile = false;

private void timerReadWrite_Tick(object sender, EventArgs e)
{
    DateTime now = DateTime.Now;
    int data = now.Second % 3;
    if (bAutoReadMotor && motor != null)
    {
        btnReadMotor_Click(null, null);
        motor.Write((short)data);
    }
    if (bAutoReadMobile)
    {
        btnReadMobile_Click(null, null);
        data = 3 - data;
        mobileDevice.Write((short)data);
    }
}

private void btnAutoReadMotor_Click(object sender, EventArgs e)
{
    bAutoReadMotor = !bAutoReadMotor;
}

private void btnAutoReadMobile_Click(object sender, EventArgs e)
{
    bAutoReadMobile = !bAutoReadMobile;
}

其中,下面这两个变量用于分别控制自动读写的开关。只有开启的时候,才会自动进行读写。

bool bAutoReadMotor = false;
bool bAutoReadMobile = false;

下面这两行代码是直接调用点击按钮事件,减少重复的代码。

btnReadMotor_Click(null, null);
btnReadMobile_Click(null, null);

3.5 最终运行效果

https://img-blog.csdnimg.cn/7f7e2d69dadc4271b49064464367a980.png?x-oss-process=image/resize,w_800

总结

这篇文档提供了一个关于如何使用C#与西门子PLC进行通讯的详细指南。 文档包含了以下主要内容:

  • 数据类型对照表:列出了常见的PLC数据类型与C#数据类型的对应关系,以便确保在通信过程中数据的正确传递和解释。+ C#读取PLC数据:解释了如何使用S7 Net Plus库来读取不同类型的数据从PLC,包括基本数据类型、大端存储和小端存储数据以及中文字符的处理。+ C#写入PLC数据:介绍了如何使用S7 Net Plus库来写入不同类型的数据到PLC,包括字符串数据的处理。+ 自定义数据类型:展示了如何在PLC中创建自定义数据类型,并演示了如何在C#中读取和写入这些自定义数据类型。+ WinForm项目示例:提供了一个完整的WinForm项目示例,包括PLC连接测试、读写PLC指定地址、读写PLC自定义数据类型等功能。这个示例可以将前面学到的知识应用到一个实际项目中。

非常感谢您能坚持看到这里。 欢迎交流!

终,电气问曰:「何能及君耶?」答曰:「无他,唯手熟尔。」


文章来源: https://lukailin.blog.csdn.net/article/details/134044687
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。


  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值