總目錄
前言
在工業控制、物聯網、嵌入式開發等領域,串口通信(Serial Port Communication)是連接串行設備(如條碼掃描器、GPS接收器等)與計算機的重要手段。C# 提供了內置的 SerialPort
類,簡化了串口開發的流程。本文將詳細介紹如何在C#中使用SerialPort
類進行串口通信。
一、什么是 SerialPort?
1. 定義
System.IO.Ports.SerialPort
類(簡稱 SerialPort
)是 .NET 框架中用于串口通信的核心類,它提供了對串行端口的訪問,允許我們進行數據的讀寫、配置串口參數以及處理事件等操作。
2. 串行通信簡介
串口通信(Serial Communication)是通過單條數據線按順序傳輸數據的通信方式,具有接線簡單、成本低廉的特點。在工業控制、物聯網設備、傳感器等領域廣泛應用,波特率范圍常見為 9600 9600 9600 到 115200 115200 115200。
- 在工業自動化、嵌入式系統開發等場景中,串口通信(Serial Communication)仍是硬件交互的核心技術之一。
- 串行通信是一種數據按位順序傳輸的通信方式,常用于連接各種串行設備。
- C# 通過 System.IO.Ports.SerialPort 類為開發者提供了簡潔高效的串口操作接口,支持參數配置、數據讀寫及事件處理等功能。
3. SerialPort
核心功能
- 端口配置參數:波特率、數據位、校驗位、停止位等。
- 數據讀寫操作:同步或異步發送/接收數據。
- 事件驅動通信:通過
DataReceived
事件實時響應數據到達。
4. 典型應用場景
- 工業設備通信
- 傳感器數據采集
- 嵌入式系統交互
二、常用的屬性和方法
1. 常用屬性
屬性名 | 類型/說明 |
---|---|
PortName | string 串口名稱(如 COM1 、COM3 )。 |
BaudRate | int 波特率,表示數據傳輸速率。常用的有 9600 、19200 、38400 、57600 以及115200 等 |
Parity | Parity 奇偶校驗類型如 None 無奇偶校驗、Even 偶檢驗、Odd 奇檢驗。 |
DataBits | int 數據位數。常見數據位為8位,如需要特別設置,還可設置位5位,6位,7位 |
StopBits | StopBits 停止位數如 None 不使用停止位One 1個停止位、Two 2個停止位、OnePointFive 1.5個停止位)。 |
Handshake | Handshake 握手協議(如 None 、RequestToSend 、XOnXOff )。 |
Encoding | Encoding 數據編碼方式(如 Encoding.UTF8 )。 |
ReadTimeout | int 讀取操作超時時間(毫秒),默認為 InfiniteTimeout 。 |
WriteTimeout | int 寫入操作超時時間(毫秒),默認為 InfiniteTimeout 。 |
IsOpen | bool 表示串口是否已打開。 |
NewLine | string 定義行結束符(如 "\r\n" ),用于 ReadLine() 和 WriteLine() 。 |
DtrEnable | bool 控制數據終端就緒(DTR)信號狀態。 |
RtsEnable | bool 控制請求發送(RTS)信號狀態。 |
ReceivedBytesThreshold | int 觸發 DataReceived 事件的最小字節數。 |
2. 常用方法
方法名 | 說明 |
---|---|
Open() | 打開串口。 |
Close() | 關閉已打開的串口。 |
Read(byte[] buffer, int offset, int count) | 從串口讀取指定字節數到緩沖區。 |
ReadByte() | 讀取單個字節(返回 int ,范圍 0-255 ,失敗返回 -1 )。 |
ReadLine() | 讀取一行數據,直到遇到 NewLine 定義的結束符。 |
ReadExisting() | 讀取接收緩沖區中所有可用數據(不阻塞)。 |
Write(string text) | 寫入字符串到串口(自動按 Encoding 編碼)。 |
Write(byte[] buffer, int offset, int count) | 寫入字節數組到串口。 |
DiscardInBuffer() | 清空輸入緩沖區中的數據。 |
DiscardOutBuffer() | 清空輸出緩沖區中的數據。 |
3. 常用事件
事件名 | 說明 |
---|---|
DataReceived | 當接收到數據且字節數達到 ReceivedBytesThreshold 時觸發。 |
ErrorReceived | 當串口發生錯誤(如奇偶校驗錯誤)時觸發。 |
4. 其他重要屬性(擴展)
屬性名 | 類型/說明 |
---|---|
BytesToRead | int 接收緩沖區中已接收的字節數。 |
BytesToWrite | int 輸出緩沖區中待發送的字節數。 |
ReadBufferSize | int 設置輸入緩沖區大小(默認 4096 )。 |
WriteBufferSize | int 設置輸出緩沖區大小(默認 2048 )。 |
三、SerialPort 基礎
1. 添加命名空間引用
在使用SerialPort
類之前,需要先添加對System.IO.Ports
命名空間的引用:
using System.IO.Ports;
2. 初始化與參數配置
串口通信的核心是正確配置參數,確保與硬件設備匹配,否則通信失敗。
1)方式1:構造函數
using System.IO.Ports;// 創建對象并指定端口名稱(如COM3)
SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
2)方式2:屬性配置
// 可選:手動設置參數(適用于動態配置)
SerialPort serialPort = new SerialPort();
serialPort.PortName = "COM3"; // 端口號
serialPort.BaudRate = 9600; // 波特率
serialPort.Parity = Parity.None; // 校驗位
serialPort.DataBits = 8; // 數據位
serialPort.StopBits = StopBits.One; // 停止位
3. 打開與關閉串口
1)打開串口
在進行數據傳輸之前,需要打開串行端口:
serialPort.Open(); // 打開串口
優化一下:加上try catch
做異常處理
try
{if (!serialPort.IsOpen){serialPort.Open();Console.WriteLine("端口已打開");}
}
catch (Exception ex)
{Console.WriteLine($"打開失敗:{ex.Message}");
}
2)關閉串口
數據傳輸完成后,應及時關閉串行端口。務必在程序退出前調用Close
serialPort.Close(); // 關閉端口
優化一下:加上try catch
做異常處理
try
{if (serialPort.IsOpen){// 關閉端口serialPort.Close();Console.WriteLine("端口已關閉");// 銷毀serialPort對象serialPort.Dispose();}
}
catch (Exception ex)
{Console.WriteLine($"關閉失敗:{ex.Message}");
}
4. 讀取和寫入數據
1)寫入數據
? 寫入字符串數據
- 使用
Write()
方法:將指定的字符串寫入串行端口。 - 使用
WriteLine
方法:將指定的字符串和SerialPort.NewLine
值寫入串行端口。
serialPort.Write("Hello, Serial!");// 自動添加換行符:發送字符串并添加換行符(WriteLine)
serialPort.WriteLine("Hello, Serial Port!");
? 寫入字節流(字節數組)數據
// 發送字節數組
byte[] data = Encoding.UTF8.GetBytes("Test Data");
serialPort.Write(data, 0, data.Length);byte[] data = { 0x01, 0xFF, 0xAB }; // 十六進制數據
serialPort.Write(data, 0, data.Length); // 發送字節
2)讀取數據
可以通過Read()
、ReadLine()
等方法從串口讀取數據。
// 讀取指定字節數
byte[] buffer = new byte[1024];
int bytesRead = serialPort.Read(buffer, 0, buffer.Length);
string received = Encoding.UTF8.GetString(buffer, 0, bytesRead);// 讀取一行數據(依賴 NewLine 配置)
string line = serialPort.ReadLine();
5. 處理數據接收事件
通過 DataReceived
事件實時接收串口數據。當有數據到達時,DataReceived
事件會被觸發。可以通過注冊該事件來實時處理接收到的數據:
1)使用方式1
// 注冊 DataReceived 事件
serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{SerialPort sp = (SerialPort)sender;string received = sp.ReadExisting(); // 讀取所有可用數據Console.WriteLine($"Received: {received}");
}
2)使用方式2
serialPort.DataReceived += SerialPort_DataReceived;
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{//首先實例化一個字節數組// 字節數組的大小:通過BytesToRead 屬性,獲取得到緩沖區中數據的字節數byte[] buffer = new byte[serialPort.BytesToRead];//再通過 Read() 方法 讀取緩存區內全部數據,并將數據寫入字節數組中serialPort.Read(buffer, 0, buffer.Length);// 解碼string received = Encoding.ASCII.GetString(buffer); Console.WriteLine(received);
}
3)使用方式3
serialPort.DataReceived += (sender, e) =>
{if (e.EventType == SerialData.Chars){byte[] buffer = new byte[serialPort.BytesToRead];serialPort.Read(buffer, 0, buffer.Length);string data = Encoding.ASCII.GetString(buffer);Console.WriteLine($"收到數據:{data}");}
};
以上三種方式,基本原理一致,只是部分的讀取數據方式和事件定義的方式稍有不同而已。
6. 完整示例
示例1:簡單示例
using System;
using System.IO.Ports;
using System.Windows.Forms;public class SerialPortDemo
{//配置并創建SerialPort實例private SerialPort sp = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);public void Start(){try{// 注冊數據接收事件sp.DataReceived += DataReceivedHandler;// 打開串口sp.Open();//寫入數據sp.WriteLine("Start Communication");}catch (Exception ex){MessageBox.Show($"初始化失敗:{ex.Message}");}}// 接收數據private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e){// 讀取數據string data = sp.ReadExisting();Console.WriteLine($"實時數據:{data}");}// 關閉端口public void Stop() => sp.Close();
}
示例2:WinForm 示例
以下是一個完整的示例,展示如何在C#中使用SerialPort
類進行串口通信。
using System;
using System.IO.Ports;
using System.Windows.Forms;namespace SerialPortExample
{public partial class MainForm : Form{private SerialPort serialPort;public MainForm(){InitializeComponent();InitializeSerialPort();}private void InitializeSerialPort(){serialPort = new SerialPort();serialPort.PortName = "COM1";serialPort.BaudRate = 9600;serialPort.Parity = Parity.None;serialPort.DataBits = 8;serialPort.StopBits = StopBits.One;serialPort.Handshake = Handshake.None;serialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPort_DataReceived);}private void btnOpen_Click(object sender, EventArgs e){try{if (!serialPort.IsOpen){serialPort.Open();MessageBox.Show("串口已打開");}}catch (Exception ex){MessageBox.Show("打開串口時出錯: " + ex.Message);}}private void btnSend_Click(object sender, EventArgs e){if (serialPort.IsOpen){string message = txtSend.Text;serialPort.WriteLine(message);txtReceive.AppendText("發送: " + message + Environment.NewLine);}else{MessageBox.Show("請先打開串口");}}private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e){SerialPort sp = (SerialPort)sender;string indata = sp.ReadExisting();this.Invoke(new Action(() =>{txtReceive.AppendText("接收: " + indata);}));}private void btnClose_Click(object sender, EventArgs e){if (serialPort.IsOpen){serialPort.Close();MessageBox.Show("串口已關閉");}}}
}
示例3:掃碼槍數據接收
SerialPort port = new("COM3", 115200, Parity.None, 8, StopBits.One);
port.DataReceived += (s, e) =>
{byte[] barcode = new byte[port.BytesToRead];port.Read(barcode, 0, barcode.Length);// 解析掃碼槍數據(通常以回車結尾)if (barcode.Last() == 0x0D) {string code = Encoding.ASCII.GetString(barcode);ProcessBarcode(code);}
};
四、SerialPort 進階
1. 高級參數配置
1)配置超時時間
防止長時間阻塞(單位:毫秒)。
// 設置讀取超時為 1000ms
serialPort.ReadTimeout = 1000;
// 設置寫入超時
serialPort.WriteTimeout = 500;
2)緩沖區設置
ReceivedBytesThreshold
:指定觸發 DataReceived
事件的字節閾值,即觸發 DataReceived
事件的最小字節數。
serialPort.ReceivedBytesThreshold = 2; //默認值 為 1
3)其余參數配置
sp.Encoding = Encoding.ASCII; // 編碼格式
sp.NewLine = "\r\n"; // 換行符定義
2. 獲取所有可用 COM 端口
可以通過SerialPort.GetPortNames()
方法獲取系統中所有可用的串行端口名稱:
// 獲取所有可用 COM 端口
string[] ports = SerialPort.GetPortNames();
foreach (var port in ports)
{Console.WriteLine(port);
}
3. ErrorReceived事件
當串行通信中發生錯誤(如幀錯誤/緩沖區溢出)時,ErrorReceived
事件會被觸發:
serialPort.ErrorReceived += new SerialErrorReceivedEventHandler(serialPort_ErrorReceived);
private static void serialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{if ((e.EventType & SerialError.Frame) == SerialError.Frame){Console.WriteLine("Frame error detected.");}if ((e.EventType & SerialError.Overrun) == SerialError.Overrun){Console.WriteLine("Overrun error detected.");}if ((e.EventType & SerialError.RXParity) == SerialError.RXParity){Console.WriteLine("Parity error detected.");}
}
sp.ErrorReceived += (sender, e) =>
{Console.WriteLine($"錯誤類型:{e.EventType}");
};
4. 異常處理
使用try-catch
塊捕獲和處理可能的異常,確保程序的穩定性:
try
{serialPort.Open();serialPort.WriteLine("Hello, Serial Port!");serialPort.Close();
}
catch (Exception ex)
{Console.WriteLine("Error: " + ex.Message);
}
// 常見異常類型
try
{// 串口操作代碼
}
catch (UnauthorizedAccessException ex)
{Console.WriteLine("端口訪問被拒絕");
}
catch (TimeoutException ex)
{Console.WriteLine("操作超時");
}
catch (IOException ex)
{Console.WriteLine("I/O錯誤");
}
5. 跨線程處理
為了能夠實時響應串口接收到的數據,可以使用DataReceived
事件。需要注意的是,該事件在單獨的線程中觸發,若要更新UI控件,涉及到跨線程,需要使用Invoke
方法。
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{SerialPort sp = (SerialPort)sender;string received = sp.ReadExisting();// 數據接收后,通過委托更新UI(假設在 WinForms 中更新TextBox)this.Invoke(new Action(() =>{textBox.Text += received ;}));
}
6. 多線程處理
推薦使用生產者-消費者模式:
BlockingCollection<string> dataQueue = new BlockingCollection<string>();// 生產者線程
void DataReceivedHandler(...)
{dataQueue.Add(receivedData);
}// 消費者線程
Task.Run(() =>
{foreach (var data in dataQueue.GetConsumingEnumerable()){// 處理數據}
});
五、常見問題與注意事項
1. 注意事項
- 參數一致性:確保雙方設備的波特率、數據位、停止位、校驗位一致。
- 權限問題:在訪問串口時,需要確保程序有相應的權限,否則可能會拋出異常。
- 線程安全:在
DataReceived
事件中處理數據時,由于它是在單獨的線程中觸發的,若要更新UI控件,必須使用Invoke
方法來確保線程安全 - 異常處理:使用
try-catch
處理Open()
、Read()
、Write()
可能拋出的異常。 - 緩沖區管理:通過
DiscardInBuffer
和DiscardOutBuffer
清空緩沖區,避免數據殘留。- 避免重復打開同一端口,關閉前調用
sp.DiscardInBuffer()
清空緩存。
- 避免重復打開同一端口,關閉前調用
- 硬件流控制
在高速傳輸場景中啟用Handshake.RequestToSend
,避免數據丟失。
2. 常見問題
- 端口無法打開
- 檢查設備是否占用(如其他程序已連接)
- 確認串口號是否正確(虛擬串口可能動態變化)
- 異常處理:
try...catch(IOException)
捕獲端口不存在錯誤
- 數據接收不完整
- 使用
BytesToRead
確保讀取全部緩存數據 - 添加延遲(如
Thread.Sleep(50)
)等待完整幀 - 硬件延遲:部分設備發送數據有間隔,需調整時序
- 使用
- 數據亂碼
- 發送/接收端波特率必須一致,否則數據亂碼。
- 字符編碼問題
- 若接收中文字符亂碼,需設置:
serialPort.Encoding = Encoding.GetEncoding("GB2312")
。- 或
serialPort.Encoding = Encoding.UTF8;
六、擴展
1. 跨平臺方案(RJCP.SerialPortStream)
對于需要跨平臺(Linux/macOS)的場景,推薦使用 RJCP.SerialPortStream 庫(通過 NuGet 安裝 RJCP.SerialPortStream
):
using RJCP.IO.Ports;SerialPortStream serialPort = new SerialPortStream("COM3");
serialPort.BaudRate = 9600;
serialPort.Open();// 事件處理與讀寫方式與 SerialPort 類似
serialPort.DataReceived += (sender, e) =>
{byte[] data = new byte[serialPort.BytesToRead];serialPort.Read(data, 0, data.Length);Console.WriteLine(Encoding.UTF8.GetString(data));
};
2. 編碼
使用串口通信的時候,會涉及到數據的編碼格式,如果我們使用ASCII編碼格式,但是接收的時候采用的是其他的編碼格式,可能就會導致數據解析錯誤,因此在一些特定場景下我們需要規定好發送和接收數據的編碼格式,這個時候就需要用到System.Text.Encoding
類中的編碼解碼的功能。
詳見:C# System.Text.Encoding 使用詳解
3. 清空緩沖區的方法
關于 DiscardInBuffer / DiscardOutBuffer
的使用,詳見:C# SerialPort 類中清空緩存區的方法。
4. Handshake 設置
常用握手協議:
Handshake.None
:無握手。Handshake.RequestToSend
:RTS/CTS(請求發送/清除發送)。Handshake.XOnXOff
:軟件流控制(XON/XOFF 字符)。
serialPort.Handshake = Handshake.RequestToSend;
詳見:C# SerialPort 類中 Handshake 屬性的作用
結語
回到目錄頁:C# 上位機知識匯總
希望以上內容可以幫助到大家,如文中有不對之處,還請批評指正。
參考資料:
- 官方文檔:SerialPort Class