上位机基础-PLC通信篇

Posted 聆听微风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了上位机基础-PLC通信篇相关的知识,希望对你有一定的参考价值。

上位机基础-通信PLC篇

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

1. Modbus Slave 的使用教程

以读取输出线圈功能为例(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 八个线圈位置

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. 设置当前模块为允许通信

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

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

方法1:S7-PLCSIM Advanced

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

  1. 配置仿真器

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

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

  1. 点击装载

  2. 点击Run

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

方法2:NetToPLCsim

PLC模块通信配置略过

  1. 点击仿真

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

  1. 仿真器点击Run

  2. 配置NetToPLCsim

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

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

    配置卡槽号信息:

    点击Start

    注意最重要的一点

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

代码实现

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

  1. 安装指定的PLC类库

  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);

代码如下(读取代码):

            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;
        

上位机开发之单片机通信实践

经常会有一些学员会问到上位机与单片机之间通信的问题,而我们经常会讲上位机与PLC之间通信,那么其实对上位机开发来说,不管是和PLC通信,还是和单片机通信,通信原理都是一样的。PLC的本质就是单片机,在单片机的基础上添加一些外围电路并形成产品化,即构成了PLC控制器。今天在这里给大家分享一个上位机与单片机通信的实例,希望对大家开发上位机有所启发。

1. 单片机硬件介绍

只要做上位机开发,就离不开通信协议。一般来说,单片机可以与上位机之间以串口通信为主,当然也不排除现在有的单片机也集成了以太网口。就串口通信而言,常用的几种通信方式,包括串口自定义协议、Modbus协议、CAN总线,接下来介绍的这个单片机是某个锂电池的核心板,它主要是支持Modbus协议和CAN总线的方式。

技术图片

 

 

 

图表 1 单片机硬件

2. 通信测试

(1)这里我们选择的是基于485总线的ModbusRTU通信协议,如果要实现上位机开发,需要单片机开发人员提供一份通信变量表,如果读取变量较多或者不连续,需要进行分组读取。

(2)通信变量表一般包含参数名称、Modbus地址、存储区、数据类型、换算公式等内容,能够将通信变量表看明白并完成通信测试,是能够完成上位机开发的前提。

(3)这里,我截取部分变量表跟大家做一个分析:

技术图片

 

 

 

图表 2 Modbus寄存器表

上表中,以电芯总电压为例,Modbus地址为0x1003,对应十进制即为4099,寄存器地址即为44100,读取类型为ushort类型,换算公式为读取之后乘以0.01,比如读取值为5630,即为5.63V。

分析明白之后,我们就可以先用Modbus Poll软件来一波初步测试,如果需要Modbus软件资料的,可以关注一下喜科堂官方关注:dotNet工控上位机,然后像聊天一样发送关键词:Modbus软件套装即可。这里我们需要通过485转USB连接到电脑中,然后通过设备管理器,看到端口号为COM4。

技术图片

 

 

 

图表 3 通信端口

打开Modbus Poll软件,通信参数选择COM4、9600、N、8、1,读取寄存器起始地址为4099,读取长度为10,具体配置如下图所示:

技术图片

 

 

 

图表 4 通信参数配置

技术图片

 

 

 

图表 5 通信读取配置

配置完成后,即可读取到单片机的数据,具体如下图所示:

技术图片

 

 

 

图表 6 ModbusPoll读取

(4)实现读取之后,我们分析一下结果,4099读取到的值为4206,说明当前电池的电压为42.06V。我们可以用实际开发完成的上位机软件做下对比,验证一下数据是否正确:

技术图片

 

 

 

图表 7 上位机软件

(5) 我们也可以用喜科堂通信测试平台来做下测试,测试结果如下:

技术图片

 

 

 

3. 整体总结

本文主要针对单片机的Modbus通信实例做了较为详尽的描述,由于篇幅有限,仅仅介绍了通信测试部分,对于后续的项目实战部分,会通过后续的文章进行进一步的阐述。

 

公众号:thinger_swj

技术图片

以上是关于上位机基础-PLC通信篇的主要内容,如果未能解决你的问题,请参考以下文章

基于OPC技术的上位机与PLC之间的通信

C#实现上位机与PLC通信

西门子200/300PLC通过CHNet-S7200/300与海得上位机软件ModbusTCP通信

三菱Q系列PLC与上位机易控组态软件ModbusTCP通信案例

兴达易控MPI转光纤模块应用-300PLC与远端3公里外地上位机MPI通信

三菱FX系列PLC与上位机易控INSPEC软件ModbusTCP 通信