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);
    }
}

  

使用说明

  1. 连接设备

csharp
using var client = new ModbusClient();
await client.ConnectAsync("192.168.1.100");
  1. 读取保持寄存器

csharp
var registers = await client.ReadHoldingRegistersAsync(unitId: 1, startAddress: 40001, registerCount: 2);
float value = ModbusClient.ConvertToFloat(registers[0], registers[1]);
  1. 写入多个寄存器

csharp
var values = new ushort[] { 0x1234, 0x5678 };
await client.WriteMultipleRegistersAsync(unitId: 1, startAddress: 40010, values);

关键设计要点

 

  1. 协议处理

    • MBAP头处理:自动生成事务ID并验证响应

    • 大端序转换:所有数值类型均进行正确的字节序处理

    • 异常响应:自动检测异常码并抛出ModbusException

  2. 连接管理

    • 实现IDisposable接口确保资源释放

    • 异步连接和IO操作

  3. 扩展性设计

    • 模块化的PDU构造方法

    • 方便添加新的功能码支持

    • 提供基础数据类型转换方法

  4. 错误处理

    • 网络异常处理(连接中断、超时)

    • Modbus协议级异常处理

    • 数据校验(事务ID、协议ID、单元ID验证)

最佳实践建议

  1. 连接复用:保持长连接避免频繁重连

  2. 异常处理:使用try-catch处理网络和协议异常

  3. 性能优化:批量读取时尽量一次读取多个寄存器

  4. 线程安全:在并发访问时添加额外同步机制

  5. 超时配置:根据网络环境调整Timeout设置

这个实现提供了Modbus TCP协议的核心功能,可以方便地扩展支持更多功能码和协议特性。

 

posted @ 2025-02-16 11:05  funiyi816  阅读(835)  评论(0)    收藏  举报