首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計綜合問答

C# SerialPort 串口通訊:非同步接收與輪詢發送的正確模式

     編輯:程式設計綜合問答

問題現象

上位機需循環發送指令並接收回應,但使用 Thread.Sleep 封鎖 UI 執行緒導致 DataReceived 事件無法觸發,造成「發送完畢後才開始接收」的假象。

根本原因

  • Thread.Sleep 阻塞了處理訊息迴圈的執行緒(通常是 UI 執行緒),導致 Control.Invoke 無法執行,DataReceived 事件排隊等待。
  • 同步阻塞模式不適合事件驅動的串口通訊架構。

解決方案:非同步事件驅動 + 狀態機

核心原則

  1. 發送指令後立即返回,不阻塞等待回應。
  2. 在 DataReceived 事件中解析完整封包,觸發自訂事件或回呼。
  3. 使用狀態機或 TaskCompletionSource 管理「發送-等待回應」的邏輯流程。

改良程式碼範例

public partial class MainForm : Form {
    private SerialPort _port;
    private readonly Queue _sendQueue = new Queue();
    private byte[] _expectedResponseHeader; // 用於識別回應封包
    private TaskCompletionSource _responseTcs;

    public MainForm() {
        InitializeComponent();
        _port = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
        _port.DataReceived += OnDataReceived;
        _port.Open();
    }

    // 非同步發送並等待回應
    public async Task SendCommandAsync(byte[] cmd, byte[] expectedHeader, int timeoutMs = 3000) {
        lock (_sendQueue) {
            _expectedResponseHeader = expectedHeader;
            _responseTcs = new TaskCompletionSource();
            _port.Write(cmd, 0, cmd.Length);
        }
        
        using var cts = new CancellationTokenSource(timeoutMs);
        try {
            return await _responseTcs.Task.WaitAsync(cts.Token);
        } catch (OperationCanceledException) {
            throw new TimeoutException("串口回應逾時");
        }
    }

    private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) {
        int bytes = _port.BytesToRead;
        byte[] buffer = new byte[bytes];
        _port.Read(buffer, 0, bytes);
        
        // 簡單封包判斷:檢查Header
        if (_responseTcs != null && buffer.Length >= _expectedResponseHeader.Length) {
            bool match = true;
            for (int i = 0; i < _expectedResponseHeader.Length; i++) {
                if (buffer[i] != _expectedResponseHeader[i]) { match = false; break; }
            }
            if (match) {
                _responseTcs.TrySetResult(buffer);
                _responseTcs = null;
            }
        }
        // 若需處理黏包/分包,請實作緩衝區累積與封包解析邏輯
    }

    // 使用範例:按鈕點擊發送指令
    private async void btnTest_Click(object sender, EventArgs e) {
        try {
            byte[] cmd = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 }; // Modbus 範例
            byte[] header = { 0x01, 0x03 };
            byte[] resp = await SendCommandAsync(cmd, header);
            txtLog.AppendText("收到: " + BitConverter.ToString(resp) + Environment.NewLine);
        } catch (Exception ex) {
            MessageBox.Show(ex.Message);
        }
    }
}

關鍵改進點

  • 移除 Thread.Sleep + Application.DoEvents:改用 async/await 非同步等待,UI 保持回應。
  • 封裝發送/接收邏輯:SendCommandAsync 返回 Task,呼叫端程式碼線性易讀。
  • 取消權杖控制逾時:CancellationTokenSource 取代迴圈計數,語意清晰。
  • 執行緒安全:共用變數以 lock 保護;DataReceived 於背景執行緒觸發,UI 更新需 Invoke(範例中 txtLog.AppendText 需自行 Marshal)。

進階建議

  • 實作完整的緩衝區管理處理黏包/分包(環狀緩衝區 + 狀態機解析)。
  • 採用 System.IO.Pipelines (Core 2.1+) 簡化高效能緩衝處理。
  • 若通訊協議複雜,考慮使用成熟庫如 Modbus.Net、NModbus。

採用非同步事件驅動模式,能徹底解決串口通訊中的阻塞、死鎖與 UI 凍結問題,是專業上位機開發的標準做法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved