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#类库,我们将按照以下步骤构建一个高效且可靠的实现:

 

完整代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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
1
2
using var client = new ModbusClient();
await client.ConnectAsync("192.168.1.100");
  1. 读取保持寄存器

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

csharp
1
2
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 @   funiyi816  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示