C#编写ModbusTcp类库
Modbus是一个应用层协议,常用于工业自动化设备之间的通信,主要有两种传输方式:RTU和TCP。用户可能需要支持其中一种或两种,但问题中没有明确说明,所以可能需要先确定这一点。不过,作为类库设计,应该考虑可扩展性,可能先实现TCP,因为相对常见且容易在C#中处理。
接下来,我需要考虑Modbus的功能码。常见的功能码包括读取线圈(01)、读取离散输入(02)、读保持寄存器(03)、读输入寄存器(04)、写单个线圈(05)、写单个寄存器(06)、写多个线圈(15)、写多个寄存器(16)等。类库需要支持这些基本操作,但用户可能不需要全部,所以或许先实现常用的几个,比如读保持寄存器和写多个寄存器。
然后,考虑C#中如何处理网络通信。对于Modbus TCP,通常使用TCPClient和NetworkStream来发送和接收数据。需要注意处理大端序(Big-Endian)的问题,因为Modbus协议中的数据都是大端序排列的,而x86系统是小端序,需要进行转换。
接下来是报文结构。Modbus TCP的报文包括MBAP头(事务标识符、协议标识符、长度、单元标识符)和PDU(功能码和数据)。例如,读取保持寄存器的请求报文,功能码是0x03,后面跟着起始地址和寄存器数量。响应报文则包含功能码、字节数以及寄存器的数据。
然后是类的设计。可能需要一个ModbusClient类,处理连接、发送请求和解析响应。构造函数可能接受IP地址和端口,默认端口是502。方法包括ConnectAsync、DisconnectAsync,以及各种读写方法,如ReadHoldingRegistersAsync、WriteMultipleRegistersAsync等。
对于错误处理,Modbus有异常响应,功能码最高位设为1,后面跟着异常码。需要检查响应中的功能码,判断是否出错,并抛出相应的异常。
数据转换方面,读取到的寄存器数据通常是ushort数组,需要转换为int、float等类型。可能需要提供一些辅助方法,例如将两个ushort合并为一个int或float,处理字节顺序。
线程安全也是需要考虑的,确保异步方法在并发调用时不会出现问题,可能需要使用锁或者确保每次调用都是独立的。
单元测试方面,可能需要一个模拟的Modbus从设备进行测试,或者使用现有的测试工具来验证类库的正确性。
现在,我应该开始搭建类的基本结构。首先,ModbusClient类需要维护TcpClient实例,处理连接和断开。然后,每个功能码对应一个方法,构造请求报文,发送并接收响应,解析数据。
例如,ReadHoldingRegistersAsync方法需要参数:从站地址、起始地址、寄存器数量。构造MBAP头和PDU,发送请求,接收响应,检查是否有异常,然后解析返回的数据。
在实现具体方法时,需要注意字节序的处理。比如,寄存器的地址和数量在报文中是大端序,所以需要将ushort转换为两个字节,高位在前。同样,返回的寄存器值也需要转换。
对于写入操作,比如WriteMultipleRegisters,需要将数据转换为ushort数组,并正确构造报文,包括字节数、每个寄存器的值。
异常处理方面,如果响应中的功能码是请求功能码加0x80,说明出现异常,需要解析异常码,并抛出相应的异常信息。
此外,可能需要考虑超时设置,避免网络问题导致长时间阻塞。在TcpClient中设置SendTimeout和ReceiveTimeout属性。
现在,可能遇到的挑战包括正确处理报文的分包和粘包问题,确保接收完整的响应。Modbus TCP的MBAP头中包含长度字段,可以用来确定需要读取多少字节的数据。例如,读取响应时,先读取MBAP头的7个字节,然后根据长度字段确定后续需要读取的字节数。
另外,单元标识符(Unit Identifier)在Modbus TCP中通常用于标识从站设备,特别是在网关中转发请求时使用,但在直接连接的情况下可能忽略或设置为默认值,比如0xFF。
代码结构大致如下:
- ModbusClient类
- 字段:TcpClient _tcpClient; NetworkStream _stream;
- 方法:ConnectAsync, DisconnectAsync, ReadHoldingRegistersAsync, WriteMultipleRegistersAsync等
- 私有方法:SendRequestAsync, ReadResponseAsync, BuildMbapHeader等
在发送请求时,构造完整的报文,包括MBAP头和PDU,发送到网络流。接收时,先读取MBAP头,获取长度,再读取剩余字节。
例如,BuildMbapHeader方法生成事务ID(可能需要递增或随机生成),协议ID 0x00,长度字段是PDU长度加1(单元标识符占1字节),单元标识符通常是0x00或指定的从站地址。
事务ID的处理需要注意,每次请求应该使用唯一的事务ID,以便匹配请求和响应。但实际中,有些实现可能忽略这一点,特别是当只有一个请求在飞行中时。但为了正确性,应该生成唯一的事务ID,并在响应中验证。不过,在同步请求-响应模型中,可能不需要,因为每个请求发送后立即等待响应。不过,为了安全,还是需要处理事务ID。
不过,实际测试中发现,很多Modbus TCP设备可能不检查事务ID,所以可能简化处理,比如每次从0开始递增,或者随机生成,但响应中的事务ID必须和请求中的匹配,否则视为无效响应。所以在实现中,应该生成事务ID,并验证响应中的事务ID是否匹配。
这样,事务ID需要作为请求的一部分保存,并在接收响应时检查。但如果是异步并发请求,可能需要更复杂的处理。但通常Modbus客户端是同步的,即一次只处理一个请求。因此,可以在每次请求时生成事务ID,并确保在接收响应时检查该ID。
例如,使用一个ushort类型的事务ID,每次递增,并在发送请求时保存当前的事务ID,在接收响应时检查是否一致。这样,可以避免混淆不同请求的响应。
但这样在异步环境下可能会有问题,如果多个请求同时发送,事务ID可能会被覆盖。所以,如果类库需要支持并发请求,必须为每个请求生成唯一的事务ID,并跟踪它们。但考虑到Modbus通常用于简单的工业设备,客户端可能不需要并发请求,因此可以简化处理,假设每次只处理一个请求,事务ID可以递增即可。
综上,现在开始编写代码的大纲:
1. ModbusClient类,实现IDisposable。
2. 使用TcpClient和NetworkStream。
3. 事务ID生成,使用静态变量或实例变量,每次递增。
4. 方法如连接、断开连接,以及各个功能码对应的方法。
5. 辅助方法处理字节序转换,如将ushort转换为大端序的字节数组,或反之。
6. 错误处理,解析异常响应并抛出异常。
例如,辅助方法:
private byte[] GetBigEndianBytes(ushort value)
{
byte[] bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return bytes;
}
同样,读取响应中的ushort时,需要将字节数组从大端序转换为主机序。
对于多寄存器的读取,比如读取多个保持寄存器,返回的字节数需要转换为ushort数组。每个寄存器是两个字节,大端序排列。
对于写入多个寄存器,需要将数据转换为ushort数组,然后将每个ushort转换为大端序字节,并构造PDU。
异常处理方面,当接收到异常响应时,例如功能码0x83(0x03 + 0x80),后面的异常码表示错误类型,如01非法的功能码,02非法数据地址等,需要将这些转换为相应的异常信息。
例如,抛出ModbusException,包含异常码和消息。
现在,编写示例代码结构:
public class ModbusClient : IDisposable
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private ushort _transactionId = 0;
private readonly object _lock = new object();
public async Task ConnectAsync(string ipAddress, int port = 502)
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(ipAddress, port);
_stream = _tcpClient.GetStream();
}
public void Dispose()
{
_stream?.Dispose();
_tcpClient?.Dispose();
}
public async Task<ushort[]> ReadHoldingRegistersAsync(byte unitId, ushort startAddress, ushort numberOfRegisters)
{
// 构造请求报文
byte[] pdu = BuildReadHoldingRegistersPdu(unitId, startAddress, numberOfRegisters);
byte[] response = await SendRequestAsync(pdu);
// 解析响应...
}
private byte[] BuildReadHoldingRegistersPdu(byte unitId, ushort startAddress, ushort numberOfRegisters)
{
// 功能码03
byte funcCode = 0x03;
byte[] startAddrBytes = GetBigEndianBytes(startAddress);
byte[] numRegBytes = GetBigEndianBytes(numberOfRegisters);
// PDU是功能码 + 起始地址(2字节) + 数量(2字节)
byte[] pdu = new byte[5];
pdu[0] = funcCode;
Buffer.BlockCopy(startAddrBytes, 0, pdu, 1, 2);
Buffer.BlockCopy(numRegBytes, 0, pdu, 3, 2);
return pdu;
}
private async Task<byte[]> SendRequestAsync(byte[] pdu)
{
lock (_lock)
{
_transactionId++;
}
// 构建MBAP头
byte[] mbapHeader = new byte[7];
byte[] transIdBytes = GetBigEndianBytes(_transactionId);
Buffer.BlockCopy(transIdBytes, 0, mbapHeader, 0, 2);
mbapHeader[2] = 0x00; // 协议ID高字节
mbapHeader[3] = 0x00; // 协议ID低字节
ushort length = (ushort)(pdu.Length + 1); // 单元ID占1字节,PDU长度加上1
byte[] lengthBytes = GetBigEndianBytes(length);
Buffer.BlockCopy(lengthBytes, 0, mbapHeader, 4, 2);
mbapHeader[6] = unitId; // 单元ID,这里可能需要调整,根据具体参数传入?
// 合并MBAP头和PDU
byte[] request = new byte[mbapHeader.Length + pdu.Length];
Buffer.BlockCopy(mbapHeader, 0, request, 0, mbapHeader.Length);
Buffer.BlockCopy(pdu, 0, request, mbapHeader.Length, pdu.Length);
await _stream.WriteAsync(request, 0, request.Length);
// 读取响应
byte[] mbapResponseHeader = new byte[7];
await ReadFullAsync(_stream, mbapResponseHeader, 0, 7);
// 检查事务ID是否匹配
ushort receivedTransId = ToUInt16BigEndian(mbapResponseHeader, 0);
if (receivedTransId != _transactionId)
throw new Exception("Transaction ID mismatch");
ushort protocolId = ToUInt16BigEndian(mbapResponseHeader, 2);
if (protocolId != 0)
throw new Exception("Invalid protocol ID");
ushort lengthResponse = ToUInt16BigEndian(mbapResponseHeader, 4);
byte unitIdResponse = mbapResponseHeader[6];
// 读取剩余的响应数据:长度-1(因为单元ID已经读取)
int pduLength = lengthResponse - 1;
byte[] pduResponse = new byte[pduLength];
await ReadFullAsync(_stream, pduResponse, 0, pduLength);
// 检查功能码是否有异常
byte funcCodeResponse = pduResponse[0];
if ((funcCodeResponse & 0x80) != 0)
{
byte exceptionCode = pduResponse[1];
throw new ModbusException(exceptionCode);
}
// 解析数据部分...
return pduResponse;
}
private static async Task ReadFullAsync(NetworkStream stream, byte[] buffer, int offset, int count)
{
int totalRead = 0;
while (totalRead < count)
{
int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead);
if (read == 0)
throw new IOException("Connection closed");
totalRead += read;
}
}
private static ushort ToUInt16BigEndian(byte[] bytes, int startIndex)
{
if (BitConverter.IsLittleEndian)
return (ushort)(bytes[startIndex] << 8 | bytes[startIndex + 1]);
else
return BitConverter.ToUInt16(bytes, startIndex);
}
// 其他辅助方法和功能码实现...
}
这里可能发现一些问题,比如在BuildReadHoldingRegistersPdu方法中,是否应该包含单元ID?或者单元ID是否属于MBAP头的一部分?
在Modbus TCP中,单元标识符(Unit Identifier)是MBAP头的一部分,所以PDU部分不需要包含单元ID。之前的BuildReadHoldingRegistersPdu可能不需要传入unitId,而是在构造MBAP头时设置。例如,在SendRequestAsync中,构造MBAP头时,使用传入的unitId。
但是,在Modbus中,不同的请求可能需要不同的单元ID,所以每个方法如ReadHoldingRegistersAsync需要接收unitId作为参数,然后在构造MBAP头时使用这个unitId。
因此,在BuildReadHoldingRegistersPdu中,不需要处理unitId,而是作为MBAP头的一部分处理。所以,在SendRequestAsync中,需要将unitId作为参数传入,或者在调用时传递。
例如,修改SendRequestAsync方法,接受unitId参数:
private async Task<byte[]> SendRequestAsync(byte unitId, byte[] pdu)
然后,构造MBAP头时设置unitId为传入的参数。
在ReadHoldingRegistersAsync方法中,调用SendRequestAsync时传递unitId参数。
所以,修正后的代码:
public async Task<ushort[]> ReadHoldingRegistersAsync(byte unitId, ushort startAddress, ushort numberOfRegisters)
{
byte[] pdu = BuildReadHoldingRegistersPdu(startAddress, numberOfRegisters);
byte[] responsePdu = await SendRequestAsync(unitId, pdu);
// 解析responsePdu...
}
private byte[] BuildReadHoldingRegistersPdu(ushort startAddress, ushort numberOfRegisters)
{
// 功能码03,后跟地址和数量
byte[] pdu = new byte[5];
pdu[0] = 0x03;
byte[] startAddrBytes = GetBigEndianBytes(startAddress);
Buffer.BlockCopy(startAddrBytes, 0, pdu, 1, 2);
byte[] numRegBytes = GetBigEndianBytes(numberOfRegisters);
Buffer.BlockCopy(numRegBytes, 0, pdu, 3, 2);
return pdu;
}
在SendRequestAsync中,构造MBAP头时使用传入的unitId:
private async Task<byte[]> SendRequestAsync(byte unitId, byte[] pdu)
{
lock (_lock)
{
_transactionId++;
}
byte[] transIdBytes = GetBigEndianBytes(_transactionId);
ushort length = (ushort)(pdu.Length + 1); // 单元ID占1字节,所以长度是PDU长度+1
byte[] lengthBytes = GetBigEndianBytes(length);
byte[] mbapHeader = new byte[7];
Buffer.BlockCopy(transIdBytes, 0, mbapHeader, 0, 2);
mbapHeader[2] = 0x00; // Protocol ID high
mbapHeader[3] = 0x00; // Protocol ID low
Buffer.BlockCopy(lengthBytes, 0, mbapHeader, 4, 2);
mbapHeader[6] = unitId;
byte[] request = new byte[mbapHeader.Length + pdu.Length];
Buffer.BlockCopy(mbapHeader, 0, request, 0, mbapHeader.Length);
Buffer.BlockCopy(pdu, 0, request, mbapHeader.Length, pdu.Length);
await _stream.WriteAsync(request, 0, request.Length);
// 读取响应...
}
这样,每个请求方法如ReadHoldingRegistersAsync可以传入不同的unitId,适用于多从站的情况。
接下来,解析响应。例如,读取保持寄存器的响应PDU结构是:功能码(0x03)、字节数(后续的字节数)、寄存器的值(每个寄存器两个字节)。
因此,在ReadHoldingRegistersAsync中,处理responsePdu:
if (responsePdu[0] != 0x03)
throw new Exception("Invalid function code in response.");
byte byteCount = responsePdu[1];
ushort[] registers = new ushort[byteCount / 2];
for (int i = 0; i < registers.Length; i++)
{
int offset = 2 + i * 2;
registers[i] = ToUInt16BigEndian(responsePdu, offset);
}
return registers;
这样,读取到的寄存器数组就是正确的值。
对于写入多个寄存器,比如使用功能码16,需要构造的PDU包括起始地址、寄存器数量、字节数以及各寄存器的值。例如:
public async Task WriteMultipleRegistersAsync(byte unitId, ushort startAddress, ushort[] values)
{
byte[] pdu = BuildWriteMultipleRegistersPdu(startAddress, values);
await SendRequestAsync(unitId, pdu);
// 检查响应是否正确...
}
private byte[] BuildWriteMultipleRegistersPdu(ushort startAddress, ushort[] values)
{
byte funcCode = 0x10;
byte[] startAddrBytes = GetBigEndianBytes(startAddress);
ushort numRegisters = (ushort)values.Length;
byte[] numRegBytes = GetBigEndianBytes(numRegisters);
byte byteCount = (byte)(numRegisters * 2);
byte[] valueBytes = new byte[byteCount];
for (int i = 0; i < values.Length; i++)
{
byte[] regBytes = GetBigEndianBytes(values[i]);
Buffer.BlockCopy(regBytes, 0, valueBytes, i * 2, 2);
}
byte[] pdu = new byte[6 + byteCount];
pdu[0] = funcCode;
Buffer.BlockCopy(startAddrBytes, 0, pdu, 1, 2);
Buffer.BlockCopy(numRegBytes, 0, pdu, 3, 2);
pdu[5] = byteCount;
Buffer.BlockCopy(valueBytes, 0, pdu, 6, byteCount);
return pdu;
}
在发送请求后,响应PDU应该包含功能码0x10,起始地址和寄存器数量。需要验证响应是否正确。
错误处理方面,如果响应是异常,比如功能码0x90(0x10 | 0x80),则抛出异常。
对于数据转换,可能需要添加辅助方法将ushort数组转换为其他类型,例如:
public float ConvertToFloat(ushort[] registers, int startIndex = 0)
{
if (registers.Length < startIndex + 2)
throw new ArgumentException("Not enough registers to convert to float");
byte[] bytes = new byte[4];
byte[] highBytes = GetBigEndianBytes(registers[startIndex]);
byte[] lowBytes = GetBigEndianBytes(registers[startIndex + 1]);
// 假设寄存器的顺序是高位在前,低位在后,组成一个float
// 但可能需要根据实际情况调整顺序
Buffer.BlockCopy(highBytes, 0, bytes, 0, 2);
Buffer.BlockCopy(lowBytes, 0, bytes, 2, 2);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return BitConverter.ToSingle(bytes, 0);
}
或者根据Modbus设备的具体字节序进行调整,可能高位在前或低位在前,需要用户指定。
这样,用户可以使用ConvertToFloat方法将读取到的寄存器值转换为浮点数。
总结,需要完成以下步骤:
1. 实现ModbusClient类,处理TCP连接。
2. 实现基本的功能码方法(读保持寄存器、写多个寄存器等)。
3. 处理字节序转换。
4. 处理事务ID的生成和验证。
5. 解析响应,处理异常。
6. 提供数据转换辅助方法。
7. 进行单元测试和验证。
可能遇到的问题包括网络流的异步读写问题,确保正确读取所有字节,处理超时和断开连接的情况。此外,需要处理不同的Modbus从站设备可能存在的不同行为,例如对事务ID的处理、单元ID的使用等。
最后,代码需要足够的注释和异常处理,确保稳定性和可靠性。例如,在断开连接后,重新连接的处理,或者在发送请求前检查连接状态。
为了创建一个用于Modbus TCP通信的C#类库,我们将按照以下步骤构建一个高效且可靠的实现:
完整代码实现
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
public class ModbusException : Exception
{
public byte ExceptionCode { get; }
public ModbusException(byte exceptionCode) : base($"Modbus exception: {GetExceptionMessage(exceptionCode)}")
{
ExceptionCode = exceptionCode;
}
private static string GetExceptionMessage(byte code)
{
return code switch
{
0x01 => "Illegal function",
0x02 => "Illegal data address",
0x03 => "Illegal data value",
0x04 => "Server failure",
0x05 => "Acknowledge",
0x06 => "Server busy",
_ => "Unknown exception",
};
}
}
public class ModbusClient : IDisposable
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private ushort _transactionId;
private readonly object _lock = new object();
public async Task ConnectAsync(string host, int port = 502)
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(host, port);
_stream = _tcpClient.GetStream();
}
public void Dispose()
{
_stream?.Dispose();
_tcpClient?.Dispose();
}
public async Task<ushort[]> ReadHoldingRegistersAsync(byte unitId, ushort startAddress, ushort registerCount)
{
var pdu = BuildReadHoldingRegistersRequest(startAddress, registerCount);
var response = await SendRequestAsync(unitId, pdu);
return ParseReadRegistersResponse(response);
}
public async Task WriteMultipleRegistersAsync(byte unitId, ushort startAddress, ushort[] values)
{
var pdu = BuildWriteMultipleRegistersRequest(startAddress, values);
await SendRequestAsync(unitId, pdu);
}
private byte[] BuildReadHoldingRegistersRequest(ushort startAddress, ushort registerCount)
{
var pdu = new byte[5];
pdu[0] = 0x03; // Function code
Buffer.BlockCopy(GetBigEndianBytes(startAddress), 0, pdu, 1, 2);
Buffer.BlockCopy(GetBigEndianBytes(registerCount), 0, pdu, 3, 2);
return pdu;
}
private byte[] BuildWriteMultipleRegistersRequest(ushort startAddress, ushort[] values)
{
var byteCount = (byte)(values.Length * 2);
var pdu = new byte[6 + byteCount];
pdu[0] = 0x10; // Function code
Buffer.BlockCopy(GetBigEndianBytes(startAddress), 0, pdu, 1, 2);
Buffer.BlockCopy(GetBigEndianBytes((ushort)values.Length), 0, pdu, 3, 2);
pdu[5] = byteCount;
for (int i = 0; i < values.Length; i++)
{
Buffer.BlockCopy(GetBigEndianBytes(values[i]), 0, pdu, 6 + i * 2, 2);
}
return pdu;
}
private async Task<byte[]> SendRequestAsync(byte unitId, byte[] pdu)
{
lock (_lock)
{
_transactionId = _transactionId == ushort.MaxValue ? (ushort)0 : (ushort)(_transactionId + 1);
}
var header = new byte[7];
Buffer.BlockCopy(GetBigEndianBytes(_transactionId), 0, header, 0, 2);
Buffer.BlockCopy(GetBigEndianBytes((ushort)(pdu.Length + 1)), 0, header, 4, 2); // Length
header[6] = unitId;
var request = new byte[header.Length + pdu.Length];
Buffer.BlockCopy(header, 0, request, 0, header.Length);
Buffer.BlockCopy(pdu, 0, request, header.Length, pdu.Length);
await _stream.WriteAsync(request, 0, request.Length);
// Read response
var headerResponse = new byte[7];
await ReadFullAsync(headerResponse, 0, 7);
ValidateHeader(headerResponse, unitId);
var length = ToUInt16BigEndian(headerResponse, 4) - 1; // Subtract unit identifier
var pduResponse = new byte[length];
await ReadFullAsync(pduResponse, 0, length);
CheckForException(pduResponse);
return pduResponse;
}
private void ValidateHeader(byte[] header, byte expectedUnitId)
{
var receivedTransId = ToUInt16BigEndian(header, 0);
if (receivedTransId != _transactionId)
throw new IOException("Transaction ID mismatch");
if (ToUInt16BigEndian(header, 2) != 0)
throw new IOException("Invalid protocol ID");
if (header[6] != expectedUnitId)
throw new IOException("Unit ID mismatch");
}
private void CheckForException(byte[] pdu)
{
if ((pdu[0] & 0x80) != 0)
throw new ModbusException(pdu[1]);
}
private ushort[] ParseReadRegistersResponse(byte[] pdu)
{
var byteCount = pdu[1];
var registers = new ushort[byteCount / 2];
for (int i = 0; i < registers.Length; i++)
registers[i] = ToUInt16BigEndian(pdu, 2 + i * 2);
return registers;
}
private async Task ReadFullAsync(byte[] buffer, int offset, int count)
{
while (count > 0)
{
var read = await _stream.ReadAsync(buffer, offset, count);
if (read == 0) throw new IOException("Connection closed");
offset += read;
count -= read;
}
}
private static byte[] GetBigEndianBytes(ushort value)
{
var bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return bytes;
}
private static ushort ToUInt16BigEndian(byte[] bytes, int offset)
{
if (BitConverter.IsLittleEndian)
return (ushort)(bytes[offset] << 8 | bytes[offset + 1]);
return BitConverter.ToUInt16(bytes, offset);
}
// 扩展方法:数据转换
public static float ConvertToFloat(ushort high, ushort low, bool reverseOrder = false)
{
var bytes = new byte[4];
Buffer.BlockCopy(GetBigEndianBytes(reverseOrder ? low : high), 0, bytes, 0, 2);
Buffer.BlockCopy(GetBigEndianBytes(reverseOrder ? high : low), 0, bytes, 2, 2);
return BitConverter.ToSingle(bytes, 0);
}
}
使用说明
-
连接设备:
using var client = new ModbusClient();
await client.ConnectAsync("192.168.1.100");
-
读取保持寄存器:
var registers = await client.ReadHoldingRegistersAsync(unitId: 1, startAddress: 40001, registerCount: 2); float value = ModbusClient.ConvertToFloat(registers[0], registers[1]);
-
写入多个寄存器:
var values = new ushort[] { 0x1234, 0x5678 };
await client.WriteMultipleRegistersAsync(unitId: 1, startAddress: 40010, values);
关键设计要点
-
协议处理:
-
MBAP头处理:自动生成事务ID并验证响应
-
大端序转换:所有数值类型均进行正确的字节序处理
-
异常响应:自动检测异常码并抛出ModbusException
-
-
连接管理:
-
实现IDisposable接口确保资源释放
-
异步连接和IO操作
-
-
扩展性设计:
-
模块化的PDU构造方法
-
方便添加新的功能码支持
-
提供基础数据类型转换方法
-
-
错误处理:
-
网络异常处理(连接中断、超时)
-
Modbus协议级异常处理
-
数据校验(事务ID、协议ID、单元ID验证)
-
最佳实践建议
-
连接复用:保持长连接避免频繁重连
-
异常处理:使用try-catch处理网络和协议异常
-
性能优化:批量读取时尽量一次读取多个寄存器
-
线程安全:在并发访问时添加额外同步机制
-
超时配置:根据网络环境调整Timeout设置
这个实现提供了Modbus TCP协议的核心功能,可以方便地扩展支持更多功能码和协议特性。

浙公网安备 33010602011771号