在C# Modbus RTU串口通信中,避免读写操作相互影响的关键在于合理的同步机制和资源管理。以下是几种有效的解决方案:
1. 使用锁机制(推荐)
基本锁实现
public class ModbusRTUManager
{
private readonly SerialPort _serialPort;
private readonly object _lockObject = new object();
public ModbusRTUManager(string portName, int baudRate)
{
_serialPort = new SerialPort(portName, baudRate)
{
Parity = Parity.None,
StopBits = StopBits.One,
DataBits = 8,
ReadTimeout = 1000,
WriteTimeout = 1000
};
}
public byte[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count)
{
lock (_lockObject)
{
if (!_serialPort.IsOpen)
_serialPort.Open();
// 发送读取请求
var request = CreateReadHoldingRegistersRequest(slaveId, startAddress, count);
_serialPort.Write(request, 0, request.Length);
// 读取响应
return ReadResponse(slaveId, 3); // 3是功能码
}
}
public void WriteSingleRegister(byte slaveId, ushort address, ushort value)
{
lock (_lockObject)
{
if (!_serialPort.IsOpen)
_serialPort.Open();
// 发送写入请求
var request = CreateWriteSingleRegisterRequest(slaveId, address, value);
_serialPort.Write(request, 0, request.Length);
// 读取响应(如果需要)
ReadResponse(slaveId, 6); // 6是功能码
}
}
private byte[] ReadResponse(byte expectedSlaveId, byte expectedFunctionCode)
{
// 实现响应读取逻辑,包括CRC校验
// ...
}
}
2. 使用SemaphoreSlim(支持异步)
public class AsyncModbusRTUManager
{
private readonly SerialPort _serialPort;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public async Task<byte[]> ReadHoldingRegistersAsync(byte slaveId, ushort startAddress, ushort count)
{
await _semaphore.WaitAsync();
try
{
if (!_serialPort.IsOpen)
_serialPort.Open();
// 清除输入缓冲区
_serialPort.DiscardInBuffer();
// 发送请求
var request = CreateReadHoldingRegistersRequest(slaveId, startAddress, count);
await WriteAsync(request);
// 读取响应
return await ReadResponseAsync(slaveId, 3);
}
finally
{
_semaphore.Release();
}
}
private async Task WriteAsync(byte[] data)
{
await Task.Run(() => _serialPort.Write(data, 0, data.Length));
}
private async Task<byte[]> ReadResponseAsync(byte expectedSlaveId, byte expectedFunctionCode)
{
// 异步读取实现
// ...
}
}
3. 使用读写锁(ReadWriteLockSlim)
public class ReadWriteModbusRTUManager
{
private readonly SerialPort _serialPort;
private readonly ReaderWriterLockSlim _lockSlim = new ReaderWriterLockSlim();
// 用于读取操作(允许多个读取,但实际串口需要独占)
public byte[] ReadData(byte slaveId, ushort address, ushort count)
{
_lockSlim.EnterWriteLock(); // 串口读取也需要写锁,因为涉及实际的IO操作
try
{
// 读取操作
return PerformReadOperation(slaveId, address, count);
}
finally
{
_lockSlim.ExitWriteLock();
}
}
// 用于写入操作
public void WriteData(byte slaveId, ushort address, ushort value)
{
_lockSlim.EnterWriteLock();
try
{
// 写入操作
PerformWriteOperation(slaveId, address, value);
}
finally
{
_lockSlim.ExitWriteLock();
}
}
}
4. 完整的线程安全实现示例
public class ThreadSafeModbusRTU : IDisposable
{
private readonly SerialPort _serialPort;
private readonly object _syncRoot = new object();
private bool _disposed = false;
public ThreadSafeModbusRTU(string portName, int baudRate = 9600)
{
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
Handshake = Handshake.None
};
}
public ModbusResult ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count)
{
lock (_syncRoot)
{
EnsureNotDisposed();
EnsurePortOpen();
try
{
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
// 1. 发送请求
var request = BuildReadRequest(slaveId, startAddress, count);
_serialPort.Write(request, 0, request.Length);
// 2. 读取响应
var response = ReadExpectedBytes(5 + count * 2); // 基础5字节 + 数据
// 3. 验证响应
if (!ValidateResponse(response, slaveId, 0x03))
return ModbusResult.CreateErrorResult("响应验证失败");
// 4. 解析数据
var data = ParseRegisterData(response, count);
return ModbusResult.CreateSuccessResult(data);
}
catch (TimeoutException ex)
{
return ModbusResult.CreateErrorResult($"读取超时: {ex.Message}");
}
catch (Exception ex)
{
return ModbusResult.CreateErrorResult($"读取失败: {ex.Message}");
}
}
}
public ModbusResult WriteSingleRegister(byte slaveId, ushort address, ushort value)
{
lock (_syncRoot)
{
EnsureNotDisposed();
EnsurePortOpen();
try
{
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
// 发送写入请求
var request = BuildWriteRequest(slaveId, address, value);
_serialPort.Write(request, 0, request.Length);
// 读取响应
var response = ReadExpectedBytes(8); // 写入响应固定8字节
if (!ValidateResponse(response, slaveId, 0x06))
return ModbusResult.CreateErrorResult("写入响应验证失败");
return ModbusResult.CreateSuccessResult();
}
catch (Exception ex)
{
return ModbusResult.CreateErrorResult($"写入失败: {ex.Message}");
}
}
}
private void EnsurePortOpen()
{
if (!_serialPort.IsOpen)
_serialPort.Open();
}
private void EnsureNotDisposed()
{
if (_disposed)
throw new ObjectDisposedException("ModbusRTU已被释放");
}
public void Dispose()
{
lock (_syncRoot)
{
if (!_disposed)
{
_serialPort?.Close();
_serialPort?.Dispose();
_disposed = true;
}
}
}
}
5. 使用建议
-
单一职责原则:每个串口由一个管理器实例负责
-
超时设置:合理设置读写超时,避免线程长时间阻塞
-
缓冲区清理:每次通信前清理缓冲区
-
异常处理:确保异常情况下锁能被正确释放
-
资源释放:正确实现IDisposable接口
6. 调用示例
using (var modbus = new ThreadSafeModbusRTU("COM1", 9600))
{
// 读取操作 - 自动同步,不会相互干扰
var result1 = modbus.ReadHoldingRegisters(1, 0, 10);
// 写入操作 - 会等待读取完成后执行
var result2 = modbus.WriteSingleRegister(1, 5, 100);
// 并发调用也会自动序列化
Task.WaitAll(
Task.Run(() => modbus.ReadHoldingRegisters(1, 0, 5)),
Task.Run(() => modbus.WriteSingleRegister(1, 6, 200))
);
}
这种设计确保了串口读写的原子性,避免了多个线程同时访问串口导致的通信混乱。