ModbusTcp协议
一、基本概念
Modbus TCP 是一种基于 Modbus 协议的以太网通信协议。它是 Modbus 协议在 TCP/IP 网络上的实现,保留了 Modbus 协议的核心功能和数据模型,同时利用了 TCP/IP 协议的传输能力,使得设备之间可以通过以太网进行通信。
二、通信架构
采用客户端 - 服务器(Client - Server)模式。服务器端通常是数据采集设备或从设备(Slave),它在 IANA(Internet Assigned Numbers Authority,互联网号码分配当局)分配的 TCP 端口 502 上进行监听,等待客户端的连接请求。客户端一般是主控设备(Master),主动向服务器端发起连接并发送请求,服务器端接收到请求后进行处理并返回响应结果
三、数据存储区
规定了 4 个主要的存储区,这些存储区用于存储不同类型的数据:
-
离散输入(Discrete Inputs)存储区(1 区) :用于存储从设备的数字输入点状态,只读。例如,一个传感器的状态信号可以通过离散输入存储区进行采集,客户端只能读取这些状态信息,不能对它们进行修改。
-
线圈(Coils)存储区(0 区) :可读可写的布尔量存储区。它通常用于控制输出设备,比如继电器线圈。客户端可以向线圈存储区写入数据(数据值为 0 或 1),从而控制对应的输出设备的通断;也可以读取线圈的当前状态。
-
输入寄存器(Input Registers)存储区(3 区) :用于存储模拟量输入或其他只读数据,只读。例如,一个温度传感器采集的温度值可能存储在输入寄存器中,客户端可以读取这些数据以了解当前温度状态,但不能修改它。
-
保持寄存器(Holding Registers)存储区(4 区) :可读可写的寄存器存储区。它可用于存储参数设置、控制命令等需要读写操作的数据。客户端可以根据需要对保持寄存器中的数据进行读取和修改。
四、报文结构
Modbus TCP 报文基于 TCP/IP 协议,其结构与 Modbus RTU 报文有所不同。一个完整的 Modbus TCP 报文包括以下部分:
-
事务标识符(Transaction Identifier) :由两个字节组成,用于标识一个请求 - 响应事务。客户端向服务器发送请求时,会给这个标识符赋一个唯一的值;服务器在响应时,将使用相同的值,以便客户端可以关联请求和响应。
-
协议标识符(Protocol Identifier) :两个字节,取值为 0,用于指定协议类型为 Modbus TCP/IP。
-
长度字段(Length) :两个字节,表示后续字节的长度(即从单元标识符到功能码和数据的总字节长度),以便接收方可以正确解析报文。
-
单元标识符(Unit Identifier) :一个字节,用于在网络上有多个从设备时标识目标从设备的地址(1 - 247)。在简单的单主单从通信中,该值通常为 0,表示不指定特定的从设备(因为通信只涉及一个从设备)。
-
功能码(Function Code) :一个字节,表示客户端请求服务器执行的具体操作类型,例如读取寄存器、写入寄存器等。功能码的值决定了服务器端如何处理请求以及返回哪种类型的响应。
-
数据(Data) :用于包含请求或响应的具体数据,其内容根据功能码的不同而有所变化。例如,在读取寄存器的功能码请求中,数据部分包含要读取的寄存器起始地址和数量;在写入寄存器的请求中,数据部分包含要写入的寄存器地址和数据值。
-
错误检查 :与 Modbus RTU 使用的 CRC - 16 错误检查不同,Modbus TCP 凭借 TCP/IP 协议本身提供的可靠性传输机制,不需要额外的错误校验字段。
Modbus TCP 功能码详解
一、常见功能码概述
功能码是 Modbus TCP 协议中的核心指令,用于指定客户端希望服务器执行的操作。以下是一些常用的功能码:
(一)0x01(功能码 1):读线圈状态
-
用途 :用于读取离散输入存储区中的输入状态,这些状态通常是表示设备的输入点是否被激活(如开关状态、传感器报警等)。
-
请求报文结构 :
-
功能码(Function Code) :0x01。
-
起始地址(Starting Address) :两个字节,指定要读取的第一个线圈的地址。例如,地址 0x0000 表示从第一个线圈开始读取。
-
读取数量(Quantity of Coils) :两个字节,指定要读取的线圈数量,取值范围为 1 - 2000。例如,读取 10 个线圈,则数量为 10。
响应报文结构 :
-
功能码(Function Code) :0x01。
-
字节计数(Byte Count) :一个字节,表示接下来数据部分的字节数。例如,如果读取 10 个线圈,由于每个线圈占用 1 位,10 位需要 2 个字节(一个字节 8 位)来存储,字节计数为 2。
-
线圈数据(Coil Data) :若干字节,每个字节包含 8 个线圈的状态,每个线圈状态用 1 位表示(0 表示 OFF,1 表示 ON)。例如,数据为 0x0F(二进制 00001111)表示该字节对应的最后 4 个线圈为 ON,前面 4 个线圈为 OFF。
(二)0x02(功能码 2):读离散输入状态
-
用途 :与读线圈状态类似,但读取的是离散输入存储区中的输入状态。离散输入通常用于采集外部设备的数字信号,如按钮按下状态、开关位置等。
-
请求报文结构 :与功能码 0x01 的请求报文结构相同,也是包含功能码、起始地址和读取数量。
-
响应报文结构 :与功能码 0x01 的响应报文结构相似,包含功能码、字节计数和离散输入数据(每个字节包含 8 个离散输入状态位)。
(三)0x03(功能码 3):读保持寄存器
-
用途 :用于读取保持寄存器中的数据。保持寄存器可以存储参数设置、计算结果等需要读写的数据,并且这些数据在设备断电后通常会保持其值(具体取决于设备实现)。
-
请求报文结构 :
-
功能码(Function Code) :0x03。
-
起始地址(Starting Address) :两个字节,指定要读取的第一个保持寄存器的地址。例如,地址 0x0004 表示从第 5 个保持寄存器开始读取(地址从 0 开始计数)。
-
读取数量(Quantity of Registers) :两个字节,指定要读取的保持寄存器数量,取值范围为 1 - 125。例如,读取 5 个寄存器。
-
响应报文结构 :
-
功能码(Function Code) :0x03。
-
字节计数(Byte Count) :一个字节,表示接下来数据部分的字节数。每个保持寄存器为 16 位(即 2 个字节),所以字节计数 = 读取数量 × 2。例如,读取 5 个寄存器,字节计数为 10。
-
寄存器数据(Register Data) :若干字节,按顺序包含读取到的保持寄存器的值。例如,寄存器数据为 0x000A 0x000B 0x000C(假设读取 3 个寄存器),表示第一个寄存器值为 0x000A,第二个为 0x000B,第三个为 0x000C。
(四)0x04(功能码 4):读输入寄存器
-
用途 :用于读取输入寄存器中的数据。输入寄存器通常用于存储模拟量输入或其他只读数据,如传感器采集的温度、压力等值。这些数据一般只能读取,不能被客户端修改。
-
请求报文结构 :与功能码 0x03 的请求报文结构一致,包括功能码、起始地址和读取数量。
-
响应报文结构 :与功能码 0x03 的响应报文结构类似,包含功能码、字节计数和输入寄存器数据。每个输入寄存器为 16 位(2 字节),数据顺序与寄存器地址顺序一致。
(五)0x05(功能码 5):写单个线圈
-
用途 :用于向线圈存储区写入单个线圈的状态,以控制输出设备的通断,例如启动或停止电机、控制继电器等。
-
请求报文结构 :
-
功能码(Function Code) :0x05。
-
输出地址(Output Address) :两个字节,指定要写入的线圈的地址。例如,地址 0x0001 表示要写入第二个线圈(地址从 0 开始计数)。
-
输出值(Output Value) :两个字节,表示要写入的线圈状态值,FF 00 表示 ON(线圈激活),00 00 表示 OFF(线圈未激活)。例如,将线圈状态设为 ON,则输出值为 FF 00。
- 响应报文结构 :
-
功能码(Function Code) :0x05。
-
输出地址(Output Address) :与请求中的输出地址相同,用于确认被写入的线圈地址。
-
输出值(Output Value) :与请求中的输出值相同,用于确认写入的线圈状态值。
(六)0x06(功能码 6):写单个保持寄存器
-
用途 :用于向保持寄存器存储区写入单个保持寄存器的值。这可以用于设置设备的参数、发送控制命令等操作,例如设置电机的转速、设定温度控制器的目标温度等。
-
请求报文结构 :
-
功能码(Function Code) :0x06。
-
寄存器地址(Register Address) :两个字节,指定要写入的保持寄存器的地址。例如,地址 0x0002 表示第三个保持寄存器。
-
寄存器值(Register Value) :两个字节,表示要写入的保持寄存器的数值。例如,写入值 0x001A。
-
响应报文结构 :
-
功能码(Function Code) :0x06。
-
寄存器地址(Register Address) :与请求中的寄存器地址相同,用于确认被写入的寄存器地址。
-
寄存器值(Register Value) :与请求中的寄存器值相同,用于确认写入的寄存器值。
二、其他功能码简述
除了上述常用功能码,还有其他一些功能码用于特定的操作:
-
0x07(功能码 7):读异常状态 :用于读取设备的异常状态信息,帮助诊断设备是否出现故障或异常情况。
-
0x0F(功能码 15):写多个线圈 :客户端可以使用此功能码同时写入多个线圈的状态,适用于批量控制多个输出设备的场合。请求报文包含起始地址、线圈数量和一个字节数组(每个字节包含 8 个线圈状态位),响应报文用于确认写入操作的起始地址和线圈数量。
-
0x10(功能码 16):写多个保持寄存器 :用于向保持寄存器存储区写入多个保持寄存器的值。请求报文包括起始地址、寄存器数量和一个字节数组(每个寄存器值占 2 字节),响应报文用于确认写入操作的起始地址和寄存器数量。
-
0x11(功能码 17):报告从机 ID :客户端发送此请求时,服务器将返回其设备的标识信息,包括设备的唯一 ID、设备信息等,用于设备识别和管理。
Modbus TCP 在 C# 中的应用示例
一、使用 NModbus4 库
(一)安装库
在 Visual Studio 中打开 “解决方案资源管理器”,右键点击项目,选择 “管理 NuGet 包”,搜索 NModbus4 并安装。安装完成后,项目中就包含了 NModbus4 库的引用,可以使用其中的 Modbus 相关类和方法。
(二)创建客户端
引入 using Modbus.Device; 命名空间,使用 ModbusIpMaster 类创建 Modbus TCP 客户端实例,并指定服务器的 IP 地址和端口。例如:
using System;
using Modbus.Device;
using System.Net.Sockets;class Program
{static void Main(string[] args){// 创建 TCP 客户端TcpClient tcpClient = new TcpClient();tcpClient.Connect("192.168.1.100", 502); // 假设服务器 IP 地址为 192.168.1.100,端口为 502// 创建 Modbus TCP 客户端实例IModbusSerialMaster modbusMaster = ModbusIpMaster.CreateIp(tcpClient);}
}
(三)读写操作示例
1. 读保持寄存器
使用 ReadHoldingRegisters 方法读取保持寄存器的值。该方法需要两个参数:起始地址和要读取的寄存器数量。例如,读取从地址 0 开始的 5 个保持寄存器:
try
{// 读取保持寄存器,起始地址为 0,读取 5 个寄存器ushort[] registers = modbusMaster.ReadHoldingRegisters(0, 5);Console.WriteLine("保持寄存器的值:");foreach (ushort register in registers){Console.WriteLine(register);}
}
catch (Exception ex)
{Console.WriteLine("读取保持寄存器出错:" + ex.Message);
}
2. 写单个保持寄存器
使用 WriteSingleRegister 方法向单个保持寄存器写入值。需要指定寄存器地址和要写入的值。例如,向地址为 2 的保持寄存器写入值 0x001A:
try
{modbusMaster.WriteSingleRegister(2, 0x001A);Console.WriteLine("成功向保持寄存器地址 2 写入值 0x001A");
}
catch (Exception ex)
{Console.WriteLine("写入保持寄存器出错:" + ex.Message);
}
3. 读输入寄存器
使用 ReadInputRegisters 方法读取输入寄存器的值。同样需要起始地址和读取数量参数。例如,读取从地址 3 开始的 3 个输入寄存器:
try
{ushort[] inputRegisters = modbusMaster.ReadInputRegisters(3, 3);Console.WriteLine("输入寄存器的值:");foreach (ushort inputRegister in inputRegisters){Console.WriteLine(inputRegister);}
}
catch (Exception ex)
{Console.WriteLine("读取输入寄存器出错:" + ex.Message);
}
4. 写单个线圈
使用 WriteSingleCoil 方法向单个线圈写入状态值。需要指定线圈地址和状态值(true 表示 ON,false 表示 OFF)。例如,将地址为 1 的线圈状态设为 ON:
try
{modbusMaster.WriteSingleCoil(1, true);Console.WriteLine("成功向线圈地址 1 写入状态 ON");
}
catch (Exception ex)
{Console.WriteLine("写入线圈出错:" + ex.Message);
}
5. 读离散输入状态
使用 ReadDiscreteInputs 方法读取离散输入的状态。指定起始地址和读取数量。例如,读取从地址 0 开始的 8 个离散输入状态:
try
{bool[] discreteInputs = modbusMaster.ReadDiscreteInputs(0, 8);Console.WriteLine("离散输入的状态:");foreach (bool discreteInput in discreteInputs){Console.WriteLine(discreteInput ? "ON" : "OFF");}
}
catch (Exception ex)
{Console.WriteLine("读取离散输入状态出错:" + ex.Message);
}
二、使用 EasyModbus 库
(一)安装库
同样通过 NuGet 包管理器安装 EasyModbus 库,在 Visual Studio 中找到并安装该库后,即可在项目中使用。
(二)连接服务器
创建 EasyModbusTCPClient 类的实例,传入服务器的 IP 地址和端口号来建立连接。例如:
using EasyModbus;class Program
{static void Main(string[] args){EasyModbusTCPClient client = new EasyModbusTCPClient("192.168.1.100", 502);}
}
(三)数据读写示例
1. 读保持寄存器
使用 ReadHoldingRegisters 方法读取保持寄存器的值。例如,读取地址 0 开始的 5 个保持寄存器:
try
{ushort[] registers = client.ReadHoldingRegisters(0, 5);Console.WriteLine("保持寄存器的值:");foreach (ushort register in registers){Console.WriteLine(register);}
}
catch (Exception ex)
{Console.WriteLine("读取保持寄存器出错:" + ex.Message);
}
2. 写单个保持寄存器
使用 WriteSingleRegister 方法向单个保持寄存器写入值。例如,向地址为 2 的保持寄存器写入值 0x001A:
try
{client.WriteSingleRegister(2, 0x001A);Console.WriteLine("成功向保持寄存器地址 2 写入值 0x001A");
}
catch (Exception ex)
{Console.WriteLine("写入保持寄存器出错:" + ex.Message);
}
3. 读输入寄存器
使用 ReadInputRegisters 方法读取输入寄存器的值。例如,读取地址为 3 开始的 3 个输入寄存器:
try
{ushort[] inputRegisters = client.ReadInputRegisters(3, 3);Console.WriteLine("输入寄存器的值:");foreach (ushort inputRegister in inputRegisters){Console.WriteLine(inputRegister);}
}
catch (Exception ex)
{Console.WriteLine("读取输入寄存器出错:" + ex.Message);
}
4. 写单个线圈
使用 WriteSingleCoil 方法向单个线圈写入状态值。例如,将地址为 1 的线圈状态设为 ON:
try
{client.WriteSingleCoil(1, true);Console.WriteLine("成功向线圈地址 1 写入状态 ON");
}
catch (Exception ex)
{Console.WriteLine("写入线圈出错:" + ex.Message);
}
5. 读离散输入状态
使用 ReadDiscreteInputs 方法读取离散输入的状态。例如,读取从地址 0 开始的 8 个离散输入状态:
try
{bool[] discreteInputs = client.ReadDiscreteInputs(0, 8);Console.WriteLine("离散输入的状态:");foreach (bool discreteInput in discreteInputs){Console.WriteLine(discreteInput ? "ON" : "OFF");}
}
catch (Exception ex)
{Console.WriteLine("读取离散输入状态出错:" + ex.Message);
}
专有名词解释
-
Modbus TCP :基于 Modbus 协议的以太网通信协议,用于在设备之间通过以太网进行数据传输,具有简单、可靠的特点,广泛应用于工业自动化领域。
-
客户端 - 服务器模式(Client - Server Model) :一种网络通信架构,客户端主动向服务器请求服务,服务器负责处理请求并返回结果。在 Modbus TCP 中,客户端通常是主控设备,服务器是数据采集设备或从设备。
-
数据存储区(Data Storage Area) : Modbus 协议中用于存储不同类型数据的区域,包括线圈、离散输入、保持寄存器和输入寄存器存储区,每个存储区用于特定类型数据的读写操作。
-
功能码(Function Code) : Modbus 协议中的指令代码,用于告诉服务器要执行的具体操作,如读取寄存器、写入寄存器、读取线圈状态等。不同的功能码对应不同的操作类型和数据格式。