调试一个工控项目时,串口数据丢包严重得要命。追查半天才发现——接收模式选错了!这让我想起刚入行那会儿,对串口通信的理解简直是一团糟。
想想看,咱们在实际项目中用串口通信时,是不是经常碰到这样的窘境?数据时快时慢,偶尔丢包,有时候还死锁。根本原因往往不是硬件问题,而是接收机制选择不当。
今天就来聊聊C#串口编程中的三种接收模式——每种都有各自的适用场景,用错了就是灾难。我会把这些年踩过的坑都抖出来,希望能帮你们少走些弯路。


做过设备通信的同学都知道,串口接收数据的方式大致分为三种:
每种模式背后的设计思路截然不同。选错了,性能和稳定性都会大打折扣。
轮询模式就像是个勤快的门卫——每隔一段时间就去看看有没有数据到达。虽然听起来很"笨",但在某些场景下反而是最稳妥的方案。
csharpprivate async Task PollingLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
ReadFromBuffer(); // 主动读取缓冲区数据
await Task.Delay(50, cancellationToken); // 50ms轮询间隔
}
catch (OperationCanceledException)
{
break; // 正常取消
}
catch (Exception ex)
{
RaiseError($"轮询接收异常:{ex.Message}");
await Task.Delay(100, cancellationToken); // 出错后稍微等等
}
}
}
适用场景:
优势:稳定性极高,逻辑简单,出问题好排查 劣势:CPU占用略高,实时性一般
我在一个水处理项目中就用过这种模式。PLC每秒只发送一次状态数据,50ms的轮询间隔完全够用,而且运行两年多没出过问题。
这是.NET SerialPort类的默认推荐方式。当有数据到达时,系统会自动触发DataReceived事件——就像门铃一样,有人来了就响。
csharpprivate void SerialPortOnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
ReadFromBuffer(); // 在事件中读取数据
}
catch (Exception ex)
{
RaiseError($"事件接收异常:{ex.Message}");
}
}
private void ReadFromBuffer()
{
if (_serialPort is null || !_serialPort.IsOpen) return;
lock (_readLock) // 加锁防止并发读取
{
var available = _serialPort.BytesToRead;
if (available <= 0) return;
var buffer = new byte[available];
var readCount = _serialPort.Read(buffer, 0, available);
if (readCount > 0)
{
// 处理实际读取到的数据
var actualData = readCount != buffer.Length
? buffer.Take(readCount).ToArray()
: buffer;
RaiseDataReceived(actualData);
}
}
}
注意:这里的锁很关键!我见过不少项目因为没加锁导致数据错乱,特别是在高频数据传输时。
适用场景:
踩坑预警:
这是最"时髦"的做法,充分利用了.NET的异步特性。不阻塞线程,性能最佳。
csharpprivate async Task AsyncReadLoop(CancellationToken cancellationToken)
{
if (_serialPort is null) return;
var buffer = new byte[1024]; // 读取缓冲区
while (!cancellationToken.IsCancellationRequested)
{
try
{
var readCount = await _serialPort.BaseStream.ReadAsync(
buffer.AsMemory(0, buffer.Length),
cancellationToken);
if (readCount > 0)
{
var data = new byte[readCount];
Buffer.BlockCopy(buffer, 0, data, 0, readCount);
RaiseDataReceived(data);
}
}
catch (OperationCanceledException)
{
break; // 正常取消
}
catch (Exception ex)
{
RaiseError($"异步读取异常:{ex.Message}");
await Task.Delay(100, cancellationToken); // 避免死循环
}
}
}
适用场景:
优势:性能最佳,线程利用率高,扩展性好 劣势:复杂度稍高,对.NET版本有要求
在我最近的一个数据采集项目中,需要同时处理8个串口的数据,异步模式的优势就体现出来了——CPU占用率比事件模式低了30%。
实际项目中,不同阶段可能需要不同的接收模式。比如调试时用轮询(方便观察),生产环境用事件驱动(稳定),高负载时切换到异步。
csharppublic void SwitchMode(ReceiveMode mode)
{
if (!IsOpen)
{
CurrentMode = mode; // 串口未打开,只记录模式
return;
}
StopReceive(); // 停止当前接收
CurrentMode = mode; // 更新模式
StartReceive(mode); // 启动新模式
}
private void StartReceive(ReceiveMode mode)
{
if (!IsOpen || _serialPort is null) return;
_receiveCts = new CancellationTokenSource();
switch (mode)
{
case ReceiveMode.Polling:
_receiveTask = Task.Run(() => PollingLoop(_receiveCts.Token));
break;
case ReceiveMode.EventDriven:
_serialPort.DataReceived += SerialPortOnDataReceived;
break;
case ReceiveMode.AsyncRead:
_receiveTask = Task.Run(() => AsyncReadLoop(_receiveCts.Token));
break;
}
}
这种设计让你可以在运行时动态调整策略。比如检测到数据量突然增大时,自动切换到异步模式。
别小看缓冲区的处理,这里面学问大着呢:
csharpprivate void ReadFromBuffer()
{
// ... 前面的检查代码
var available = _serialPort.BytesToRead;
if (available <= 0) return;
var buffer = new byte[available];
var readCount = _serialPort.Read(buffer, 0, available);
// 关键:处理实际读取的数据量
if (readCount != buffer.Length)
{
var actual = new byte[readCount];
Buffer.BlockCopy(buffer, 0, actual, 0, readCount);
buffer = actual;
}
RaiseDataReceived(buffer);
}
为什么要这样处理?因为BytesToRead和实际Read到的数据量可能不一致。我就遇到过BytesToRead返回100,但实际只读取到95字节的情况。
串口编程中,异常处理绝对不能马虎:
csharpprivate void StopReceive()
{
// 先取消事件订阅
if (_serialPort is not null)
_serialPort.DataReceived -= SerialPortOnDataReceived;
if (_receiveCts is null) return;
try
{
_receiveCts.Cancel();
_receiveTask?.Wait(600); // 给600ms等待时间
}
catch (AggregateException ex) when (ex.InnerExceptions.All(
e => e is TaskCanceledException or OperationCanceledException))
{
// 取消异常是正常的,忽略
}
catch (Exception ex)
{
RaiseError($"停止接收任务异常:{ex.Message}");
}
finally
{
_receiveCts.Dispose();
_receiveCts = null;
_receiveTask = null;
}
}
注意那个when条件子句——这是C# 6.0引入的异常过滤器,可以精确处理特定类型的异常。
频繁的byte数组分配会给GC造成压力,特别是在高频数据传输时:
csharp// ❌ 错误做法:每次都new
private void BadReadFromBuffer()
{
var buffer = new byte[_serialPort.BytesToRead]; // 每次分配新数组
// ...
}
// ✅ 正确做法:复用缓冲区(在异步模式中)
private async Task AsyncReadLoop(CancellationToken cancellationToken)
{
var buffer = new byte[1024]; // 在循环外分配一次
while (!cancellationToken.IsCancellationRequested)
{
var readCount = await _serialPort.BaseStream.ReadAsync(
buffer.AsMemory(0, buffer.Length), cancellationToken);
if (readCount > 0)
{
// 只有在需要传递数据时才分配新数组
var data = new byte[readCount];
Buffer.BlockCopy(buffer, 0, data, 0, readCount);
RaiseDataReceived(data);
}
}
}
别忽视统计功能,这在生产环境排查问题时特别有用:
csharppublic long ReceivedBytes { get; private set; }
public long SentBytes { get; private set; }
private void RaiseDataReceived(byte[] data)
{
ReceivedBytes += data.Length; // 统计接收字节数
DataReceived?.Invoke(this, data);
}
public void SendHex(string hexText)
{
var buffer = ParseHexString(hexText);
_serialPort.Write(buffer, 0, buffer.Length);
SentBytes += buffer.Length; // 统计发送字节数
}
有了这些统计数据,你可以很容易发现通信异常——比如只有发送没有接收,或者数据量突然骤减等。
c#using System.IO.Ports;
using System.Text;
namespace AppSerial01;
public enum ReceiveMode
{
Polling,
EventDriven,
AsyncRead
}
public sealed class SerialService : IDisposable
{
private readonly object _readLock = new();
private SerialPort? _serialPort;
private CancellationTokenSource? _receiveCts;
private Task? _receiveTask;
public event EventHandler<byte[]>? DataReceived;
public event EventHandler<string>? ErrorOccurred;
public ReceiveMode CurrentMode { get; private set; } = ReceiveMode.EventDriven;
public bool IsOpen => _serialPort?.IsOpen == true;
public long ReceivedBytes { get; private set; }
public long SentBytes { get; private set; }
public void Open(string portName, int baudRate, int dataBits, StopBits stopBits, Parity parity, ReceiveMode mode)
{
Close();
try
{
_serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
{
ReadTimeout = 500,
WriteTimeout = 500,
Encoding = Encoding.ASCII
};
_serialPort.Open();
CurrentMode = mode;
StartReceive(mode);
}
catch (Exception ex)
{
RaiseError($"打开串口失败:{ex.Message}");
Close();
throw;
}
}
public void SwitchMode(ReceiveMode mode)
{
if (!IsOpen)
{
CurrentMode = mode;
return;
}
StopReceive();
CurrentMode = mode;
StartReceive(mode);
}
public void SendText(string text)
{
if (!IsOpen || _serialPort is null)
{
throw new InvalidOperationException("串口未打开。");
}
_serialPort.Write(text);
SentBytes += _serialPort.Encoding.GetByteCount(text);
}
public void SendHex(string hexText)
{
if (!IsOpen || _serialPort is null)
{
throw new InvalidOperationException("串口未打开。");
}
var buffer = ParseHexString(hexText);
_serialPort.Write(buffer, 0, buffer.Length);
SentBytes += buffer.Length;
}
public void ResetStatistics()
{
ReceivedBytes = 0;
SentBytes = 0;
}
public void Close()
{
StopReceive();
if (_serialPort is null)
{
return;
}
try
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
}
catch (Exception ex)
{
RaiseError($"关闭串口异常:{ex.Message}");
}
finally
{
_serialPort.Dispose();
_serialPort = null;
}
}
private void StartReceive(ReceiveMode mode)
{
if (!IsOpen || _serialPort is null)
{
return;
}
_receiveCts = new CancellationTokenSource();
switch (mode)
{
case ReceiveMode.Polling:
_receiveTask = Task.Run(() => PollingLoop(_receiveCts.Token));
break;
case ReceiveMode.EventDriven:
_serialPort.DataReceived += SerialPortOnDataReceived;
break;
case ReceiveMode.AsyncRead:
_receiveTask = Task.Run(() => AsyncReadLoop(_receiveCts.Token));
break;
}
}
private void StopReceive()
{
if (_serialPort is not null)
{
_serialPort.DataReceived -= SerialPortOnDataReceived;
}
if (_receiveCts is null)
{
return;
}
try
{
_receiveCts.Cancel();
_receiveTask?.Wait(600);
}
catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is TaskCanceledException or OperationCanceledException))
{
}
catch (Exception ex)
{
RaiseError($"停止接收任务异常:{ex.Message}");
}
finally
{
_receiveCts.Dispose();
_receiveCts = null;
_receiveTask = null;
}
}
private async Task PollingLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
ReadFromBuffer();
await Task.Delay(50, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
RaiseError($"轮询接收异常:{ex.Message}");
await Task.Delay(100, cancellationToken);
}
}
}
private async Task AsyncReadLoop(CancellationToken cancellationToken)
{
if (_serialPort is null)
{
return;
}
var buffer = new byte[1024];
while (!cancellationToken.IsCancellationRequested)
{
try
{
var readCount = await _serialPort.BaseStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
if (readCount > 0)
{
var data = new byte[readCount];
Buffer.BlockCopy(buffer, 0, data, 0, readCount);
RaiseDataReceived(data);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
RaiseError($"异步读取异常:{ex.Message}");
await Task.Delay(100, cancellationToken);
}
}
}
private void SerialPortOnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
ReadFromBuffer();
}
catch (Exception ex)
{
RaiseError($"事件接收异常:{ex.Message}");
}
}
private void ReadFromBuffer()
{
if (_serialPort is null || !_serialPort.IsOpen)
{
return;
}
lock (_readLock)
{
var available = _serialPort.BytesToRead;
if (available <= 0)
{
return;
}
var buffer = new byte[available];
var readCount = _serialPort.Read(buffer, 0, available);
if (readCount <= 0)
{
return;
}
if (readCount != buffer.Length)
{
var actual = new byte[readCount];
Buffer.BlockCopy(buffer, 0, actual, 0, readCount);
buffer = actual;
}
RaiseDataReceived(buffer);
}
}
private static byte[] ParseHexString(string hexText)
{
var cleaned = hexText.Replace(" ", string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty);
if (cleaned.Length == 0)
{
return Array.Empty<byte>();
}
if (cleaned.Length % 2 != 0)
{
throw new FormatException("十六进制字符串长度必须为偶数。");
}
var bytes = new byte[cleaned.Length / 2];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(cleaned.Substring(i * 2, 2), 16);
}
return bytes;
}
private void RaiseDataReceived(byte[] data)
{
ReceivedBytes += data.Length;
DataReceived?.Invoke(this, data);
}
private void RaiseError(string message)
{
ErrorOccurred?.Invoke(this, message);
}
public void Dispose()
{
Close();
GC.SuppressFinalize(this);
}
}
| 场景类型 | 推荐模式 | 理由 |
|---|---|---|
| 工控设备,低频数据 | 轮询 | 稳定性最重要 |
| 通用串口应用 | 事件驱动 | 平衡性能和复杂度 |
| 高性能数据采集 | 异步读取 | 充分利用系统资源 |
| 多串口并发 | 异步读取 | 线程效率最高 |
| 调试阶段 | 轮询 | 便于单步调试 |
| 嵌入式通信 | 事件驱动 | 兼容性好 |
当然,这只是经验总结。实际项目中,最好是三种模式都测试一下,根据具体表现来决定。
串口通信看似简单,实则暗藏玄机。选对接收模式,能让你的应用稳如老狗;选错了,就是无尽的调试地狱。
这三种模式各有千秋:
最后想问大家:你们在串口项目中都踩过哪些坑?有没有遇到过模式选择导致的性能问题?欢迎在评论区分享你们的实战经验。
技术标签:#C#开发 #串口通信 #异步编程 #性能优化 #设备通信
相关信息
通过网盘分享的文件:AppSerial01.zip 链接: https://pan.baidu.com/s/1K-agyEfZ8cO0CnllG2k6VQ?pwd=siic 提取码: siic --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!