C#串口通信完全指南:從連接到數(shù)據(jù)傳輸?shù)淖罴褜嵺`
作為一名.NET開發(fā)者,你是否曾為串口通信的穩(wěn)定性問題而頭疼?設(shè)備連接不穩(wěn)定、數(shù)據(jù)丟失、界面卡頓…這些都是工業(yè)軟件開發(fā)中的常見痛點。
今天,我將通過一個完整的串口通信解決方案,帶你徹底搞定C#串口編程的所有技術(shù)難點。這套方案已在多個工業(yè)項目中驗證,具備異步處理、緩沖優(yōu)化、連接管理等企業(yè)級特性。
核心問題分析
傳統(tǒng)串口編程的三大痛點
1. 連接狀態(tài)管理混亂
// 錯誤示例:狀態(tài)管理不一致
if (serialPort.IsOpen) // 這里可能拋異常
{
serialPort.Write(data); // 寫入時端口可能已斷開
}2. 數(shù)據(jù)接收不完整
// 錯誤示例:可能丟失數(shù)據(jù)
private void DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = serialPort.ReadExisting(); // 可能讀取不完整
}3. UI線程阻塞
// 錯誤示例:同步操作阻塞界面
serialPort.Write(data); // 可能導(dǎo)致界面卡頓企業(yè)級解決方案設(shè)計
架構(gòu)設(shè)計思路
我們的解決方案采用三層架構(gòu):
- 配置層
SerialPortConfig- 統(tǒng)一管理連接參數(shù) - 處理層
OptimizedSerialPortHandler- 核心通信邏輯 - 界面層
Form1- 用戶交互和狀態(tài)展示
核心代碼實現(xiàn)
1. 配置管理類
public class SerialPortConfig
{
publicstring PortName { get; set; }
publicint BaudRate { get; set; }
public Parity Parity { get; set; }
publicint DataBits { get; set; }
public StopBits StopBits { get; set; }
publicint ReadBufferSize { get; set; } = 4096;
publicint WriteBufferSize { get; set; } = 4096;
publicint ReadTimeout { get; set; } = 500;
publicint WriteTimeout { get; set; } = 500;
}設(shè)計亮點:
- 默認值設(shè)置合理(4KB緩沖區(qū))
- 支持完整的串口參數(shù)配置
- 便于后期擴展和維護
2. 連接狀態(tài)管理
private bool _isConnected = false;
private readonly object _lockObject = new object();
publicbool IsConnected => _isConnected && (_serialPort?.IsOpen ?? false);
public async Task<bool> ConnectAsync(SerialPortConfig config)
{
try
{
lock (_lockObject)
{
if (_serialPort?.IsOpen == true)
{
_serialPort.Close();
_isConnected = false;
}
_serialPort = new SerialPort(config.PortName, config.BaudRate,
config.Parity, config.DataBits, config.StopBits)
{
ReadBufferSize = config.ReadBufferSize,
WriteBufferSize = config.WriteBufferSize,
ReadTimeout = config.ReadTimeout,
WriteTimeout = config.WriteTimeout
};
}
await Task.Run(() => _serialPort.Open());
_isConnected = true;
returntrue;
}
catch (Exception ex)
{
_isConnected = false;
ErrorOccurred?.Invoke($"連接失敗: {ex.Message}");
returnfalse;
}
}技術(shù)要點:
- ? 雙重狀態(tài)檢查(
_isConnected+IsOpen) - ? 線程安全的鎖機制
- ? 異步連接避免UI阻塞
- ? 完善的異常處理
3. 異步數(shù)據(jù)發(fā)送
// 同步發(fā)送 - 適用于小數(shù)據(jù)量
public void SendData(byte[] data)
{
if (!IsConnected) return;
try
{
lock (_lockObject)
{
if (_serialPort.IsOpen)
{
_serialPort.Write(data, 0, data.Length);
}
}
}
catch (TimeoutException ex)
{
ErrorOccurred?.Invoke($"發(fā)送超時: {ex.Message}");
}
}
// 異步發(fā)送 - 適用于大數(shù)據(jù)量或高頻發(fā)送
public async Task<bool> SendDataAsync(byte[] data, int timeoutMilliseconds = 1000)
{
if (!IsConnected) returnfalse;
var timeoutCts = new CancellationTokenSource(timeoutMilliseconds);
try
{
await Task.Run(() =>
{
lock (_lockObject)
{
if (_serialPort.IsOpen)
{
_serialPort.Write(data, 0, data.Length);
}
}
}, timeoutCts.Token);
returntrue;
}
catch (OperationCanceledException)
{
ErrorOccurred?.Invoke("發(fā)送操作超時");
returnfalse;
}
}4. 優(yōu)化的數(shù)據(jù)接收
private async void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
byte[] data = await ReadDataAsync();
if (data.Length > 0)
{
DataReceived?.Invoke(data);
}
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"數(shù)據(jù)接收異常: {ex.Message}");
}
}
private async Task<byte[]> ReadDataAsync()
{
using (var ms = new MemoryStream())
{
try
{
int bytesRead;
do
{
bytesRead = await _serialPort.BaseStream.ReadAsync(
_receiveBuffer, 0, _receiveBuffer.Length);
if (bytesRead > 0)
{
ms.Write(_receiveBuffer, 0, bytesRead);
}
} while (bytesRead > 0 && _serialPort.BytesToRead > 0);
return ms.ToArray();
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"讀取數(shù)據(jù)失敗: {ex.Message}");
returnnew byte[0];
}
}
}核心優(yōu)勢:
- ?? 內(nèi)存流緩沖:避免數(shù)據(jù)分片丟失
- ?? 循環(huán)讀取:確保獲取完整數(shù)據(jù)包
- ?? 異步處理:不阻塞事件線程
UI交互優(yōu)化
連接狀態(tài)實時反饋
private void UpdateConnectionStatus(bool isConnected)
{
if (isConnected)
{
lblStatus.Text = "狀態(tài): 已連接";
lblStatus.ForeColor = Color.FromArgb(76, 175, 80); // 綠色
pnlConfig.Enabled = false; // 禁用配置面板
btnConnect.Enabled = false;
btnDisconnect.Enabled = true;
}
else
{
lblStatus.Text = "狀態(tài): 未連接";
lblStatus.ForeColor = Color.FromArgb(244, 67, 54); // 紅色
pnlConfig.Enabled = true; // 啟用配置面板
btnConnect.Enabled = true;
btnDisconnect.Enabled = false;
}
}數(shù)據(jù)統(tǒng)計展示
private void OnDataReceived(byte[] data)
{
if (InvokeRequired)
{
Invoke(new Action<byte[]>(OnDataReceived), data);
return;
}
_bytesReceived += data.Length;
lblBytesReceived.Text = $"接收: {_bytesReceived} 字節(jié)";
string text = Encoding.UTF8.GetString(data);
rtbReceived.SelectionColor = Color.Green;
rtbReceived.AppendText($"[{DateTime.Now:HH:mm:ss}] 接收: {text}\n");
rtbReceived.ScrollToCaret();
}完整代碼
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppSerialPortBuffer
{
publicclass SerialPortConfig
{
publicstring PortName { get; set; }
publicint BaudRate { get; set; }
public Parity Parity { get; set; }
publicint DataBits { get; set; }
public StopBits StopBits { get; set; }
publicint ReadBufferSize { get; set; } = 4096;
publicint WriteBufferSize { get; set; } = 4096;
publicint ReadTimeout { get; set; } = 500;
publicint WriteTimeout { get; set; } = 500;
}
}using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppSerialPortBuffer
{
publicclass OptimizedSerialPortHandler : IDisposable
{
private SerialPort _serialPort;
private CancellationTokenSource _cancellationTokenSource;
private byte[] _receiveBuffer;
privatebool _disposed = false;
privatebool _isConnected = false;
private readonly object _lockObject = new object();
public event Action<byte[]> DataReceived;
public event Action<string> ErrorOccurred;
publicbool IsConnected => _isConnected && (_serialPort?.IsOpen ?? false);
public async Task<bool> ConnectAsync(SerialPortConfig config)
{
try
{
lock (_lockObject)
{
if (_serialPort?.IsOpen == true)
{
_serialPort.Close();
_isConnected = false;
}
_serialPort = new SerialPort(config.PortName, config.BaudRate, config.Parity, config.DataBits, config.StopBits)
{
ReadBufferSize = config.ReadBufferSize,
WriteBufferSize = config.WriteBufferSize,
ReadTimeout = config.ReadTimeout,
WriteTimeout = config.WriteTimeout,
Handshake = Handshake.None
};
_receiveBuffer = new byte[config.ReadBufferSize];
_serialPort.DataReceived += SerialPort_DataReceived;
_serialPort.ErrorReceived += SerialPort_ErrorReceived;
}
await Task.Run(() => _serialPort.Open());
_cancellationTokenSource = new CancellationTokenSource();
_isConnected = true; // 連接成功后設(shè)置標志
returntrue;
}
catch (Exception ex)
{
_isConnected = false; // 連接失敗時確保標志為false
ErrorOccurred?.Invoke($"連接失敗: {ex.Message}");
returnfalse;
}
}
public void Disconnect()
{
try
{
_isConnected = false; // 立即設(shè)置連接狀態(tài)為false
_cancellationTokenSource?.Cancel();
lock (_lockObject)
{
if (_serialPort?.IsOpen == true)
{
_serialPort.DataReceived -= SerialPort_DataReceived;
_serialPort.ErrorReceived -= SerialPort_ErrorReceived;
_serialPort.Close();
}
}
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"斷開連接時出錯: {ex.Message}");
}
finally
{
_isConnected = false; // 確保在任何情況下都設(shè)置為false
}
}
// 同步發(fā)送
public void SendData(byte[] data)
{
if (!IsConnected) return;
try
{
lock (_lockObject)
{
if (_serialPort.IsOpen)
{
_serialPort.Write(data, 0, data.Length);
}
}
}
catch (TimeoutException ex)
{
ErrorOccurred?.Invoke($"發(fā)送超時: {ex.Message}");
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"發(fā)送失敗: {ex.Message}");
}
}
// 異步發(fā)送
public async Task<bool> SendDataAsync(byte[] data, int timeoutMilliseconds = 1000)
{
if (!IsConnected) returnfalse;
var timeoutCts = new CancellationTokenSource(timeoutMilliseconds);
try
{
await Task.Run(() =>
{
lock (_lockObject)
{
if (_serialPort.IsOpen)
{
_serialPort.Write(data, 0, data.Length);
}
}
}, timeoutCts.Token);
returntrue;
}
catch (OperationCanceledException)
{
ErrorOccurred?.Invoke("發(fā)送操作超時");
returnfalse;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"異步發(fā)送失敗: {ex.Message}");
returnfalse;
}
}
private async void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
// 異步讀取數(shù)據(jù),提高響應(yīng)速度
byte[] data = await ReadDataAsync();
if (data.Length > 0)
{
DataReceived?.Invoke(data);
}
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"數(shù)據(jù)接收異常: {ex.Message}");
}
}
// 異步讀取數(shù)據(jù) - 使用內(nèi)存流優(yōu)化
private async Task<byte[]> ReadDataAsync()
{
using (var ms = new MemoryStream())
{
try
{
int bytesRead;
do
{
bytesRead = await _serialPort.BaseStream.ReadAsync(_receiveBuffer, 0, _receiveBuffer.Length);
if (bytesRead > 0)
{
ms.Write(_receiveBuffer, 0, bytesRead);
}
} while (bytesRead > 0 && _serialPort.BytesToRead > 0);
return ms.ToArray();
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"讀取數(shù)據(jù)失敗: {ex.Message}");
returnnew byte[0];
}
}
}
private void SerialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{
ErrorOccurred?.Invoke($"串口錯誤: {e.EventType}");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// 釋放托管資源
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_serialPort?.Close();
_serialPort?.Dispose();
}
// 釋放非托管資源
_receiveBuffer = null;
_disposed = true;
}
}
}
}using System.IO.Ports;
using System.Text;
using Timer = System.Windows.Forms.Timer;
namespace AppSerialPortBuffer
{
public partial class Form1 : Form
{
private OptimizedSerialPortHandler _serialHandler;
private Timer _statusTimer;
privatelong _bytesSent = 0;
privatelong _bytesReceived = 0;
public Form1()
{
InitializeComponent();
InitializeApplication();
}
private void InitializeApplication()
{
_serialHandler = new OptimizedSerialPortHandler();
_serialHandler.DataReceived += OnDataReceived;
_serialHandler.ErrorOccurred += OnErrorOccurred;
// 初始化端口列表
RefreshPortList();
// 初始化波特率
cmbBaudRate.Items.AddRange(new object[] { 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600 });
cmbBaudRate.SelectedIndex = 4; // 115200
// 初始化校驗位
foreach (Parity parity in Enum.GetValues(typeof(Parity)))
cmbParity.Items.Add(parity);
cmbParity.SelectedIndex = 0; // None
// 初始化數(shù)據(jù)位
cmbDataBits.Items.AddRange(new object[] { 7, 8 });
cmbDataBits.SelectedIndex = 1; // 8
// 初始化停止位
foreach (StopBits stopBits in Enum.GetValues(typeof(StopBits)))
cmbStopBits.Items.Add(stopBits);
cmbStopBits.SelectedIndex = 1; // One
btnConnect.Enabled = true;
btnDisconnect.Enabled = false;
// 狀態(tài)定時器
_statusTimer = new Timer();
_statusTimer.Interval = 1000;
_statusTimer.Tick += StatusTimer_Tick;
_statusTimer.Start();
}
private void RefreshPortList()
{
cmbPort.Items.Clear();
cmbPort.Items.AddRange(SerialPort.GetPortNames());
if (cmbPort.Items.Count > 0)
cmbPort.SelectedIndex = 0;
}
private async void BtnConnect_Click(object sender, EventArgs e)
{
if (cmbPort.SelectedItem == null) return;
try
{
var config = new SerialPortConfig
{
PortName = cmbPort.SelectedItem.ToString(),
BaudRate = (int)cmbBaudRate.SelectedItem,
Parity = (Parity)cmbParity.SelectedItem,
DataBits = (int)cmbDataBits.SelectedItem,
StopBits = (StopBits)cmbStopBits.SelectedItem,
ReadBufferSize = (int)nudReadBuffer.Value,
WriteBufferSize = (int)nudWriteBuffer.Value,
ReadTimeout = (int)nudTimeout.Value,
WriteTimeout = (int)nudTimeout.Value
};
pgbConnection.Style = ProgressBarStyle.Marquee;
tsslStatus.Text = "正在連接...";
// 連接期間禁用連接按鈕
btnConnect.Enabled = false;
bool success = await _serialHandler.ConnectAsync(config);
pgbConnection.Style = ProgressBarStyle.Continuous;
pgbConnection.Value = success ? 100 : 0;
if (success)
{
UpdateConnectionStatus(true);
tsslStatus.Text = "連接成功";
}
else
{
UpdateConnectionStatus(false);
tsslStatus.Text = "連接失敗";
MessageBox.Show("連接失敗,請檢查端口設(shè)置!", "錯誤",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception ex)
{
UpdateConnectionStatus(false);
tsslStatus.Text = "連接失敗";
MessageBox.Show("連接失敗,請檢查端口設(shè)置!", "錯誤",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void BtnDisconnect_Click(object sender, EventArgs e)
{
try
{
// 先禁用按鈕,防止重復(fù)點擊
btnDisconnect.Enabled = false;
_serialHandler.Disconnect();
// 強制更新UI狀態(tài)
UpdateConnectionStatus(false);
tsslStatus.Text = "已斷開連接";
}
catch (Exception ex)
{
MessageBox.Show($"斷開連接時發(fā)生錯誤: {ex.Message}", "錯誤",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
// 即使出錯也要更新狀態(tài)
UpdateConnectionStatus(false);
}
}
private void UpdateConnectionStatus(bool isConnected)
{
if (isConnected)
{
lblStatus.Text = "狀態(tài): 已連接";
lblStatus.ForeColor = Color.FromArgb(76, 175, 80);
pnlConfig.Enabled = false;
grpSettings.Enabled = false;
btnConnect.Enabled = false;
btnDisconnect.Enabled = true;
pgbConnection.Value = 100;
}
else
{
lblStatus.Text = "狀態(tài): 未連接";
lblStatus.ForeColor = Color.FromArgb(244, 67, 54);
pnlConfig.Enabled = true;
grpSettings.Enabled = true;
btnConnect.Enabled = true;
btnDisconnect.Enabled = false;
pgbConnection.Value = 0;
}
}
private async void BtnSend_Click(object sender, EventArgs e)
{
if (!_serialHandler.IsConnected || string.IsNullOrEmpty(txtSend.Text))
return;
try
{
byte[] data = Encoding.UTF8.GetBytes(txtSend.Text);
if (chkAsync.Checked)
{
await _serialHandler.SendDataAsync(data);
}
else
{
_serialHandler.SendData(data);
}
_bytesSent += data.Length;
lblBytesSent.Text = $"發(fā)送: {_bytesSent} 字節(jié)";
rtbReceived.SelectionColor = Color.Blue;
rtbReceived.AppendText($"[{DateTime.Now:HH:mm:ss}] 發(fā)送: {txtSend.Text}\n");
rtbReceived.ScrollToCaret();
txtSend.Clear();
}
catch (Exception ex)
{
MessageBox.Show($"發(fā)送失敗: {ex.Message}", "錯誤",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void BtnClear_Click(object sender, EventArgs e)
{
rtbReceived.Clear();
_bytesSent = 0;
_bytesReceived = 0;
lblBytesSent.Text = "發(fā)送: 0 字節(jié)";
lblBytesReceived.Text = "接收: 0 字節(jié)";
}
private void TxtSend_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
BtnSend_Click(sender, e);
e.Handled = true;
}
}
private void OnDataReceived(byte[] data)
{
if (InvokeRequired)
{
Invoke(new Action<byte[]>(OnDataReceived), data);
return;
}
_bytesReceived += data.Length;
lblBytesReceived.Text = $"接收: {_bytesReceived} 字節(jié)";
string text = Encoding.UTF8.GetString(data);
rtbReceived.SelectionColor = Color.Green;
rtbReceived.AppendText($"[{DateTime.Now:HH:mm:ss}] 接收: {text}\n");
rtbReceived.ScrollToCaret();
}
private void OnErrorOccurred(string error)
{
if (InvokeRequired)
{
Invoke(new Action<string>(OnErrorOccurred), error);
return;
}
rtbReceived.SelectionColor = Color.Red;
rtbReceived.AppendText($"[{DateTime.Now:HH:mm:ss}] 錯誤: {error}\n");
rtbReceived.ScrollToCaret();
}
private void StatusTimer_Tick(object sender, EventArgs e)
{
tsslTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_serialHandler?.Disconnect();
_statusTimer?.Stop();
base.OnFormClosing(e);
}
}
}
圖片
常見坑點提醒
1. 資源釋放問題
// ? 錯誤:忘記釋放資源
~OptimizedSerialPortHandler()
{
// 析構(gòu)函數(shù)中不應(yīng)該訪問托管資源
_serialPort?.Close(); // 可能引發(fā)異常
}
// ? 正確:標準Dispose模式
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_cancellationTokenSource?.Cancel();
_serialPort?.Close();
_serialPort?.Dispose();
}
_disposed = true;
}2. 跨線程操作
// ? 錯誤:直接在數(shù)據(jù)事件中更新UI
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
textBox1.Text = serialPort.ReadExisting(); // 跨線程異常
}
// ? 正確:檢查并使用Invoke
private void OnDataReceived(byte[] data)
{
if (InvokeRequired)
{
Invoke(new Action<byte[]>(OnDataReceived), data);
return;
}
// 安全更新UI
}3. 連接檢查時機
// ? 錯誤:只檢查IsOpen
if (serialPort.IsOpen)
{
serialPort.Write(data); // 可能在寫入前斷開
}
// ? 正確:雙重檢查 + 異常處理
publicbool IsConnected => _isConnected && (_serialPort?.IsOpen ?? false);
if (IsConnected)
{
try
{
lock (_lockObject)
{
if (_serialPort.IsOpen)
{
_serialPort.Write(data, 0, data.Length);
}
}
}
catch (Exception ex)
{
// 處理異常并更新狀態(tài)
}
}實際應(yīng)用場景
工業(yè)設(shè)備監(jiān)控
- 應(yīng)用PLC數(shù)據(jù)采集、傳感器讀數(shù)
- 優(yōu)勢穩(wěn)定的長時間運行,完整的數(shù)據(jù)接收
嵌入式開發(fā)調(diào)試
- 應(yīng)用單片機程序調(diào)試、固件升級
- 優(yōu)勢高效的雙向通信,實時狀態(tài)反饋
自動化測試系統(tǒng)
- 應(yīng)用設(shè)備功能測試、生產(chǎn)線檢測
- 優(yōu)勢批量操作支持,詳細的日志記錄
性能優(yōu)化建議
緩沖區(qū)設(shè)置
// 根據(jù)數(shù)據(jù)特點調(diào)整緩沖區(qū)大小
public SerialPortConfig GetOptimalConfig(DataPattern pattern)
{
return pattern switch
{
DataPattern.HighFrequency => new() { ReadBufferSize = 8192 },
DataPattern.LargePacket => new() { ReadBufferSize = 16384 },
DataPattern.Normal => new() { ReadBufferSize = 4096 },
_ => new()
};
}內(nèi)存管理
- 使用
MemoryStream代替字符串拼接 - 及時釋放不用的字節(jié)數(shù)組
- 考慮使用對象池減少GC壓力
總結(jié)與展望
通過本文的完整方案,我們解決了C#串口通信的三大核心問題:
- 連接管理雙重狀態(tài)檢查 + 線程安全機制
- 數(shù)據(jù)完整性內(nèi)存流緩沖 + 循環(huán)讀取策略
- 性能優(yōu)化異步處理 + 合理的資源管理
這套解決方案已在多個工業(yè)項目中驗證,具備良好的穩(wěn)定性和擴展性。無論是設(shè)備調(diào)試還是生產(chǎn)環(huán)境,都能提供可靠的通信保障。
技術(shù)成長建議:
- 深入理解.NET的異步編程模型
- 掌握多線程和線程安全的最佳實踐
- 關(guān)注工業(yè)通信協(xié)議(Modbus、Profinet等)
?? 互動話題:
- 你在串口通信開發(fā)中遇到過哪些棘手問題?
- 對于高并發(fā)的串口設(shè)備管理,你有什么優(yōu)化思路?
覺得這篇技術(shù)總結(jié)對你有幫助嗎?請轉(zhuǎn)發(fā)給更多需要的同行,讓我們一起提升C#開發(fā)技能!






















