让代码更简单

Modbus TCP报文详解附C#demo

重要:本文最后更新于2023-05-25 19:47:54,某些文章具有时效性,若有错误或已失效,请在下方留言或联系代码狗

Modbus TCP是一种基于TCP/IP的工业通信协议,广泛应用于自动化设备、工业控制系统等领域。相较于基于串行通信的Modbus RTU和Modbus ASCII协议,Modbus TCP具有更高的通信速率和更强的抗干扰能力,因此在现代工业通信系统中具有广泛的应用前景。

一、Modbus TCP报文结构

一个完整的Modbus TCP报文包含以下几个部分:

  1. 事务标识符(Transaction Identifier,2字节):用于标识请求报文和响应报文之间的关联,通常由客户端生成,服务器端在响应报文中原样返回。
  2. 协议标识符(Protocol Identifier,2字节):用于区分不同的协议,对于Modbus TCP协议,该字段的值固定为0x0000。
  3. 长度字段(Length,2字节):表示后续报文中的字节数,包括单元标识符、功能码和数据字段。
  4. 单元标识符(Unit Identifier,1字节):用于标识Modbus设备的地址,取值范围为1-247。在Modbus TCP协议中,该字段的作用较小,通常设为0xFF。
  5. 功能码(Function Code,1字节):用于指示报文的操作类型,例如读取寄存器、写入寄存器等。功能码的取值范围为1-255,但实际应用中常用的功能码有限。
  6. 数据字段(Data,不定长度):根据功能码的不同,该字段用于携带不同的数据信息,例如寄存器地址、寄存器值等。

二、功能码与数据编码

常用的Modbus TCP功能码及其对应的操作如下:

  1. 读取线圈状态(Read Coils,功能码0x01):读取1个或多个线圈的状态(开/关)。
  2. 读取离散输入状态(Read Discrete Inputs,功能码0x02):读取1个或多个离散输入的状态(开/关)。
  3. 读取保持寄存器(Read Holding Registers,功能码0x03):读取1个或多个保持寄存器的值。
  4. 读取输入寄存器(Read Input Registers,功能码0x04):读取1个或多个输入寄存器的值。
  5. 写单个线圈(Write Single Coil,功能码0x05):设置1个线圈的状态(开/关)。
  6. 写单个保持寄存器(Write Single Register,功能码0x06):设置1个保持寄存器的值。
  7. 写多个线圈(Write Multiple Coils,功能码0x0F):设置多个线圈的状态(开/关)。
  8. 写多个保持寄存器(Write Multiple Registers,功能码0x10):设置多个保持寄存器的值。

数据编码主要涉及寄存器地址、寄存器值、线圈状态等信息。在Modbus TCP协议中,寄存器地址和寄存器值均采用2字节(16位)的无符号整数表示,线圈状态则用1字节表示,其中0xFF表示开,0x00表示关。

三、报文示例

假设我们需要读取一个Modbus设备的保持寄存器,寄存器地址为0x1000,读取长度为2个寄存器。根据上述介绍,我们可以生成如下请求报文:

事务标识符:0x0001(假设为1)

协议标识符:0x0000

长度字段:0x0006(后续报文共6字节)

单元标识符:0xFF

功能码:0x03(读取保持寄存器)

数据字段:0x1000 0x0002(寄存器地址为0x1000,读取长度为2)

将上述字段组合在一起,得到完整的请求报文:0x0001 0x0000 0x0006 0xFF 0x03 0x1000 0x0002。

设备收到请求报文后,将返回如下响应报文:

事务标识符:0x0001(与请求报文相同)

协议标识符:0x0000

长度字段:0x0007(后续报文共7字节)

单元标识符:0xFF

功能码:0x03(与请求报文相同)

数据长度:0x04(返回数据共4字节)

数据字段:0x1234 0x5678(假设保持寄存器的值分别为0x1234和0x5678)

将上述字段组合在一起,得到完整的响应报文:0x0001 0x0000 0x0007 0xFF 0x03 0x04 0x1234 0x5678。

四、报文解析

在收到响应报文后,我们需要对其进行解析以获取所需的寄存器数据。解析步骤如下:

  1. 验证事务标识符:将响应报文中的事务标识符(0x0001)与请求报文中的事务标识符进行比较,确保它们相同。
  2. 验证协议标识符:检查响应报文中的协议标识符(0x0000)是否为Modbus TCP协议的固定值。
  3. 验证长度字段:检查响应报文中的长度字段(0x0007)是否与实际报文长度相符。
  4. 验证单元标识符:将响应报文中的单元标识符(0xFF)与请求报文中的单元标识符进行比较,确保它们相同。
  5. 验证功能码:将响应报文中的功能码(0x03)与请求报文中的功能码进行比较,确保它们相同。
  6. 解析数据长度:从响应报文中读取数据长度字段(0x04),确认返回数据的字节数。
  7. 提取数据字段:根据数据长度字段,提取响应报文中的数据字段(0x1234 0x5678),并按照寄存器地址顺序解析为对应的寄存器值。

通过上述解析步骤,我们成功获取了目标设备的保持寄存器数据,分别为0x1234和0x5678。

五、C#示例代码

使用C#封装一个类,用于构造和解析报文。

复制
using System;
using System.Net.Sockets;
using System.Text;

namespace ModbusTcpClient
{
    class ModbusTcp
    {
        private TcpClient _tcpClient;
        private NetworkStream _networkStream;

        public ModbusTcp(string ipAddress, int port)
        {
            _tcpClient = new TcpClient(ipAddress, port);
            _networkStream = _tcpClient.GetStream();
        }

        public void CloseConnection()
        {
            _networkStream.Close();
            _tcpClient.Close();
        }

        public byte[] CreateRequest(ushort transactionId, byte unitId, byte functionCode, ushort registerAddress, ushort registerCount)
        {
            byte[] request = new byte[12];

            // Transaction Identifier
            byte[] transactionIdBytes = BitConverter.GetBytes(transactionId);
            request[0] = transactionIdBytes[1];
            request[1] = transactionIdBytes[0];

            // Protocol Identifier
            request[2] = 0x00;
            request[3] = 0x00;

            // Length
            request[4] = 0x00;
            request[5] = 0x06;

            // Unit Identifier
            request[6] = unitId;

            // Function Code
            request[7] = functionCode;

            // Register Address
            byte[] registerAddressBytes = BitConverter.GetBytes(registerAddress);
            request[8] = registerAddressBytes[1];
            request[9] = registerAddressBytes[0];

            // Register Count
            byte[] registerCountBytes = BitConverter.GetBytes(registerCount);
            request[10] = registerCountBytes[1];
            request[11] = registerCountBytes[0];

            return request;
        }

        public void SendRequest(byte[] request)
        {
            _networkStream.Write(request, 0, request.Length);
        }

        public byte[] ReceiveResponse()
        {
            byte[] response = new byte[256];
            int bytesRead = _networkStream.Read(response, 0, response.Length);
            Array.Resize(ref response, bytesRead);
            return response;
        }

        public ushort[] ParseResponse(byte[] response)
        {
            int dataLength = response[8];
            ushort[] registerValues = new ushort[dataLength / 2];

            for (int i = 0; i < dataLength / 2; i++)
            {
                registerValues[i] = BitConverter.ToUInt16(new byte[] { response[9 + i * 2], response[10 + i * 2] }, 0);
            }

            return registerValues;
        }
    }
}

使用这个类,你可以像下面这样创建一个Modbus TCP客户端,发送请求报文并解析响应报文:

复制
using System;

namespace ModbusTcpClient
{
    class Program
    {
        static void Main(string[] args)
        {
            ModbusTcp modbusTcpClient = new ModbusTcp("192.168.1.1", 502);

            byte[] request = modbusTcpClient.CreateRequest(0x0001, 0xFF, 0x03, 0x0000, 0x0002);
            modbusTcpClient.SendRequest(request);

            byte[] response = modbusTcpClient.ReceiveResponse();
            ushort[] registerValues = modbusTcpClient.ParseResponse(response);

            Console.WriteLine("Register values:");
            foreach (ushort value in registerValues)
            {
                Console.WriteLine(value);
            }

            modbusTcpClient.CloseConnection();
        }
    }
}

感觉很棒!可以赞赏支持我哟~

1 打赏

评论 (0)

登录后评论
QQ咨询 邮件咨询 狗哥推荐