在工业自动化领域,Modbus协议就像设备间的"普通话",但传统实现往往需要依赖第三方库。最近我用纯C#实现了零依赖的Modbus全协议栈,从协议解析到PLC通信全流程打通。这个方案最特别的地方在于:完全基于.NET原生Socket开发,不依赖任何Modbus库,却能完整支持RTU/ASCII/TCP三种传输模式。
为什么要做这件事?在工业现场,我们经常遇到这样的困境:老旧设备需要升级通信功能,但受限于环境无法安装额外依赖;或者需要深度定制协议细节,却被商业库的黑箱所限制。这个方案就像给工业设备打造了一把瑞士军刀——轻便、全能、可自由改装。
用Wireshark抓取PLC通信数据包时,会发现Modbus协议就像个俄罗斯套娃。以TCP模式为例,一个完整帧包含:
RTU模式更精简,用CRC校验替代TCP的MBAP头,但核心功能码和数据区结构完全一致。这就好比快递可以选择空运或陆运,但包裹内容不变。
最常用的三大功能码实现逻辑:
csharp复制byte[] BuildReadHoldingRegisters(byte unitId, ushort startAddr, ushort quantity)
{
var frame = new byte[6];
frame[0] = unitId;
frame[1] = 0x03; // 功能码
Buffer.BlockCopy(BitConverter.GetBytes(startAddr), 0, frame, 2, 2);
Buffer.BlockCopy(BitConverter.GetBytes(quantity), 0, frame, 4, 2);
return frame;
}
关键点:所有数值字段都必须转为网络字节序(大端),这在x86架构的工控机上要特别注意
csharp复制public class ModbusTcpMaster
{
private TcpClient _client;
private ushort _transactionId;
public async Task<ushort[]> ReadHoldingRegistersAsync(byte unitId, ushort startAddr, ushort quantity)
{
var request = BuildTcpFrame(unitId, BuildReadHoldingRegisters(unitId, startAddr, quantity));
await _client.GetStream().WriteAsync(request, 0, request.Length);
var response = await ReadResponseAsync();
return ParseRegisterResponse(response);
}
}
使用SerialPort类时要注意:
csharp复制ushort CalculateCRC(byte[] data)
{
ushort crc = 0xFFFF;
for(int i=0; i<data.Length; i++) {
crc ^= data[i];
for(int j=0; j<8; j++) {
if((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
三菱PLC的地址需要转换:
csharp复制var values = await master.ReadHoldingRegistersAsync(1, 100, 6);
工业现场常见问题及对策:
响应超时:
CRC校验失败:
错误码处理:
csharp复制if(response[1] >= 0x80) {
var errorCode = response[2];
throw new ModbusException($"设备返回错误:{(ModbusError)errorCode}");
}
采用连接池方案:
csharp复制public class ModbusConnectionPool : IDisposable
{
private ConcurrentDictionary<string, Lazy<TcpClient>> _pool;
public async Task<ushort[]> ExecuteAsync(string ip, Func<TcpClient, Task<ushort[]>> operation)
{
var lazyClient = _pool.GetOrAdd(ip,
new Lazy<TcpClient>(() => new TcpClient(ip, 502)));
try {
return await operation(lazyClient.Value);
} catch {
_pool.TryRemove(ip, out _);
throw;
}
}
}
tcp.port == 502modbus.func_code >= 0x80modbus.trans_id == 0x1234在Wireshark中:
code复制ModbusClient (abstract)
├── ModbusTcpClient
├── ModbusRtuClient
└── ModbusAsciiClient
ModbusMessage
├── ModbusRequest
└── ModbusResponse
ModbusException
在i5-8250U工控机上的测试结果:
对比主流库(如NModbus):
网络:
安全:
通信中断:
数据异常:
以读取设备序列号为例:
csharp复制public byte[] BuildCustomFunction(byte unitId, ushort customCode)
{
var frame = new byte[4];
frame[0] = unitId;
frame[1] = 0x41; // 自定义功能码
Buffer.BlockCopy(BitConverter.GetBytes(customCode), 0, frame, 2, 2);
return frame;
}
通过协议转换实现Modbus与OPC UA互通:
在工业4.0项目中,这种轻量级方案特别适合边缘计算场景。最近在某智能产线改造中,我们用这套代码成功对接了8种不同年代的设备,包括20年前的老式PLC和最新的协作机器人。