上位机基础-PLC通信篇

上位机基础-通信PLC篇

1. ModbusRTU协议(测试与实现)

1. Modbus Slave 的使用教程

image-20230329193821391

以读取输出线圈功能为例(RTU模式使用CRC校验,Ascii 使用LRC校验):

主站:11 01 00 13 00 1B CRC

含义:读取11H从站的输出线圈(01 功能码 是输出线圈) ,起始地址0013H(19->00020),读取的线圈个数001BH(27)个

报文的起始地址为0,但是寄存器的最小地址为1.所以对应的地址需要后移动一个。

即读取从站输出线圈从0020-0046

从站报文: 11 01 04 CD 6B B2 05 CRC

从站返回输出线圈 0020-0046。

CD= 1100 1101对应 0020--0027 八个线圈位置

image-20230329200602995

image-20230329200622237

image-20230329200641445

2. 使用NModbus4包进行参数读取

        public Form1()
        {
            InitializeComponent();
        }
        SerialPort serialPort;
        private void Form1_Load(object sender, EventArgs e)
        {
            serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);
            serialPort.Open();
        }

        private void button1_Click(object sender, EventArgs e)
        {

            ModbusSerialMaster modbusMaster = ModbusSerialMaster.CreateRtu(serialPort);
            byte addr = 17;
            ushort startAddr = 19;
            ushort count = 27;
            bool[] boolData = modbusMaster.ReadCoils(addr, startAddr, count);
            string ldata = string.Empty;

            /// 先把bool 转换为 string 2进制
            foreach (bool item in boolData)
            {
                ldata += item == true ? 1 : 0;
            }
            ldata= ldata.Trim(' ').Replace(" ", "");
            textBox1.Text = StringBinTOStringHex(ldata);
        }


        public string StringBinTOStringHex(string str)
        {
            int Cnt = (str.Length / 4);
            bool IsLastFourData = str.Length % 4 == 0 ? true : false;
            if (!IsLastFourData) { Cnt++; }
            string lResult = string.Empty;
            for (int i = 0; i < Cnt; i++)
            {
                if (str.Length < 4)
                    str = str.PadLeft(4, '0');
                string temp = str.Substring(0, 4);

                if (i == Cnt - 1 && !IsLastFourData)
                    lResult += Convert.ToInt16(temp, 2).ToString("X2");
                else
                    lResult += Convert.ToInt16(temp, 2).ToString("X");

                if (i % 2 == 1)
                    lResult += " ";
                str = str.Remove(0, 4);
            }
            return lResult;
        }

2. PLC通信(配置与代码实现)

基础知识:PLC的各种数据类型

数据类型 位数 案例 说明
Bool 布尔,1位 DB9.DBX7.0 DB9块Bool类型,偏移量为7,第一位的布尔数据
Byte 字节,8位 DB9.DBB6 DB9块byte类型,偏移量为6的字节数据
Word 字,16位 DB9.DBW4 DB9块字类型,偏移量为4的字数据
Dword 双字,32位
Sint 有符号短整数,8位
Usint 无符号短整数,8位
Int 有符号整数,16位
UInt 无符号整数,16位
Real 32位单精度 DB9.DBD0 DB9块Real类型,偏移量为0的单精度数据
LReal 64位双精度

PC端通信配置

首先对需要通信的PLC模块进行设置

  1. 设置当前模块为允许通信

image-20230331235348676

  1. 取消 “优化的块访问”

    image-20230331235443290

  2. 记住当前需要通信的设备ip地址

    image-20230401000118891

方法1:S7-PLCSIM Advanced

  1. 下载的时候,选择接口为虚拟适配器

image-20230401000206794

  1. 配置仿真器

    选择适配器,配置网络地址,然后点击Start

    image-20230401000311374

    1. 下载程序到设备。先搜索设备地址,然后点击下载

image-20230401000500694

  1. 点击装载

    image-20230401000606100

  2. 点击Run

image-20230401000639940

  1. 查看仿真器,状态灯会显示绿色

    image-20230401000729495

方法2:NetToPLCsim

PLC模块通信配置略过

  1. 点击仿真

    image-20230401002511857

  2. 点击开始搜索,并且下载程序

image-20230401002530087

  1. 仿真器点击Run

    image-20230401003347710

  2. 配置NetToPLCsim

    先配置本机的IP地址,这个是选择网卡中的任何一个IP地址都行

    image-20230401003455269

    配置需要访问的PLC模块地址

    image-20230401003538004

    配置卡槽号信息:

    image-20230401003609502

    点击Start

    image-20230401003623208

    注意最重要的一点

    程序访问的时候,使用你本地的IP地址

    image-20230401003738313

代码实现

第一部分:连接PLC并且初始化

  1. 安装指定的PLC类库

image-20230401004058117

  1. 初始化连接

    Plc myPlc = new Plc(CpuType.S71500, "192.168.255.105", 0, 1);
    myPlc.Open();
    if (myPlc.IsConnected){MessageBox.Show(“连接成功”);}
    

关于写入与读取对应转换说明如下:

bool -> bit

byte -> byte

Usint,Uint,(小于等于16位)。统一使用 ushort接收。

int,word (小于等于16位)。统一使用 short接收。

Dword(大于16为,小于等于32位)。使用int接收.

Real ,用Float 接收

第二部分:读取并且写入对应类型的PLC数据

第一种方法,指定地址读写 ( 使用myPlc.Read()方法进行读,使用myPlc.Write()方法进行写入)

使用案例(略):

有部分命令格式不知道。以后有空填坑

			//Bool
            plc.Write("DB1.DBX0.0", true);
            var IsRight = plc.Read("DB1.DBX0.0");
            Console.WriteLine("DB1.DBX0.0: " + IsRight);

            //Int
            plc.Write("DB1.DBW2.0", Convert.ToInt16(1));
            int Score = (ushort)plc.Read("DB1.DBW2.0");
            Console.WriteLine("DB1.DBW2.0: " + Score);

            // Real
            plc.Write("DB1.DBD4.0", Convert.ToSingle(1.1));
            var Money = ((uint)plc.Read("DB1.DBD4.0")).ConvertToFloat();
            Console.WriteLine("DB1.DBD4.0: " + Money);

            //String写入
            var temp = Encoding.ASCII.GetBytes("Chen");   //将val字符串转换为字符数组
            var bytes = S7.Net.Types.S7String.ToByteArray("Chen", temp.Length);
            plc.WriteBytes(DataType.DataBlock, 1, 8, bytes);
            //String读取
            var reservedLength = (byte)plc.Read(DataType.DataBlock, 1, 8, VarType.Byte, 1);//获取字符串长度
            var Name = (string)plc.Read(DataType.DataBlock, 1, 8, VarType.S7String, reservedLength);//获取对应长度的字符串
            Console.WriteLine("DB1.8.0: " + Name);

            // DInt
            plc.Write("DB1.DBD264.0", Convert.ToInt32(20));
            var dIntVar = (uint)plc.Read("DB1.DBD264.0");
            Console.WriteLine("DB1.DBD264.0: " + dIntVar);

            // DWord
            plc.Write("DB1.DBD268.0", 123456);
            var dWordVar = (uint)plc.Read("DB1.DBD268.0");
            Console.WriteLine("DB1.DBD268.0: " + dWordVar);

            // Word
            plc.Write("DB1.DBD270.0", 12345678);
            var wordVar = (uint)plc.Read("DB1.DBD270.0");
            Console.WriteLine("DB1.DBD270.0: " + wordVar);
第二种方法, 解析读写

需要指定DB的类型、DB号、起始地址、PLC数据类型及读取数量。虽然它需要传入的参数变多了,但是当需要读取多个地址连续且类型相同的变量时,仅需修改最后的读取数量,S7NetPlus就会自动读取这一连串的地址,并按照指定的变量类型解析出对应的值,

函数说明:

public object Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0); 
//读取
bool result = (bool)plc.Read(DataType.DataBlock, 10, 0, VarType.Bit, 1);
//写入
plc.Write(DataType.DataBlock, 10, 0, true);

image-20230401015146870

代码如下(读取代码):

            var Real = myPlc.Read(DataType.DataBlock, 9, 0, VarType.Real,1);
            var Int = myPlc.Read(DataType.DataBlock, 9, 4, VarType.Int, 1);
            byte Byte = (byte)myPlc.Read(DataType.DataBlock, 9, 6, VarType.Byte, 1);
            var Word = myPlc.Read(DataType.DataBlock, 9, 8, VarType.Word, 1);
            var Dword = myPlc.Read(DataType.DataBlock, 9, 10, VarType.DWord, 1);
            var Uint = myPlc.Read(DataType.DataBlock, 9, 14, VarType.Int, 1);
            var LReal = myPlc.Read(DataType.DataBlock, 9, 16, VarType.LReal, 1);
            var lBool = myPlc.Read(DataType.DataBlock, 9, 24, VarType.Bit, 1);

写入代码:

            myPlc.Write(DataType.DataBlock, 9, 0, 6.5f);
            myPlc.Write(DataType.DataBlock, 9, 4,  (ushort)1);
            myPlc.Write(DataType.DataBlock, 9, 6, (byte)51);
            myPlc.Write(DataType.DataBlock, 9, 8, (ushort)11);
            myPlc.Write(DataType.DataBlock, 9, 24, false);

前两种方法,每次读取都是建立新的TCP连接,比较消耗计算机资源。

详细见文档表述:

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.

第三种方法, 块读取与写入(未验证)。

一次性从连接中读取所有需要查看的信息,然后进行解析。

public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
//读取数据选择从DB块中读取,db设置为1,起始地址为0,读取18个字节
var bytes = plc.ReadBytes(DataType.DataBlock, 1, 0, 18);
//取字节0中的第0位
var db1Bool1 = bytes[0].SelectBit(0);
Console.WriteLine("DB1.DBX0.0:" + db1Bool1);
//取字节0中的第1位
bool db1Bool2 = bytes[0].SelectBit(1); ;
Console.WriteLine("DB1.DBX0.1:" + db1Bool2);

//跳到字节2并连续取两个字节数据
int IntVariable = S7.Net.Types.Int.FromByteArray(bytes.Skip(2).Take(2).ToArray());
Console.WriteLine("DB1.DBW2.0:" + IntVariable);
//...
double RealVariable = S7.Net.Types.Real.FromByteArray(bytes.Skip(4).Take(4).ToArray());
Console.WriteLine("DB1.DBD4.0:" + RealVariable);
//...
int dIntVariable = S7.Net.Types.DInt.FromByteArray(bytes.Skip(8).Take(4).ToArray());
Console.WriteLine("DB1.DBD8.0: " + dIntVariable);
//...
uint dWordVariable = S7.Net.Types.DWord.FromByteArray(bytes.Skip(12).Take(4).ToArray());
Console.WriteLine("DB1.DBD12.0: " + Convert.ToString(dWordVariable, 16));
//...
ushort wordVariable = S7.Net.Types.Word.FromByteArray(bytes.Skip(16).Take(2).ToArray());
Console.WriteLine("DB1.DBW16.0: " + Convert.ToString(wordVariable, 16));
public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
字符串读取与写入
//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的示例中需要对总字符数和当前字符数的两个字节数组进行反转。

        /// <summary>
        /// 获取西门子PLC字符串数组--String
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private byte[] GetPLCStringByteArray(string str)
        {
            byte[] value = Encoding.Default.GetBytes(str);
            byte[] head = new byte[2];
            head[0] = Convert.ToByte(254);
            head[1] = Convert.ToByte(str.Length);
            value = head.Concat(value).ToArray();
            return value;
        }
 
        /// <summary>
        /// 获取西门子PLC字符串数组--WString
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private byte[] GetPLCWStringByteArray(string str)
        {
            byte[] value = Encoding.BigEndianUnicode.GetBytes(str);
            byte[] head = BitConverter.GetBytes((short)508);
            byte[] length = BitConverter.GetBytes((short)str.Length);
            Array.Reverse(head);
            Array.Reverse(length);
            head = head.Concat(length).ToArray();
            value = head.Concat(value).ToArray();
            return value;
        }
        
//写入String 
string str = "Example";
plc.Write(DataType.DataBlock, 10, 0, GetPLCStringByteArray(str));

//写入WString
string str = "示例";
plc.Write(DataType.DataBlock, 10, 0, GetPLCWStringByteArray(str));

旧版本的单次字节读取是有字节数限制的,每一次读取的最大字节数为200,如果需要读写更多的字节,则需要多次读写并进行拼接,以下提供两种方法,可供参考:

        /// <summary>
        /// 循环读取
        /// </summary>
        /// <param name="numBytes">要读取的字节数</param>
        /// <param name="db">DB号</param>
        /// <param name="startByteAdr">起始地址</param>
        /// <returns></returns>
        private byte[] CyclicReadMultipleBytes(int numBytes, int db, int startByteAdr = 0)
        {
            byte[] resultBytes = new byte[0];
            int index = startByteAdr;
            while (numBytes > 0)
            {
                var maxToRead = Math.Min(numBytes, 200);
                byte[] bytes = plc.ReadBytes(DataType.DataBlock, db, index, maxToRead);
                if (bytes == null)
                    return null;
                resultBytes = resultBytes.Concat(bytes).ToArray();
                numBytes -= maxToRead;
                index += maxToRead;
            }
            return resultBytes;
        }
 
        /// <summary>
        /// 递归读取
        /// </summary>
        /// <param name="numBytes">要读取的字节数</param>
        /// <param name="db">DB号</param>
        /// <param name="startByteAdr">起始地址</param>
        /// <returns></returns>
        public static byte[] RecursiveReadMultipleBytes(int numBytes, int db, int startByteAdr = 0)
        {
            byte[] result = new byte[0];
            if (numBytes > 200)
            {
                byte[] temp = plc.ReadBytes(DataType.DataBlock, db, startByteAdr, 200);
                numBytes -= 200;
                result = temp.Concat(RecursiveReadMultipleBytes(numBytes, db, startByteAdr + 200)).ToArray();
            }
            else
            {
                byte[] temp = plc.ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
                result = result.Concat(temp).ToArray();
                return result;
            }
 
            return result;
        }
posted @ 2023-04-01 03:02  聆听微风  阅读(1620)  评论(0编辑  收藏  举报