提示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
文章目录
-
1.2.1 Plc.Read方法源码浅析+ 1.2.2 PLC 中增加数据类型+ 1.2.3 C# 读取PLC中不同类型的数据+ 1.2.4 读取的代码解析+
-
1.2.4.1 基本数据类型的读取方法+ 1.2.4.2 大端存储和小端存储的问题+ 1.2.4.3 C# 读取中文乱码的问题
-
3.2.1 添加PLC连接用的控件+ 3.2.2 创建一个单例模式的PLC控制对象+ 3.2.3 窗体中的PLC控制类的调用
-
3.3.1 继续添加控件+ 3.3.2 增加读写反馈类+ 3.3.3 PLC控制类中增加读写操作+ 3.3.4 窗体功能中调用PLC控制类的读写操作
-
3.4.1 增加有立体效果的自定义控件+ 3.4.2 继续添加其他控件+ 3.4.3 添加自定义数据类型相应的对象+ 3.4.5 窗体中自定义类的读写操作
前言
翌日,斯电气之士大喜,言已成通讯之试,访吾欲构一物。余默思片刻,书此以为之。
本文基于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 的定义等。 或博图自带的帮助文档:
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 中增加数据类型
在博图软件中增加一些测试数据类型,如下:
配置信息拷贝出来,方便参考:
名称数据类型偏移量起始值监视值布尔量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);
}
}
}
下面是读取到的效果:
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")
中的DB1
的1
。VarType.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
这两个参数,则会在字符串前显示一个“?”。
具体解释参考西门子官方文档中关于string 在西门子 PLC 中的格式的解析。
在我们的案例中,获取的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(