zoukankan      html  css  js  c++  java
  • C#ModBus Tcp的学习及Master的实现

    Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。

    所以这也是我们工控领域软件开发的所必懂的通讯协议,我也是初次学习,先贴上我的学习笔记

    一 .协议概述

    (1)Modbus协议是应用于控制器上的一种通用语言,实现控制器之间,控制器通过网络和其他设备之间的通信,支持传统RS232/RS422/RS485和以太网设备,它已经成为一种通用的工业标准,有了它不同厂商生产的控制设备可以连成工业网络,进行集中控制,此协议定义了一个控制器能认识使用的消息结构

    (2) 如果按照国际 ISO/OSI 的 7 层网络模型来说,标准 MODBUS 协议定义了通信物理层、链路层及应用层;

    物理层:定义了基于 RS232 和 RS485 的异步串行通信规范;

    链路层:规定了基于站号识别、主 / 从方式的介质访问控制;

    应用层:规定了信息规范(或报文格式)及通信服务功能;

    二. 协议要点

    (1) MODBUS 是主 / 从通信协议。主站主动发送报文 , 只有与主站发送报文中呼叫地址相同的从站才向主站发送回答报文。

    (2) 报文以 0 地址发送时为广播模式,无需从站应答,可作为广播报文发送,包括:

      ①修改线圈状态;

      ②修改寄存器内容;

      ③强置多线圈;

      ④预置多寄存器;

      ⑤询问诊断;

    (3) MODBUS 规定了 2 种字符传输模式: ASCII 模式、 RTU (二进制)模式;两种传输模式不能混用;

    (4) 传输错误校验

      传输错误校验有奇偶校验、冗余校验检验。

      当校验出错时,报文处理停止,从机不再继续通信,不对此报文产生应答;

      通信错误一旦发生,报文便被视为不可靠; MODBUS 主机在一定时间过后仍未收到从站应答,即作出“通信错误已发生”的判断。

    (5) 报文级(字符级)采用 CRC-16 (循环冗余错误校验)

    (6) MODBUS 报文 RTU 格式

    三. 异常应答

    (1) 从机接收到的主机报文,没有传输错误,但从机无法正确执行主机命令或无法作出正确应答,从机将以“异常应答”回答之。

    (2) 异常应答报文格式

    例:主机发请求报文,功能码 01 :读 1 个 04A1 线圈值

      由于从机最高线圈地址为 0400 ,则 04A1 超地址上限,从机作出异常应答如下(注意:功能码最高位置 1 ):

    (3)异常应答码

    四. 寄存器和功能码

    modbus的功能码很多,且不同功能码对应的报文也不一致,后续博客我会借用开源库实现一个modbus master 测试功能码 解析报文

    下边我用表格总结一下寄存器,功能码,报文格式

    注:

    (1)报文中的所有字节均为16进制

    (2)由上图我们总结出不同的功能码的报文(无论询问报文还是响应报文)前8个字节都是一致的 都是2字节消息号+2字节ModBus标识+2字节长度+1字节站号+1字节功能码 后边根据功能码不同而不同

    (3)报文中,指定线圈通断标志  FF00 置线圈为ON  0000置线圈为OFF

    五.具体实现

    接下来我们使用开源库NModbus库,来实现一个Modbus master

    创建工程,从NuGet管理器安装NModbusu

    先简单介绍一下NModbus中的几个重要方法

    接下来做具体实现

      1 using System;
      2 using System.Collections.Generic;
      3 using System.ComponentModel;
      4 using System.Data;
      5 using System.Drawing;
      6 using System.Linq;
      7 using System.Text;
      8 using System.Threading.Tasks;
      9 using System.Windows.Forms;
     10 using NModbus;
     11 using System.Net.Sockets;
     12 using System.Threading;
     13 
     14 namespace ModbusTcp
     15 {
     16     public partial class Form1 : Form
     17     {
     18 
     19         private static ModbusFactory modbusFactory;
     20         private static IModbusMaster master;
     21         //写线圈或写寄存器数组
     22         bool[] coilsBuffer;
     23         ushort[] registerBuffer;
     24         //功能码
     25         string functionCode;
     26         //参数(分别为站号,起始地址,长度)
     27         byte slaveAddress;
     28         ushort startAddress;
     29         ushort numberOfPoints;
     30 
     31         public Form1()
     32         {
     33             InitializeComponent();
     34 
     35         }
     36         private void Form1_Load(object sender, EventArgs e)
     37         {
     38             //初始化modbusmaster
     39             modbusFactory = new ModbusFactory();
     40             //在本地测试 所以使用回环地址,modbus协议规定端口号 502
     41             master = modbusFactory.CreateMaster(new TcpClient("127.0.0.1", 502));
     42             //设置读取超时时间
     43             master.Transport.ReadTimeout = 2000;
     44             master.Transport.Retries = 2000;
     45             groupBox1.Enabled = false;
     46             groupBox2.Enabled = false;
     47         }
     48         /// <summary>
     49         /// 读/写
     50         /// </summary>
     51         /// <param name="sender"></param>
     52         /// <param name="e"></param>
     53         private void button1_Click(object sender, EventArgs e)
     54         {
     55             ExecuteFunction();
     56         }
     57 
     58         private async void ExecuteFunction()
     59         {
     60             try
     61             {
     62                 //重新实例化是为了 modbus slave更换连接时不报错 
     63                 master = modbusFactory.CreateMaster(new TcpClient("127.0.0.1", 502));
     64                 if (functionCode != null)
     65                 {
     66                     switch (functionCode)
     67                     {
     68                         case "01 Read Coils"://读取单个线圈
     69                             SetReadParameters();
     70                             coilsBuffer = master.ReadCoils(slaveAddress, startAddress, numberOfPoints);
     71 
     72                             for (int i = 0; i < coilsBuffer.Length; i++)
     73                             {
     74                                 SetMsg(coilsBuffer[i] + "");
     75                             }
     76                             break;
     77                         case "02 Read DisCrete Inputs"://读取输入线圈/离散量线圈
     78                             SetReadParameters();
     79 
     80                             coilsBuffer = master.ReadInputs(slaveAddress, startAddress, numberOfPoints);
     81                             for (int i = 0; i < coilsBuffer.Length; i++)
     82                             {
     83                                 SetMsg(coilsBuffer[i] + "");
     84                             }
     85                             break;
     86                         case "03 Read Holding Registers"://读取保持寄存器
     87                             SetReadParameters();
     88                             registerBuffer = master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints);
     89                             for (int i = 0; i < registerBuffer.Length; i++)
     90                             {
     91                                 SetMsg(registerBuffer[i] + "");
     92                             }
     93                             break;
     94                         case "04 Read Input Registers"://读取输入寄存器
     95                             SetReadParameters();
     96                             registerBuffer = master.ReadInputRegisters(slaveAddress, startAddress, numberOfPoints);
     97                             for (int i = 0; i < registerBuffer.Length; i++)
     98                             {
     99                                 SetMsg(registerBuffer[i] + "");
    100                             }
    101                             break;
    102                         case "05 Write Single Coil"://写单个线圈
    103                             SetWriteParametes();
    104                             await master.WriteSingleCoilAsync(slaveAddress, startAddress, coilsBuffer[0]);
    105                             break;
    106                         case "06 Write Single Registers"://写单个输入线圈/离散量线圈
    107                             SetWriteParametes();
    108                             await master.WriteSingleRegisterAsync(slaveAddress, startAddress, registerBuffer[0]);
    109                             break;
    110                         case "0F Write Multiple Coils"://写一组线圈
    111                             SetWriteParametes();
    112                             await master.WriteMultipleCoilsAsync(slaveAddress, startAddress, coilsBuffer);
    113                             break;
    114                         case "10 Write Multiple Registers"://写一组保持寄存器
    115                             SetWriteParametes();
    116                             await master.WriteMultipleRegistersAsync(slaveAddress, startAddress, registerBuffer);
    117                             break;
    118                         default:
    119                             break;
    120                     }
    121 
    122                 }
    123                 else
    124                 {
    125                     MessageBox.Show("请选择功能码!");
    126                 }
    127                master.Dispose();
    128             }
    129             catch (Exception ex)
    130             {
    131 
    132                 MessageBox.Show(ex.Message);
    133             }
    134         }
    135         private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
    136         {
    137             if (comboBox1.SelectedIndex >= 4)
    138             {
    139                 groupBox2.Enabled = true;
    140                 groupBox1.Enabled = false;
    141             }
    142             else
    143             {
    144                 groupBox1.Enabled = true;
    145                 groupBox2.Enabled = false;
    146             }
    147             comboBox1.Invoke(new Action(() => { functionCode = comboBox1.SelectedItem.ToString(); }));
    148         }
    149 
    150         /// <summary>
    151         /// 初始化读参数
    152         /// </summary>
    153         private void SetReadParameters()
    154         {
    155             if (txt_startAddr1.Text == "" || txt_slave1.Text == "" || txt_length.Text == "")
    156             {
    157                 MessageBox.Show("请填写读参数!");
    158             }
    159             else
    160             {
    161                 slaveAddress = byte.Parse(txt_slave1.Text);
    162                 startAddress = ushort.Parse(txt_startAddr1.Text);
    163                 numberOfPoints = ushort.Parse(txt_length.Text);
    164             }
    165         }
    166         /// <summary>
    167         /// 初始化写参数
    168         /// </summary>
    169         private void SetWriteParametes()
    170         {
    171             if (txt_startAddr2.Text == "" || txt_slave2.Text == "" || txt_data.Text == "")
    172             {
    173                 MessageBox.Show("请填写写参数!");
    174             }
    175             else
    176             {
    177                 slaveAddress = byte.Parse(txt_slave2.Text);
    178                 startAddress = ushort.Parse(txt_startAddr2.Text);
    179                 //判断是否写线圈
    180                 if (comboBox1.SelectedIndex == 4 || comboBox1.SelectedIndex == 6)
    181                 {
    182                     string[] strarr = txt_data.Text.Split(' ');
    183                     coilsBuffer = new bool[strarr.Length];
    184                     //转化为bool数组
    185                     for (int i = 0; i < strarr.Length; i++)
    186                     {
    187                         // strarr[i] == "0" ? coilsBuffer[i] = true : coilsBuffer[i] = false;
    188                         if (strarr[i] == "0")
    189                         {
    190                             coilsBuffer[i] = false;
    191                         }
    192                         else
    193                         {
    194                             coilsBuffer[i] = true;
    195                         }
    196                     }
    197                 }
    198                 else
    199                 {
    200                     //转化ushort数组
    201                     string[] strarr = txt_data.Text.Split(' ');
    202                     registerBuffer = new ushort[strarr.Length];
    203                     for (int i = 0; i < strarr.Length; i++)
    204                     {
    205                         registerBuffer[i] = ushort.Parse(strarr[i]);
    206                     }
    207                 }
    208             }
    209         }
    210         /// <summary>
    211         /// 清除文本
    212         /// </summary>
    213         /// <param name="sender"></param>
    214         /// <param name="e"></param>
    215         private void button2_Click(object sender, EventArgs e)
    216         {
    217             richTextBox1.Clear();
    218         }
    219         /// <summary>
    220         /// SetMessage
    221         /// </summary>
    222         /// <param name="msg"></param>
    223         public void SetMsg(string msg)
    224         {
    225             richTextBox1.Invoke(new Action(() => { richTextBox1.AppendText(msg + "
    "); }));
    226         }
    227 
    228     }
    229 }
    View Code

    界面布局

    六 功能测试及报文解析

    这里功能测试我们需要借助测试工具  Modbus Slave(Modbus从站客户端)

    链接:https://pan.baidu.com/s/1Z3bET3l_2a4e6cu_p250tg
    提取码:hq1r

    简单说明一下,这里我实现了常用的几个功能码

    0x01 读一组线圈

    0x02 读一组输入线圈/离散量线圈

    0x03 读一组保持寄存器

    0x04 读一组输入寄存器

    0x05 写单个线圈

    0x06 写单个保持寄存器

    0x0F 写多个线圈

    0x10 写多个保持寄存器

    简单说一下Modbus Slave 的操作

    打开连接,建立连接,选择连接方式为Tcp/Ip 设置 Ip和端口号

    选择线圈或寄存器

    点击Setup->Slave Definition,这里的Function我们需要读/写什么线圈或寄存器就对应选择

    测试1 功能码0x01

    这里我们所有的测试从站都使用站号1 起始地址0 长度10

    功能码0x01 读取线圈 Modbus Slave的Function选择01 Coil Status(0x)

    测试结果:

    点击Display->Communication 可以截取报文,我也不知道为什么他报文字体那么小(绝望ing)

    000000-Rx:00 01 00 00 00 06 01 01 00 00 00 05
    000001-Tx:00 01 00 00 00 04 01 01 01 06

    测试2  功能码0x10

    功能码0x10 写入一组数据到保持寄存器  Modbus Slave的Function选择03 Holding Register(4x) (说明一下 线圈和保持寄存器才有写操作)

    测试结果

     报文

    000070-Rx:00 01 00 00 00 11 01 10 00 00 00 05 0A 00 0C 00 22 00 38 00 4E 00 5A
    000071-Tx:00 01 00 00 00 06 01 10 00 00 00 05

    上文测试了一个读操作和一个写操作,其他功能码的测试与上文一致,有兴趣的可以自行测试,

    下一篇博客我要针对不同的功能码做对应的报文解析

    程序源码:

    链接:https://pan.baidu.com/s/1mPAhRixLbsDb7h2ePENTRA
    提取码:b5w6

    以上都为我自己学习总结并实现,有错误之处,希望大家不吝赐教,感谢(抱拳)!

  • 相关阅读:
    组合模式
    迭代器模式
    命令模式
    装饰者模式
    观察者模式
    策略模式
    适配器模式和外观模式
    Servlet
    Java 递归
    Java 反射
  • 原文地址:https://www.cnblogs.com/pandefu/p/10824331.html
Copyright © 2011-2022 走看看