索引
- 一、SerialPort串口通信
- 二、使用SerialPort
- 1.創建SerialPort對象,進行基本配置
- 2.寫入串口數據
- ①.寫入串口數據的方法
- ②.封裝數據
- 3.讀取串口數據
- ①.讀取串口數據的方法
- ②.解析數據
- 4.讀取串口數據的時機
- ①.DataReceived事件
- ②.多線程接收數據
- 5.粘包問題處理
一、SerialPort串口通信
C#中的SerialPort
類是.NET
框架提供的一個用于串口通信的強大工具,主要用于實現計算機與外部設備(如傳感器、嵌入式設備、PLC等)之間的數據交換。
二、使用SerialPort
1.創建SerialPort對象,進行基本配置
使用SerialPort
時,直接創建一個SerialPort
對象即可,其基本配置項包括:
PortName:串口號,如COM1。
BaudRate:波特率,如9600或115200。
DataBits:數據位,通常為7或8。
StopBits:停止位,通常為1、1.5或2。
Parity:奇偶校驗位,如None(無校驗)、Odd(奇校驗)或Even(偶校驗)。
代碼如下:
/// <summary>/// 串口號/// </summary>[Label("串口號")] public string portName;/// <summary>/// 波特率/// </summary>[Label("波特率")] public int baudRate;/// <summary>/// 數據位/// </summary>[Label("數據位")] public int dataBits;/// <summary>/// 停止位/// </summary>[Label("停止位")] public StopBits stopBits;/// <summary>/// 奇偶校驗位/// </summary>[Label("奇偶校驗位")] public Parity parity;private SerialPort _serialPort;protected override void Awake(){base.Awake();_serialPort = new SerialPort();_serialPort.PortName = portName;_serialPort.BaudRate = baudRate;_serialPort.DataBits = dataBits;_serialPort.StopBits = stopBits;_serialPort.Parity = parity;try{_serialPort.Open();}catch (Exception e){Log.Error(e.Message);}}
2.寫入串口數據
①.寫入串口數據的方法
寫入串口數據的方法非常簡單,如下:
//將數據封裝為字節數組(可以直接強轉字符串,也可以按16進制處理等)byte[] bytes = EncapsulatePackage(data);_serialPort.Write(bytes, 0, bytes.Length);
②.封裝數據
如何封裝數據
取決于數據交換的協議,比如直接強轉字符串:
/// <summary>/// 單個數據包結束符(換行符)/// </summary>private const byte EndSign = 10;/// <summary>/// 封裝數據包(這里要求所有字符必須為ASCII碼,也即是一個字符只占一個字節)/// </summary>/// <param name="data">原始數據</param>/// <returns>數據包</returns>private byte[] EncapsulatePackage(string data){byte[] bytes = new byte[data.Length + 1];for (int i = 0; i < data.Length; i++){bytes[i] = (byte)data[i];}//在每個數據包后面補充【結束符】,表明此數據包結束bytes[bytes.Length - 1] = EndSign;return bytes;}
這里以簡單協議進行講解(每個數據包以【換行符】代表結束符
)。
3.讀取串口數據
①.讀取串口數據的方法
讀取串口數據的方法非常簡單,如下:
//建立數據緩沖區(大小為串口中可讀取數據大小:_serialPort.BytesToRead)byte[] bytes = new byte[_serialPort.BytesToRead];_serialPort.Read(bytes, 0, bytes.Length);//解析數據包string data = AnalyzePackage(bytes);
②.解析數據
如何解析數據
取決于數據交換的協議,比如直接強轉字符串:
/// <summary>/// 解析數據包/// </summary>/// <param name="bytes">數據包</param>/// <returns>數據</returns>private string AnalyzePackage(byte[] bytes){return Encoding.Default.GetString(bytes);}
4.讀取串口數據的時機
那么對如上的簡單的寫入、讀取串口數據
有了基本的了解之后,接下來便是相對不那么簡單
的部分了。
首先,寫入串口數據的時機
由我們說了算,何時調用便何時寫入,這點毋容置疑。
但是,讀取串口數據的時機
該是何時?參考下面兩種方式。
①.DataReceived事件
SerialPort的DataReceived
事件當對象對應的串口接收到了數據時便會觸發,用他來讀取數據簡直不要太絲滑:
protected override void Awake(){base.Awake();//......_serialPort.DataReceived += OnDataReceived;//......}private void OnDataReceived(object sender, SerialDataReceivedEventArgs e){if (_serialPort.IsOpen && _serialPort.BytesToRead > 0){//建立數據緩沖區(大小為串口中可讀取數據大小:_serialPort.BytesToRead)byte[] bytes = new byte[_serialPort.BytesToRead];_serialPort.Read(bytes, 0, bytes.Length);//解析數據包string data = AnalyzePackage(bytes);}}
但是,這里必須得有一個但是,只要在Unity中用過SerialPort
的都會知道DataReceived
這玩意他不起作用,主要原因如下(別懷疑,就是問的AI):
1.Unity引擎對System.IO.Ports命名空間的支持有限,尤其是對DataReceived事件的支持存在缺陷。在Unity中,DataReceived事件通常不會像在常規C#項目中那樣正常觸發,這主要是因為Unity的運行環境與普通.NET應用有所不同,特別是在事件處理機制上存在差異。
2.DataReceived事件需要在一個獨立的線程中監聽串口數據,但在Unity中,主線程(用于游戲邏輯和渲染)和串口數據接收線程之間的同步機制存在問題。因此,DataReceived事件可能無法被正確觸發。
3.Unity的Update和FixedUpdate等事件函數運行在主線程中,而串口數據接收通常需要多線程支持。當串口操作在主線程中執行時,可能會因為線程沖突導致DataReceived事件無法觸發。
那么,我們不得不考慮更換其他方案了。
②.多線程接收數據
老樣子,像處理Socket
通信那樣,多線程永遠是最強的利器:
private Thread _receiveThread;protected override void Awake(){base.Awake();//新建一個線程,啟動數據接收方法ReceivedData_receiveThread = new Thread(new ThreadStart(ReceivedData));_receiveThread.Start();}protected override void OnDestroy(){base.OnDestroy();_receiveThread.Abort();_receiveThread = null;}/// <summary>/// 從串口接收數據/// </summary>private void ReceivedData(){while (true){if (_serialPort.IsOpen && _serialPort.BytesToRead > 0){try{//建立數據緩沖區(大小為串口中可讀取數據大小:_serialPort.BytesToRead)byte[] bytes = new byte[_serialPort.BytesToRead];_serialPort.Read(bytes, 0, bytes.Length);//解析數據包string data = AnalyzePackage(bytes);//......}catch (Exception e){Log.Error(e.Message);}}}}
5.粘包問題處理
當然,到此時我們并不能高枕無憂,還有另一個在數據交換領域普遍存在的問題需要我們解決,那就是數據的粘包
問題。
粘包問題是指多個數據包在接收端被錯誤地合并成一個數據包,導致接收方無法正確區分每個數據包的邊界。這通常發生在連續發送多個數據包時,接收端來不及解析或緩沖區管理不當的情況下。
就像Socket
通信一樣,發送方發出的數據是A03568
,但接收方可能會分多次接收到,比如2次才接收完,那就可能是A03
、568
,接收方只是一個機器不是人,自然不知道這2個包該連起來合成一個包,那么后續的處理自然就亂套了。
這就是通信協議
存在的必要了,以我們的簡單通信協議為例(每個數據包以【換行符】代表結束符
),在讀取數據的方法中進行防粘包處理:
private byte[] _buffer = new byte[16];private List<byte> _receiveBuffer = new List<byte>();/// <summary>/// 從串口接收數據/// </summary>private void ReceivedData(){while (true){if (_serialPort.IsOpen && _serialPort.BytesToRead > 0){try{//讀取一次數據(最大不超過緩沖區大小)int count = _serialPort.Read(_buffer, 0, Mathf.Min(_buffer.Length, _serialPort.BytesToRead));for (int i = 0; i < count; i++){//如果為結束符,則代表一個數據包接收完成,解析該包if (_buffer[i] == EndSign){string data = AnalyzePackage(_receiveBuffer.ToArray());//清空緩沖區_receiveBuffer.Clear();//處理數據HandlerData(data);Log.Info($"接收串口數據:{data}");}//否則加入數據緩沖區else{_receiveBuffer.Add(_buffer[i]);}}}catch (Exception e){Log.Error(e.Message);}}}}
代碼很簡單,相信處理過字節流的人應該都能看懂,事實上在串口通信中如上的簡單通信協議已能勝任大多數情況。