搞过工业数据采集的朋友应该都有体会——Modbus RTU 这玩意儿,说简单也简单,说折腾也是真折腾。
现场二十多台设备通过485总线串联。测试环境跑得好好的,一到现场就各种抽风:时不时读不到数据、偶尔返回乱码、高峰期直接卡死。排查了三天,发现问题出在超时设置不合理、缺乏重试机制、异常处理太粗暴这三个老毛病上。
读完这篇文章,你将掌握:
咱们直接上干货,代码都是从实际项目里抽出来的,拿去就能用。
很多人设置超时就是凭感觉——"500毫秒应该够了吧"。但 Modbus RTU 的实际响应时间受多种因素影响:
| 影响因素 | 典型延迟 | 说明 |
|---|---|---|
| 波特率传输 | 10-50ms | 9600波特率下,100字节约需104ms |
| 从站处理 | 5-200ms | 取决于设备性能和寄存器数量 |
| 485总线转发 | 2-10ms | 转换器质量参差不齐 |
| 线路干扰重传 | 0-50ms | 工业现场电磁环境复杂 |
真实案例:某项目波特率9600,读取40个寄存器。理论传输时间约85ms,但超时设为100ms后失败率高达15%。原因是没考虑从站处理时间和总线延迟。调整到350ms后,失败率降到0.3%以下。
极端一:不重试
读取失败 → 直接报错 → 用户看到满屏红色告警
结果:偶发的通信干扰被放大成"系统故障",运维电话被打爆。
极端二:无脑重试
读取失败 → 立即重试 → 再失败 → 再重试(循环10次)
结果:一个从站卡住,整条总线的采集周期被拖长,数据实时性崩盘。
我见过最离谱的代码是这样的:
csharptry
{
var data = ReadRegisters(slaveId, address, count);
}
catch
{
// 吞掉所有异常,假装什么都没发生
}
这种写法在测试环境可能跑得挺欢,但到了生产环境:
问题积累到一定程度,系统直接崩溃,排查时毫无头绪。
在动手写代码之前,咱们先把几个核心原则理清楚:
📌 超时计算公式
安全超时 = (字节数 × 10 / 波特率 × 1000) + 从站处理时间 + 安全余量
一般建议安全余量取理论传输时间的1.5-2倍。
📌 重试策略三要素
📌 异常分级处理
| 异常类型 | 处理策略 | 是否重试 |
|---|---|---|
| 超时异常 | 记录并重试 | ✅ |
| CRC校验错误 | 记录并重试 | ✅ |
| 串口被占用 | 等待后重试 | ✅(延迟重试) |
| 从站异常响应 | 记录具体错误码 | ❌ |
| 串口不存在 | 立即上报 | ❌ |
先来个能跑起来的基础版本,后面再逐步增强。
csharpusing System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppModbusRtu
{
public class ModbusRtuBasic : IDisposable
{
private SerialPort _serialPort;
private readonly object _lockObject = new object();
// 帧间隔时间(3.5个字符时间)
private int _frameInterval;
public ModbusRtuBasic(string portName, int baudRate = 9600)
{
_serialPort = new SerialPort
{
PortName = portName,
BaudRate = baudRate,
DataBits = 8,
Parity = Parity.None,
StopBits = StopBits.One,
// 关键:根据波特率计算合理的超时时间
ReadTimeout = CalculateTimeout(baudRate, 256),
WriteTimeout = 1000
};
// 计算帧间隔(Modbus RTU 规范要求至少3.5个字符时间)
_frameInterval = Math.Max(5, (int)(3.5 * 11 * 1000 / baudRate));
}
/// <summary>
/// 计算超时时间
/// </summary>
private int CalculateTimeout(int baudRate, int maxBytes)
{
// 每字节10位(1起始+8数据+1停止)
double transmissionTime = (double)maxBytes * 10 / baudRate * 1000;
// 加上从站处理时间(100ms)和安全余量(1.5倍)
return (int)(transmissionTime * 1.5 + 100);
}
public void Open()
{
if (!_serialPort.IsOpen)
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
}
}
/// <summary>
/// 读取保持寄存器(功能码03)
/// </summary>
public ushort[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count)
{
lock (_lockObject)
{
// 构建请求帧
byte[] request = BuildReadRequest(slaveId, 0x03, startAddress, count);
// 发送请求
_serialPort.DiscardInBuffer();
_serialPort.Write(request, 0, request.Length);
// 等待帧间隔
Thread.Sleep(_frameInterval);
// 计算期望的响应长度:从站地址(1) + 功能码(1) + 字节数(1) + 数据(n*2) + CRC(2)
int expectedLength = 5 + count * 2;
byte[] response = ReadResponse(expectedLength);
// 验证响应
ValidateResponse(response, slaveId, 0x03);
// 解析数据
return ParseRegisters(response, count);
}
}
private byte[] BuildReadRequest(byte slaveId, byte functionCode,
ushort startAddress, ushort count)
{
byte[] request = new byte[8];
request[0] = slaveId;
request[1] = functionCode;
request[2] = (byte)(startAddress >> 8);
request[3] = (byte)(startAddress & 0xFF);
request[4] = (byte)(count >> 8);
request[5] = (byte)(count & 0xFF);
// 计算CRC16
ushort crc = CalculateCrc16(request, 6);
request[6] = (byte)(crc & 0xFF);
request[7] = (byte)(crc >> 8);
return request;
}
private byte[] ReadResponse(int expectedLength)
{
byte[] buffer = new byte[expectedLength];
int offset = 0;
int remaining = expectedLength;
DateTime deadline = DateTime.Now.AddMilliseconds(_serialPort.ReadTimeout);
while (remaining > 0 && DateTime.Now < deadline)
{
if (_serialPort.BytesToRead > 0)
{
int bytesRead = _serialPort.Read(buffer, offset, remaining);
offset += bytesRead;
remaining -= bytesRead;
}
else
{
Thread.Sleep(5);
}
}
if (remaining > 0)
{
throw new TimeoutException($"响应超时,期望{expectedLength}字节,实际收到{offset}字节");
}
return buffer;
}
private void ValidateResponse(byte[] response, byte expectedSlaveId, byte expectedFunction)
{
// 检查从站地址
if (response[0] != expectedSlaveId)
{
throw new InvalidOperationException($"从站地址不匹配:期望{expectedSlaveId},实际{response[0]}");
}
// 检查是否为异常响应
if ((response[1] & 0x80) != 0)
{
byte exceptionCode = response[2];
throw new ModbusException(exceptionCode);
}
// 验证CRC
int dataLength = response.Length - 2;
ushort receivedCrc = (ushort)(response[dataLength] | (response[dataLength + 1] << 8));
ushort calculatedCrc = CalculateCrc16(response, dataLength);
if (receivedCrc != calculatedCrc)
{
throw new InvalidDataException($"CRC校验失败:接收{receivedCrc:X4},计算{calculatedCrc:X4}");
}
}
private ushort[] ParseRegisters(byte[] response, ushort count)
{
ushort[] registers = new ushort[count];
for (int i = 0; i < count; i++)
{
registers[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return registers;
}
private ushort CalculateCrc16(byte[] data, int length)
{
ushort crc = 0xFFFF;
for (int i = 0; i < length; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
{
crc = (ushort)((crc >> 1) ^ 0xA001);
}
else
{
crc >>= 1;
}
}
}
return crc;
}
public void Dispose()
{
_serialPort?.Close();
_serialPort?.Dispose();
}
}
/// <summary>
/// Modbus异常类
/// </summary>
public class ModbusException : Exception
{
public byte ExceptionCode { get; }
private static readonly Dictionary<byte, string> ExceptionMessages = new()
{
{ 0x01, "非法功能码" },
{ 0x02, "非法数据地址" },
{ 0x03, "非法数据值" },
{ 0x04, "从站设备故障" },
{ 0x05, "确认——请求已接受,处理中" },
{ 0x06, "从站设备忙" }
};
public ModbusException(byte code)
: base(ExceptionMessages.TryGetValue(code, out var msg) ? msg : $"未知异常(0x{code:X2})")
{
ExceptionCode = code;
}
}
}

⚠️ 踩坑预警:
SerialPort.ReadTimeout 是整体超时,不是单字节超时。数据量大时要相应调大。DiscardInBuffer(),否则可能读到上次残留的数据。基础版能用,但不够健壮。咱们加上重试逻辑:
csharpusing System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
/// <summary>
/// 带重试机制的Modbus RTU通信
/// </summary>
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppModbusRtu
{
public class ModbusRtuWithRetry : ModbusRtuBasic
{
private readonly ILogger _logger;
private readonly RetryPolicy _retryPolicy;
public ModbusRtuWithRetry(string portName, int baudRate,
ILogger logger, RetryPolicy policy = null)
: base(portName, baudRate)
{
_logger = logger;
_retryPolicy = policy ?? RetryPolicy.Default;
}
/// <summary>
/// 带重试的读取操作
/// </summary>
public async Task<ushort[]> ReadHoldingRegistersWithRetryAsync(
byte slaveId, ushort startAddress, ushort count,
CancellationToken cancellationToken = default)
{
int attempt = 0;
Exception lastException = null;
while (attempt < _retryPolicy.MaxRetries)
{
try
{
attempt++;
// 非首次尝试时等待递增的间隔
if (attempt > 1)
{
int delay = _retryPolicy.GetDelay(attempt);
_logger?.LogWarning(
"从站{SlaveId} 第{Attempt}次重试,等待{Delay}ms",
slaveId, attempt, delay);
await Task.Delay(delay, cancellationToken);
}
var result = ReadHoldingRegisters(slaveId, startAddress, count);
// 成功后记录恢复日志
if (attempt > 1)
{
_logger?.LogInformation(
"从站{SlaveId} 第{Attempt}次重试成功", slaveId, attempt);
}
return result;
}
catch (TimeoutException ex)
{
lastException = ex;
_logger?.LogWarning(ex,
"从站{SlaveId} 读取超时 (尝试 {Attempt}/{MaxRetries})",
slaveId, attempt, _retryPolicy.MaxRetries);
}
catch (InvalidDataException ex) // CRC错误
{
lastException = ex;
_logger?.LogWarning(ex,
"从站{SlaveId} CRC校验失败 (尝试 {Attempt}/{MaxRetries})",
slaveId, attempt, _retryPolicy.MaxRetries);
}
catch (ModbusException ex) when (ex.ExceptionCode == 0x06) // 设备忙
{
lastException = ex;
_logger?.LogWarning(
"从站{SlaveId} 设备忙,延长等待时间", slaveId);
await Task.Delay(_retryPolicy.BusyWaitTime, cancellationToken);
}
catch (ModbusException ex)
{
// Modbus协议层错误,不重试
_logger?.LogError(ex,
"从站{SlaveId} Modbus异常: {Message}", slaveId, ex.Message);
throw;
}
catch (InvalidOperationException ex) when (ex.Message.Contains("从站地址不匹配"))
{
// 地址冲突,可能是总线上有其他设备响应
lastException = ex;
_logger?.LogWarning(ex,
"从站{SlaveId} 响应地址不匹配,可能存在地址冲突", slaveId);
}
}
// 重试耗尽,抛出最后一个异常
_logger?.LogError(lastException,
"从站{SlaveId} 在{MaxRetries}次尝试后仍然失败",
slaveId, _retryPolicy.MaxRetries);
throw new AggregateException(
$"从站{slaveId}通信失败,已重试{_retryPolicy.MaxRetries}次",
lastException);
}
}
/// <summary>
/// 重试策略配置
/// </summary>
public class RetryPolicy
{
/// <summary>最大重试次数</summary>
public int MaxRetries { get; set; } = 3;
/// <summary>基础延迟时间(ms)</summary>
public int BaseDelay { get; set; } = 100;
/// <summary>最大延迟时间(ms)</summary>
public int MaxDelay { get; set; } = 2000;
/// <summary>延迟增长因子</summary>
public double BackoffMultiplier { get; set; } = 2.0;
/// <summary>设备忙时的等待时间(ms)</summary>
public int BusyWaitTime { get; set; } = 1000;
/// <summary>默认策略</summary>
public static RetryPolicy Default => new RetryPolicy();
/// <summary>激进策略——快速失败</summary>
public static RetryPolicy Aggressive => new RetryPolicy
{
MaxRetries = 2,
BaseDelay = 50,
MaxDelay = 200
};
/// <summary>保守策略——尽量成功</summary>
public static RetryPolicy Conservative => new RetryPolicy
{
MaxRetries = 5,
BaseDelay = 200,
MaxDelay = 5000
};
/// <summary>
/// 计算第n次重试的延迟时间(指数退避)
/// </summary>
public int GetDelay(int attempt)
{
double delay = BaseDelay * Math.Pow(BackoffMultiplier, attempt - 1);
return (int)Math.Min(delay, MaxDelay);
}
}
}
// 调用
using Microsoft.Extensions.Logging;
namespace AppModbusRtu
{
internal class Program
{
static void Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(LogLevel.Information)
.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "HH:mm:ss ";
});
});
var logger = loggerFactory.CreateLogger<ModbusRtuWithRetry>();
var modbus = new ModbusRtuWithRetry("COM1", 9600, logger);
modbus.Open();
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
try
{
var task = modbus.ReadHoldingRegistersWithRetryAsync(1, 0, 10, cancellationToken);
task.Wait(cancellationToken);
var registers = task.Result;
Console.WriteLine("Read registers:");
foreach (var reg in registers)
{
Console.WriteLine(reg);
}
}
catch (AggregateException ex)
{
Console.WriteLine($"Error reading registers: {ex.InnerException?.Message}");
}
finally
{
modbus.Dispose();
}
}
}
}

📊 性能对比数据
测试环境:.NET 9, Windows 11, RS485总线挂载8个从站,波特率9600
| 策略 | 采集成功率 | 单轮采集耗时 | 适用场景 |
|---|---|---|---|
| 无重试 | 94.2% | 2.1秒 | 实验室环境 |
| 激进策略 | 98.7% | 2.4秒 | 实时性要求高 |
| 默认策略 | 99.6% | 2.8秒 | 一般工业场景 |
| 保守策略 | 99.9% | 3.5秒 | 数据完整性优先 |
实际项目中,光有重试还不够。咱们需要:
csharpusing Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppModbusRtu
{
public class ProductionModbusCollector : IDisposable
{
private readonly string _portName;
private readonly int _baudRate;
private readonly ILogger _logger;
private readonly RetryPolicy _retryPolicy;
private ModbusRtuBasic _modbusClient;
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1);
private readonly ConcurrentDictionary<byte, SlaveStatus> _slaveStatuses = new();
private bool _isDisposed;
public ProductionModbusCollector(string portName, int baudRate,
ILogger logger, RetryPolicy policy = null)
{
_portName = portName;
_baudRate = baudRate;
_logger = logger;
_retryPolicy = policy ?? RetryPolicy.Default;
}
/// <summary>
/// 采集数据(带完整异常处理)
/// </summary>
public async Task<CollectionResult> CollectAsync(
byte slaveId, ushort startAddress, ushort count,
CancellationToken cancellationToken = default)
{
var result = new CollectionResult
{
SlaveId = slaveId,
StartAddress = startAddress,
Timestamp = DateTime.Now
};
// 检查从站熔断状态
var status = GetOrCreateSlaveStatus(slaveId);
if (status.IsCircuitOpen)
{
if (DateTime.Now < status.CircuitOpenUntil)
{
result.Success = false;
result.ErrorType = ErrorType.CircuitBreaker;
result.ErrorMessage = $"从站{slaveId}处于熔断状态,{(status.CircuitOpenUntil - DateTime.Now).TotalSeconds:F0}秒后恢复";
return result;
}
// 熔断恢复,进入半开状态
_logger?.LogInformation("从站{SlaveId} 熔断器进入半开状态,尝试恢复", slaveId);
status.IsCircuitOpen = false;
}
int attempt = 0;
Exception lastException = null;
while (attempt < _retryPolicy.MaxRetries)
{
try
{
attempt++;
// 确保连接可用
await EnsureConnectionAsync(cancellationToken);
if (attempt > 1)
{
await Task.Delay(_retryPolicy.GetDelay(attempt), cancellationToken);
}
var data = _modbusClient.ReadHoldingRegisters(slaveId, startAddress, count);
// 成功,更新状态
status.RecordSuccess();
result.Success = true;
result.Data = data;
result.Attempts = attempt;
return result;
}
catch (UnauthorizedAccessException ex)
{
// 串口被占用
lastException = ex;
_logger?.LogWarning("串口{Port}被占用,尝试重新连接", _portName);
await ReconnectAsync(cancellationToken);
}
catch (System.IO.IOException ex)
{
// 串口IO错误(可能是USB拔出)
lastException = ex;
_logger?.LogError(ex, "串口{Port} IO错误", _portName);
await ReconnectAsync(cancellationToken);
}
catch (TimeoutException ex)
{
lastException = ex;
status.RecordFailure();
_logger?.LogWarning("从站{SlaveId} 超时 ({Attempt}/{Max})",
slaveId, attempt, _retryPolicy.MaxRetries);
}
catch (InvalidDataException ex)
{
lastException = ex;
status.RecordFailure();
_logger?.LogWarning("从站{SlaveId} 数据校验失败", slaveId);
}
catch (ModbusException ex) when (ex.ExceptionCode == 0x06)
{
lastException = ex;
await Task.Delay(_retryPolicy.BusyWaitTime, cancellationToken);
}
catch (ModbusException ex)
{
// 协议层错误,不重试
status.RecordFailure();
result.Success = false;
result.ErrorType = ErrorType.Protocol;
result.ErrorMessage = ex.Message;
result.Attempts = attempt;
return result;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
lastException = ex;
_logger?.LogError(ex, "从站{SlaveId} 未预期的异常", slaveId);
}
}
// 检查是否需要熔断
if (status.ConsecutiveFailures >= 5)
{
status.OpenCircuit(TimeSpan.FromSeconds(30));
_logger?.LogWarning("从站{SlaveId} 连续失败{Count}次,触发熔断30秒",
slaveId, status.ConsecutiveFailures);
}
result.Success = false;
result.ErrorType = ErrorType.MaxRetriesExceeded;
result.ErrorMessage = lastException?.Message ?? "未知错误";
result.Attempts = attempt;
return result;
}
private async Task EnsureConnectionAsync(CancellationToken cancellationToken)
{
if (_modbusClient != null)
return;
await _connectionLock.WaitAsync(cancellationToken);
try
{
if (_modbusClient != null)
return;
_modbusClient = new ModbusRtuBasic(_portName, _baudRate);
_modbusClient.Open();
_logger?.LogInformation("串口{Port}连接成功", _portName);
}
finally
{
_connectionLock.Release();
}
}
private async Task ReconnectAsync(CancellationToken cancellationToken)
{
await _connectionLock.WaitAsync(cancellationToken);
try
{
_modbusClient?.Dispose();
_modbusClient = null;
// 等待一段时间再重连
await Task.Delay(500, cancellationToken);
_modbusClient = new ModbusRtuBasic(_portName, _baudRate);
_modbusClient.Open();
_logger?.LogInformation("串口{Port}重连成功", _portName);
}
catch (Exception ex)
{
_logger?.LogError(ex, "串口{Port}重连失败", _portName);
throw;
}
finally
{
_connectionLock.Release();
}
}
private SlaveStatus GetOrCreateSlaveStatus(byte slaveId)
{
return _slaveStatuses.GetOrAdd(slaveId, id => new SlaveStatus { SlaveId = id });
}
/// <summary>
/// 获取所有从站状态(用于监控)
/// </summary>
public IReadOnlyDictionary<byte, SlaveStatus> GetSlaveStatuses()
{
return _slaveStatuses;
}
public void Dispose()
{
if (_isDisposed) return;
_modbusClient?.Dispose();
_connectionLock.Dispose();
_isDisposed = true;
}
}
/// <summary>
/// 从站状态跟踪
/// </summary>
public class SlaveStatus
{
public byte SlaveId { get; set; }
public int TotalRequests { get; private set; }
public int SuccessCount { get; private set; }
public int FailureCount { get; private set; }
public int ConsecutiveFailures { get; private set; }
public DateTime? LastSuccessTime { get; private set; }
public DateTime? LastFailureTime { get; private set; }
public bool IsCircuitOpen { get; set; }
public DateTime CircuitOpenUntil { get; private set; }
public double SuccessRate => TotalRequests > 0
? (double)SuccessCount / TotalRequests * 100 : 0;
public void RecordSuccess()
{
TotalRequests++;
SuccessCount++;
ConsecutiveFailures = 0;
LastSuccessTime = DateTime.Now;
}
public void RecordFailure()
{
TotalRequests++;
FailureCount++;
ConsecutiveFailures++;
LastFailureTime = DateTime.Now;
}
public void OpenCircuit(TimeSpan duration)
{
IsCircuitOpen = true;
CircuitOpenUntil = DateTime.Now.Add(duration);
}
}
/// <summary>
/// 采集结果
/// </summary>
public class CollectionResult
{
public byte SlaveId { get; set; }
public ushort StartAddress { get; set; }
public DateTime Timestamp { get; set; }
public bool Success { get; set; }
public ushort[] Data { get; set; }
public int Attempts { get; set; }
public ErrorType ErrorType { get; set; }
public string ErrorMessage { get; set; }
}
public enum ErrorType
{
None,
Timeout,
CrcError,
Protocol,
Connection,
CircuitBreaker,
MaxRetriesExceeded
}
}

最后,把前面的组件整合成一个完整的采集服务:
csharpusing System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
/// <summary>
/// Modbus数据采集后台服务
/// </summary>
public class ModbusCollectionService : BackgroundService
{
private readonly ILogger<ModbusCollectionService> _logger;
private readonly ProductionModbusCollector _collector;
private readonly List<SlaveConfig> _slaveConfigs;
private readonly TimeSpan _collectionInterval;
public ModbusCollectionService(
ILogger<ModbusCollectionService> logger,
string portName,
List<SlaveConfig> slaveConfigs,
TimeSpan? interval = null)
{
_logger = logger;
_slaveConfigs = slaveConfigs;
_collectionInterval = interval ?? TimeSpan.FromSeconds(5);
var policy = RetryPolicy.Default;
_collector = new ProductionModbusCollector(portName, 9600, logger, policy);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Modbus采集服务启动");
while (!stoppingToken.IsCancellationRequested)
{
var cycleStart = DateTime.Now;
foreach (var config in _slaveConfigs)
{
if (stoppingToken.IsCancellationRequested)
break;
try
{
var result = await _collector.CollectAsync(
config.SlaveId,
config.StartAddress,
config.RegisterCount,
stoppingToken);
if (result.Success)
{
// 处理采集到的数据
ProcessData(config, result.Data);
}
else
{
_logger.LogWarning(
"从站{SlaveId}采集失败: {Error}",
config.SlaveId, result.ErrorMessage);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "从站{SlaveId}采集异常", config.SlaveId);
}
}
// 计算剩余等待时间,保证采集周期稳定
var elapsed = DateTime.Now - cycleStart;
var waitTime = _collectionInterval - elapsed;
if (waitTime > TimeSpan.Zero)
{
await Task.Delay(waitTime, stoppingToken);
}
else
{
_logger.LogWarning("采集周期超时,实际耗时{Elapsed}ms", elapsed.TotalMilliseconds);
}
}
_logger.LogInformation("Modbus采集服务停止");
_collector.Dispose();
}
private void ProcessData(SlaveConfig config, ushort[] data)
{
_logger.LogDebug("从站{SlaveId}数据: {Data}",
config.SlaveId, string.Join(", ", data));
// 这里可以:
// 1. 存入数据库
// 2. 推送到消息队列
// 3. 触发告警判断
// 4. 更新缓存供API查询
}
}
public class SlaveConfig
{
public byte SlaveId { get; set; }
public ushort StartAddress { get; set; }
public ushort RegisterCount { get; set; }
public string Description { get; set; }
}
// 使用示例
public class Program
{
public static async Task Main(string[] args)
{
var slaveConfigs = new List<SlaveConfig>
{
new() { SlaveId = 1, StartAddress = 0, RegisterCount = 10, Description = "1#温度变送器" },
new() { SlaveId = 2, StartAddress = 0, RegisterCount = 20, Description = "2#流量计" },
new() { SlaveId = 3, StartAddress = 100, RegisterCount = 5, Description = "3#液位计" }
};
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Debug);
});
var logger = loggerFactory.CreateLogger<ModbusCollectionService>();
var service = new ModbusCollectionService(logger, "COM3", slaveConfigs);
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
};
await service.StartAsync(cts.Token);
Console.WriteLine("按 Ctrl+C 停止服务...");
await Task.Delay(Timeout.Infinite, cts.Token).ContinueWith(_ => { });
await service.StopAsync(CancellationToken.None);
}
}
// 调用
using Microsoft.Extensions.Logging;
namespace AppModbusRtu
{
internal class Program
{
static async Task Main(string[] args)
{
var slaveConfigs = new List<SlaveConfig>
{
new() { SlaveId = 1, StartAddress = 0, RegisterCount = 10, Description = "1#温度变送器" },
new() { SlaveId = 0, StartAddress = 100, RegisterCount = 20, Description = "2#流量计" },
new() { SlaveId = 0, StartAddress = 200, RegisterCount = 5, Description = "3#液位计" }
};
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Debug);
});
var logger = loggerFactory.CreateLogger<ModbusCollectionService>();
var service = new ModbusCollectionService(logger, "COM1", slaveConfigs);
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
};
await service.StartAsync(cts.Token);
Console.WriteLine("按 Ctrl+C 停止服务...");
await Task.Delay(Timeout.Infinite, cts.Token).ContinueWith(_ => { });
await service.StopAsync(CancellationToken.None);
}
}
}

🤔 抛几个问题,欢迎在评论区聊聊:
你们项目里的 Modbus 超时一般设多少?有没有遇到过"测试环境没问题,现场就抽风"的情况?
对于熔断恢复后的"半开状态",你觉得应该怎么设计更合理?直接放行还是限流探测?
"超时时间不是拍脑袋,而是算出来的。" —— 传输时间 + 处理时间 + 安全余量
"重试不是万能药,但没有重试是万万不能的。" —— 关键在于区分可重试和不可重试的异常
"一个坏设备不应该拖垮整条总线。" —— 熔断机制是生产环境的保命符
收藏这两个模板,下次开工直接用:
csharpint SafeTimeout(int baudRate, int bytes, int processingMs = 100)
=> (int)(bytes * 10.0 / baudRate * 1000 * 1.5 + processingMs);
csharpint BackoffDelay(int attempt, int baseMs = 100, int maxMs = 2000)
=> (int)Math.Min(baseMs * Math.Pow(2, attempt - 1), maxMs);
📚 进阶学习路线:
System.IO.Pipelines(高性能IO场景)如果这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多做工业软件的朋友少踩坑!
有问题随时评论区见,我会一一回复。🚀
标签:#C# #Modbus #工业通信 #串口编程 #异常处理
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!