干翻全岛蛙蛙 发表于 2024-8-9 02:55:18

C#与西门子PLC1500的ModbusTcp服务器通讯4--搭建ModbusTcp客户端

 1、客户端选择

客户端可以是一个程序或一个设备,这里我以C#WINFORM程序来实现客户机与PLC的Modbustcp服务器通讯,开发环境是VS2019,.NET Framework版本是4.7.2
2、创建winform程序

https://i-blog.csdnimg.cn/blog_migrate/6030390127526518d87b95b21a69e729.png

https://i-blog.csdnimg.cn/blog_migrate/8bfb83807c67fc8c342ea6b2f55dfce5.png
 创建类库
https://i-blog.csdnimg.cn/blog_migrate/67218dbbeaad299893ea99e1fddca738.png
 https://i-blog.csdnimg.cn/blog_migrate/166f05387c7d9f4debd1650fa1b77828.png
https://i-blog.csdnimg.cn/blog_migrate/26e6804933b3d752f63cb0511aec6990.png
编写C#各种类的转换库,该库由我提供,不消费心,文章最后提供。
项目引入这个类库 
 https://i-blog.csdnimg.cn/blog_migrate/eef72ab5807f88c5c009f7e461ee8e78.png

3、引入Nmodbus4协议

找到项目,找到引用,右键“管理nuget程序”,在下面临话框操纵
https://i-blog.csdnimg.cn/blog_migrate/f4a93e632bbf929cd5063ca228595050.png


 4、界面结构如下:

结构中用到的是下拉框combobox,文本框textbox,按钮button,标签label
https://i-blog.csdnimg.cn/blog_migrate/25d587c85c4c0b3615d09facba5ec774.png
 这个IP地址和端口号是与这里对应
https://i-blog.csdnimg.cn/blog_migrate/83ff82adb47c3ab2ef40422b1324caaf.png

https://i-blog.csdnimg.cn/blog_migrate/3eb43735238fa60d4215c0ba34b905bc.png
 https://i-blog.csdnimg.cn/blog_migrate/8715389c10af6fd864a3f2a69d8545ca.png

5、窗体定义两个变量,并引入对应的下令空间

        ModbusIpMaster master = null;//modbus对象
        TcpClient tcpClient = null;//tcp客户端对象
https://i-blog.csdnimg.cn/blog_migrate/b5136c1dabab00bc16f68f8297c6567c.png

6、毗连按钮代码

private void btnOpen_Click(object sender, EventArgs e)
      {
            string ip = txtIPAddress.Text.Trim();
            bool t = IsIP(ip);
            if (t)
            {
                try
                {
                  int port = int.Parse(txtPort.Text.Trim());
                  tcpClient = new TcpClient();
                  tcpClient.Connect(ip, port);//连接到主机
                  master = ModbusIpMaster.CreateIp(tcpClient);//Ip 主站
                  master.Transport.ReadTimeout = 1000;//读超时
                  master.Transport.WriteTimeout = 1000;//写超时
                  master.Transport.Retries = 3;//尝试重复连接次数
                  master.Transport.WaitToRetryMilliseconds = 200;//尝试重复连接间隔
                  lblMessage.Text = "连接成功!";
                  btnOpen.Enabled = false;
                }
                catch (Exception ex)
                {
                  MessageBox.Show("连接失败," + ex.Message);
                }
            }
            else
            {
                MessageBox.Show("无效的ip地址!");
            }
      } https://i-blog.csdnimg.cn/blog_migrate/8fc826fb435966dc7360c7f3e1afe229.png
 7、读取的代码--ushort范例

本例子中只用到了读取保存寄存器这个功能码,即ReadHoldingRegisters(从站地址,开始地址,寄存器数量)
/// <summary>
      /// 读取
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void myread_Click(object sender, EventArgs e)
      {
            //由于NModbus4读取到寄存器的数据都是ushort类型
            //功能码
            string readType = cboReadTypes.Text.Trim();
            //从站地址
            byte slaveAddr = byte.Parse(txtRSlaveId.Text.Trim());
            //开始地址
            ushort startAddr = ushort.Parse(txtRStartAddress.Text.Trim());
            //读取数量
            ushort readCount = ushort.Parse(txtRCount.Text.Trim());
            switch (readType)
            {
                case "读线圈":
                  bool[] blVals = master.ReadCoils(slaveAddr, startAddr, readCount);
                  txtReadDatas1.Text = string.Join(",", blVals.Select(b => b ? "1" : "0"));
                  break;
                case "读输入线圈":
                  bool[] blInputVals = master.ReadInputs(slaveAddr, startAddr, readCount);
                  txtReadDatas1.Text = string.Join(",", blInputVals.Select(b => b ? "1" : "0"));
                  break;
                case "读保持寄存器":
                  //情况1:ushort到ushort类型:即读取无符号的整数,如23,89,处理方法是:原封不动
                  //ushort[] uDatas = master.ReadHoldingRegisters(slaveAddr, startAddr, readCount);
                  //txtReadDatas.Text = string.Join(",", uDatas);

                  //功能码
                  string dataType = cmddatatype.Text.Trim();
                  switch (dataType)
                  {
                        case "ushort":
                            //利用token循环读取
                            ushortctsRead = new CancellationTokenSource();
                            Task.Run(new Action(() =>
                            {
                              ReadUshortFromPLC(slaveAddr, startAddr, readCount);
                            }), ushortctsRead.Token);
                            break;
                        case "short":
                            //利用token循环读取
                            shortctsRead = new CancellationTokenSource();
                            Task.Run(new Action(() =>
                            {
                              ReadShortFromPLC(slaveAddr, startAddr, readCount);
                            }), shortctsRead.Token);
                            break;
                        case "float":
                            //利用token循环读取
                            floatctsRead = new CancellationTokenSource();
                            Task.Run(new Action(() =>
                            {
                              ReadFloatFromPLC(slaveAddr, startAddr, readCount);
                            }), floatctsRead.Token);
                            break;
                  }
                  break;
                case "读输入寄存器":
                  ushort[] uDatas1 = master.ReadInputRegisters(slaveAddr, startAddr, readCount);
                  txtReadDatas1.Text = string.Join(",", uDatas1);
                  break;
            }
      } 这里要注意,
NModbus4读取到寄存器的数据都是ushort范例
NModbus4读取到寄存器的数据都是ushort范例
代码中用到ReadUshortFromPLC方法,ReadShortFromPLC方法,ReadFloatFromPLC方法在本文最后链接都会提供
运行程序,毗连成功,读取数据
https://i-blog.csdnimg.cn/blog_migrate/e4ee35d244cb18ca4ef9a73ab52b3723.png
 注意这里,从站地址一般都是1,除非你改了,开始地址是0,表示寄存器的起始地址,数量是3,表示读取3个寄存器数量,也就是前面3个变量,m1-speed,m1-duaror,m1-level
https://i-blog.csdnimg.cn/blog_migrate/a2c5af2d45b951b0b9f46357a021934f.png
 这里为什么数量不能是4,因为第4个变量是real,它占2个寄存器,即占4个字节,它不是ushort范例,这里地址也不能是%DB3.DBW4这种写法,这不是S7协议读取变量,是MODBUS读取寄存器,两者不一样的,别糊涂了,各位长老。
8、读取的代码--float范例

https://i-blog.csdnimg.cn/blog_migrate/2f3b505cc69c79b1e89d6ed9ba37e4b4.png
 许多人搞不清楚这个开始地址和数量,这个开始地址是Modbus的地址,Modbus地址编号从0开始,因此8个变量的地址就是0,1,2,3,4,5,6,7,数量是指要读取的寄存器个数,word占一个,real占2个,这里很难理解,比较绕比较晕,一个是PLC地址,一个是MODBUS地址
我们要读的温度是第3个寄存器,它是real范例,占2个寄存器数量
如果要读取“摩头2温度”,怎么读了?
https://i-blog.csdnimg.cn/blog_migrate/6fb247f566417d51efa0bab3f571e1be.png
https://i-blog.csdnimg.cn/blog_migrate/2bce7bdc73cf9aa471b5f66d73c80f20.png 
 各人想想,为什么开始地址是8?
9、写入的代码--ushort范例

/// <summary>
      /// 写入
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnWrite_Click(object sender, EventArgs e)
      {
            //功能码
            string writeType = cboWriteTypes.Text.Trim();
            //从站地址
            byte slaveAddr = byte.Parse(txtWSlaveId.Text.Trim());
            //开始地址
            ushort startAddr = ushort.Parse(txtWStartAddress.Text.Trim());
            //数量
            //实际数量
            string objWriteVals = "";
            string dataType = cmddatatype2.Text.Trim();
            switch (dataType)
            {
                case "ushort":
                  objWriteVals = txtWriteDatas1.Text.Trim();
                  break;
                case "short":
                  objWriteVals = txtWriteDatas2.Text.Trim();
                  break;
                case "float":
                  objWriteVals = txtWriteDatas3.Text.Trim();
                  break;
            }
            ushort writeCount = ushort.Parse(txtWCount.Text.Trim());
            ushort objWCount = (ushort)objWriteVals.Split(',').Length;
            //实际数量与要求数量不一致,不允许操作
            if (writeCount != objWCount)
            {
                MessageBox.Show("写入值的数量不正确!");
                return;
            }
            string vals = objWriteVals;
            switch (writeType)
            {
                case "写单线圈":
                  bool blVal = vals == "1" ? true : false;
                  try
                  {
                        master.WriteSingleCoil(slaveAddr, startAddr, blVal);
                        MessageBox.Show("【单线圈】写入成功!");
                  }
                  catch (Exception ex)
                  {
                        MessageBox.Show(ex.Message);
                  }
                  break;
                case "写单保持寄存器":
                  ushort uVal01 = ushort.Parse(vals);
                  try
                  {
                        master.WriteSingleRegister(slaveAddr, startAddr, uVal01);
                        MessageBox.Show("【单保持寄存器】写入成功!");
                  }
                  catch (Exception ex)
                  {
                        MessageBox.Show(ex.Message);
                  }
                  break;
                case "写多线圈":
                  bool[] blVals = vals.Split(',').Select(s => s == "1" ? true : false).ToArray();//bool数组
                  try
                  {
                        master.WriteMultipleCoils(slaveAddr, startAddr, blVals);
                        MessageBox.Show("【多线圈】写入成功!");
                  }
                  catch (Exception ex)
                  {
                        MessageBox.Show(ex.Message);
                  }
                  break;
                case "写多保持寄存器":
                  try
                  {
                        //功能码
                        //string dataType = cmddatatype2.Text.Trim();
                        switch (dataType)
                        {
                            case "ushort":
                              情况1:写入无符号的整数,即写入ushort数据,如写入33,44
                              ushort[] uVals01 = vals.Split(',').Select(s => ushort.Parse(s)).ToArray();
                              master.WriteMultipleRegisters(startAddr, uVals01);
                              break;
                            case "short":
                              //情况2:写入有符号的整数,即写入short数据,如写入-133,-65,98等,处理方法是:short[]=>byte[]=>ushort[],情况2包括了情况1
                              short[] uVals02 = vals.Split(',').Select(s => short.Parse(s)).ToArray();
                              byte[] y2 = ByteArrayLib.GetByteArrayFromShortArray(uVals02);
                              ushort[] ushorts2 = UShortLib.GetUShortArrayFromByteArray(y2);
                              master.WriteMultipleRegisters(startAddr, ushorts2);
                              MessageBox.Show("【short类型数据】写入成功!");
                              break;
                            case "float":
                              //情况3:写入有符号的小数,即写入float数据,如写入-6.3,-2.65,56.893,51,-465等,处理方法是:float[]=>byte[]=>ushort[],情况3包括了情况2和情况1
                              float[] uVals03 = vals.Split(',').Select(s => float.Parse(s)).ToArray();
                              byte[] y3 = ByteArrayLib.GetByteArrayFromFloatArray(uVals03);
                              ushort[] ushorts3 = UShortLib.GetUShortArrayFromByteArray(y3);
                              master.WriteMultipleRegisters(startAddr, ushorts3);
                              MessageBox.Show("【float类型数据】写入成功!");
                              break;
                        }



                        情况2:写入有符号的整数,即写入short数据,如写入-133,-65,98等,处理方法是:short[]=>byte[]=>ushort[],情况2包括了情况1
                        //short[] uVals02 = vals.Split(',').Select(s => short.Parse(s)).ToArray();
                        //byte[] y = ByteArrayLib.GetByteArrayFromShortArray(uVals02);
                        //ushort[] ushorts = UShortLib.GetUShortArrayFromByteArray(y);
                        //master.WriteMultipleRegisters(slaveAddr, startAddr, ushorts);

                        情况3:写入有符号的小数,即写入float数据,如写入-6.3,-2.65,56.893,51,-465等,处理方法是:float[]=>byte[]=>ushort[],情况3包括了情况2和情况1
                        //float[] uVals02 = vals.Split(',').Select(s => float.Parse(s)).ToArray();
                        //byte[] y = ByteArrayLib.GetByteArrayFromFloatArray(uVals02);
                        //ushort[] ushorts = UShortLib.GetUShortArrayFromByteArray(y);
                        //master.WriteMultipleRegisters(slaveAddr, startAddr, ushorts);

                        MessageBox.Show("【多保持寄存器】写入成功!");
                  }
                  catch (Exception ex)
                  {
                        MessageBox.Show(ex.Message);
                  }
                  break;
            }
      } https://i-blog.csdnimg.cn/blog_migrate/9d2095a1cf7688e17558890b56bff44d.png
 写入成功,同时读取的也是刚才写的值,在博途的监控表中看到
https://i-blog.csdnimg.cn/blog_migrate/879081c572c659fd344ae7f010248d44.png
10、写入的代码--float范例

https://i-blog.csdnimg.cn/blog_migrate/f7a1ae0794b781f29f9c8cf45af29f8b.png
 https://i-blog.csdnimg.cn/blog_migrate/954d97c7d356f2d3c52714d3b329b01d.png
写入负数
https://i-blog.csdnimg.cn/blog_migrate/130c98ab4c190d61ff06c67e41c67631.png
https://i-blog.csdnimg.cn/blog_migrate/7d413f848df4ecd36f6e495c1ef70d87.png
 我们向“摩头2温度”这个寄存器写入数据
https://i-blog.csdnimg.cn/blog_migrate/8a90a1283474678d0dc7c9396d1a22f2.png
再看博途中的数据
 https://i-blog.csdnimg.cn/blog_migrate/013ab8d9cd247ccf1d805e85eafe83eb.png


11、小结

客户端创建tcp client对象,然后modbus利用tcp对象创建modbus通讯,然后通过不同数据范例读写PLC数据,成功了
代码链接:
链接:https://pan.baidu.com/s/1aCqv3eSX-7SXAdGtrGNpTw 
提取码:kyqo  
https://i-blog.csdnimg.cn/blog_migrate/996eb8c7f06e5133a927e7ab117fb4a5.gif


https://i-blog.csdnimg.cn/blog_migrate/d8c87abc88191d1407e16ad71659088e.gif


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: C#与西门子PLC1500的ModbusTcp服务器通讯4--搭建ModbusTcp客户端