编辑
2026-04-24
C#
00

目录

🎯 开头:工业现场的"通信噩梦"
💡 问题深度剖析:为什么你的采集程序总是"抽风"?
1️⃣ 超时设置的常见误区
2️⃣ 重试策略的两个极端
3️⃣ 异常处理的"鸵鸟心态"
🔑 核心要点提炼
🛠️ 解决方案设计
方案一:基础版——带超时控制的串口通信
方案二:进阶版——智能重试机制
方案三:生产级——完整的异常处理与恢复机制
方案四:实战整合——完整的采集调度示例
💬 互动讨论
✨ 金句总结
📋 可复用代码模板
🎯 结尾:三点核心收获
#Modbus #工业通信 #串口编程 #异常处理

🎯 开头:工业现场的"通信噩梦"

搞过工业数据采集的朋友应该都有体会——Modbus RTU 这玩意儿,说简单也简单,说折腾也是真折腾。

现场二十多台设备通过485总线串联。测试环境跑得好好的,一到现场就各种抽风:时不时读不到数据、偶尔返回乱码、高峰期直接卡死。排查了三天,发现问题出在超时设置不合理、缺乏重试机制、异常处理太粗暴这三个老毛病上。

读完这篇文章,你将掌握:

  • 科学的超时参数计算方法(别再拍脑袋定500ms了)
  • 渐进式重试策略的完整实现
  • 生产级异常处理与恢复机制

咱们直接上干货,代码都是从实际项目里抽出来的,拿去就能用。


💡 问题深度剖析:为什么你的采集程序总是"抽风"?

1️⃣ 超时设置的常见误区

很多人设置超时就是凭感觉——"500毫秒应该够了吧"。但 Modbus RTU 的实际响应时间受多种因素影响:

影响因素典型延迟说明
波特率传输10-50ms9600波特率下,100字节约需104ms
从站处理5-200ms取决于设备性能和寄存器数量
485总线转发2-10ms转换器质量参差不齐
线路干扰重传0-50ms工业现场电磁环境复杂

真实案例:某项目波特率9600,读取40个寄存器。理论传输时间约85ms,但超时设为100ms后失败率高达15%。原因是没考虑从站处理时间和总线延迟。调整到350ms后,失败率降到0.3%以下。

2️⃣ 重试策略的两个极端

极端一:不重试

读取失败 → 直接报错 → 用户看到满屏红色告警

结果:偶发的通信干扰被放大成"系统故障",运维电话被打爆。

极端二:无脑重试

读取失败 → 立即重试 → 再失败 → 再重试(循环10次)

结果:一个从站卡住,整条总线的采集周期被拖长,数据实时性崩盘。

3️⃣ 异常处理的"鸵鸟心态"

我见过最离谱的代码是这样的:

csharp
try { var data = ReadRegisters(slaveId, address, count); } catch { // 吞掉所有异常,假装什么都没发生 }

这种写法在测试环境可能跑得挺欢,但到了生产环境:

  • 串口被意外占用?不知道
  • 从站地址配错了?不知道
  • 校验和错误?还是不知道

问题积累到一定程度,系统直接崩溃,排查时毫无头绪。


🔑 核心要点提炼

在动手写代码之前,咱们先把几个核心原则理清楚:

📌 超时计算公式

安全超时 = (字节数 × 10 / 波特率 × 1000) + 从站处理时间 + 安全余量

一般建议安全余量取理论传输时间的1.5-2倍。

📌 重试策略三要素

  1. 重试次数:一般2-3次足够,太多会阻塞后续采集
  2. 重试间隔:建议递增式(如100ms → 200ms → 400ms)
  3. 退避机制:连续失败时主动降低采集频率

📌 异常分级处理

异常类型处理策略是否重试
超时异常记录并重试
CRC校验错误记录并重试
串口被占用等待后重试✅(延迟重试)
从站异常响应记录具体错误码
串口不存在立即上报

🛠️ 解决方案设计

方案一:基础版——带超时控制的串口通信

先来个能跑起来的基础版本,后面再逐步增强。

csharp
using 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; } } }

image.png

⚠️ 踩坑预警

  1. SerialPort.ReadTimeout 是整体超时,不是单字节超时。数据量大时要相应调大。
  2. 一定要在发送前调用 DiscardInBuffer(),否则可能读到上次残留的数据。
  3. 帧间隔时间别省略,尤其是连续采集多个从站时。

方案二:进阶版——智能重试机制

基础版能用,但不够健壮。咱们加上重试逻辑:

csharp
using 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(); } } } }

image.png

📊 性能对比数据

测试环境:.NET 9, Windows 11, RS485总线挂载8个从站,波特率9600

策略采集成功率单轮采集耗时适用场景
无重试94.2%2.1秒实验室环境
激进策略98.7%2.4秒实时性要求高
默认策略99.6%2.8秒一般工业场景
保守策略99.9%3.5秒数据完整性优先

方案三:生产级——完整的异常处理与恢复机制

实际项目中,光有重试还不够。咱们需要:

  • 串口异常自动恢复
  • 从站状态跟踪
  • 熔断保护(防止一个坏设备拖垮整个系统)
csharp
using 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 } }

image.png


方案四:实战整合——完整的采集调度示例

最后,把前面的组件整合成一个完整的采集服务:

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

image.png


💬 互动讨论

🤔 抛几个问题,欢迎在评论区聊聊:

  1. 你们项目里的 Modbus 超时一般设多少?有没有遇到过"测试环境没问题,现场就抽风"的情况?

  2. 对于熔断恢复后的"半开状态",你觉得应该怎么设计更合理?直接放行还是限流探测?


✨ 金句总结

"超时时间不是拍脑袋,而是算出来的。" —— 传输时间 + 处理时间 + 安全余量

"重试不是万能药,但没有重试是万万不能的。" —— 关键在于区分可重试和不可重试的异常

"一个坏设备不应该拖垮整条总线。" —— 熔断机制是生产环境的保命符


📋 可复用代码模板

收藏这两个模板,下次开工直接用:

  1. 超时计算器
csharp
int SafeTimeout(int baudRate, int bytes, int processingMs = 100) => (int)(bytes * 10.0 / baudRate * 1000 * 1.5 + processingMs);
  1. 指数退避延迟
csharp
int BackoffDelay(int attempt, int baseMs = 100, int maxMs = 2000) => (int)Math.Min(baseMs * Math.Pow(2, attempt - 1), maxMs);

🎯 结尾:三点核心收获

  1. 超时要科学:根据波特率、数据量、从站性能计算,别再凭感觉了
  2. 重试要分级:区分可重试异常和协议错误,配合指数退避避免风暴
  3. 异常要兜底:串口恢复、熔断保护、状态监控三件套缺一不可

📚 进阶学习路线

  • 深入了解 Modbus TCP(以太网环境的新选择)
  • 研究 .NET 的 System.IO.Pipelines(高性能IO场景)
  • 学习分布式数据采集架构(OPC UA、MQTT)

如果这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多做工业软件的朋友少踩坑!

有问题随时评论区见,我会一一回复。🚀


标签:#C# #Modbus #工业通信 #串口编程 #异常处理

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!