.Net?Core應(yīng)用增強(qiáng)型跨平臺(tái)串口類庫(kù)CustomSerialPort()詳解
摘要
在使用SerialPort進(jìn)行串口協(xié)議解析過(guò)程中,經(jīng)常遇到接收單幀協(xié)議數(shù)據(jù)串口接收事件多次觸發(fā),協(xié)議解析麻煩的問(wèn)題。針對(duì)此情況,基于開(kāi)源跨平臺(tái)串口類庫(kù)SerialPortStrem進(jìn)行了進(jìn)一步封裝,實(shí)現(xiàn)了一種接收超時(shí)響應(yīng)事件機(jī)制,簡(jiǎn)化串口通訊的使用。
引言
最近,寫(xiě)了一篇博文《.net core跨平臺(tái)應(yīng)用研究-串口篇》得到了一些園友的好評(píng),文中介紹了在跨平臺(tái)應(yīng)用研究過(guò)程中,在dotnet core下使用SerialPort類庫(kù)在linux下不能支持的踩坑經(jīng)歷及解決辦法。
因網(wǎng)上關(guān)于SerialPort類庫(kù)使用的相關(guān)文章較多,在該文中,對(duì)串口類庫(kù)的使用,一筆帶過(guò)。但在實(shí)際使用,使用過(guò)SerialPort類庫(kù)的同學(xué),可能遇到過(guò)在數(shù)據(jù)接收時(shí),由于數(shù)據(jù)接收事件的觸發(fā)具有不確定性,很多時(shí)候,一幀通訊協(xié)議數(shù)據(jù),會(huì)多次觸發(fā),造成程序處理協(xié)議數(shù)據(jù)較為麻煩的問(wèn)題。
為簡(jiǎn)化串口通訊類庫(kù)的使用,筆者結(jié)合自己的相關(guān)經(jīng)驗(yàn),封裝了一個(gè)自定義增強(qiáng)型跨平臺(tái)串口類庫(kù),以解決一幀協(xié)議數(shù)據(jù),多次觸發(fā)的問(wèn)題。
基礎(chǔ)類庫(kù)的選擇
由于考慮的是跨平臺(tái)應(yīng)用,SerialPort類庫(kù)并不支持linux系統(tǒng)(在前一篇文章中已介紹過(guò)踩坑經(jīng)歷),筆者選用了SerialPortStream類庫(kù)進(jìn)行封裝。
該類庫(kù)支持windows系統(tǒng)和Linux系統(tǒng),但在Linux系統(tǒng)下運(yùn)行,需要額外編譯目標(biāo)平臺(tái)支持庫(kù)并進(jìn)行相關(guān)環(huán)境配置。
相關(guān)編譯配置說(shuō)明在https://github.com/jcurl/SerialPortStream已有介紹,也可參考本人的拙作《.net core跨平臺(tái)應(yīng)用研究-串口篇》
類庫(kù)的實(shí)現(xiàn)
創(chuàng)建跨平臺(tái)類庫(kù)
為了支持跨平臺(tái),我們使用Visual Studio 2017創(chuàng)建一個(gè)基于.NET Standard的類庫(kù)。
NET Standard是一項(xiàng)API規(guī)范,每一個(gè)特定的版本,都定義了必須實(shí)現(xiàn)的基類庫(kù)。
.NET Core是一個(gè)托管框架,針對(duì)構(gòu)建控制臺(tái)、云、ASP.NET Core和UWP應(yīng)用程序進(jìn)行了優(yōu)化。
每一種托管實(shí)現(xiàn)(如.NET Core、.NET Framework或Xamarin)都必須遵循.NET Standard實(shí)現(xiàn)基類庫(kù)(BCL)。
關(guān)于NET Standard和跨平臺(tái)的詳細(xì)說(shuō)明在此:
//www.dbjr.com.cn/article/234699.htm
筆者也不再啰嗦呵。
實(shí)現(xiàn)機(jī)制/條件
通常串口通訊中,發(fā)送數(shù)據(jù)后,會(huì)有一段時(shí)間用于等待接收方應(yīng)答,如此一來(lái),兩次數(shù)據(jù)發(fā)送之間,必然會(huì)有一定的時(shí)間間隔。如ModbusRTU協(xié)議就規(guī)定,兩次數(shù)據(jù)報(bào)文發(fā)送之間,需要等待超過(guò)發(fā)送4個(gè)字節(jié)以上的間隔時(shí)間。
筆者在單片機(jī)以及實(shí)時(shí)性較高的嵌入式系統(tǒng)中,為處理串口接收與協(xié)議的無(wú)關(guān)性,通常采用數(shù)據(jù)幀接收超時(shí)來(lái)處理數(shù)據(jù)幀的接收。根據(jù)串口通訊的速率計(jì)算出兩次通訊之間所需要超時(shí)間隔,取兩倍超時(shí)間隔時(shí)間作為超時(shí)參數(shù),每接收到一個(gè)字節(jié),將數(shù)據(jù)放入緩沖區(qū)并進(jìn)行計(jì)時(shí),當(dāng)最后一個(gè)字節(jié)的接收時(shí)間超過(guò)超時(shí)時(shí)間,返回接收數(shù)據(jù)并清空緩存,一次完整接收完成(DMA接收方式不在此討論)。
.net core跨平臺(tái)實(shí)現(xiàn)
在自定義的串口類中,訂閱基礎(chǔ)串口類數(shù)據(jù)接收事件,在接收事件每次觸發(fā)后,讀出當(dāng)前可用的緩沖數(shù)據(jù)到自定義緩沖區(qū),同時(shí),標(biāo)記最后接收時(shí)間Tick為當(dāng)前系統(tǒng)Tick。判斷是否開(kāi)啟了接收超時(shí)處理線程,如未開(kāi)啟,則開(kāi)啟一個(gè)接收超時(shí)處理線程。
接收超時(shí)處理線程中,以一個(gè)較小的時(shí)間間隔進(jìn)行判斷,如果最后接收時(shí)間與當(dāng)前時(shí)間之間的間隔小于設(shè)置值(默認(rèn)128ms),休眠一段時(shí)間(默認(rèn)16ms)后循環(huán)檢查。如間隔時(shí)間大于設(shè)定值,觸發(fā)外部接收訂閱事件,傳出接收到的數(shù)據(jù),退出超時(shí)處理線程。
此處應(yīng)有流程圖。呵呵,懶得畫(huà)了,大家自行腦補(bǔ)吧。 ^_^
在windows系統(tǒng)或linux系統(tǒng)中,因系統(tǒng)的多任務(wù)處理的特性,系統(tǒng)實(shí)時(shí)性較差,通常50ms以下時(shí)間間隔的定時(shí)任務(wù),較大程度會(huì)出現(xiàn)不可靠的情況(任務(wù)執(zhí)行時(shí)間都有可能超過(guò)調(diào)用間隔時(shí)間)。
因此,默認(rèn)超時(shí)時(shí)間間隔設(shè)置為128ms。也可根據(jù)實(shí)際使用情況調(diào)整,但最小間隔不宜低于64ms。
注:此處為個(gè)人經(jīng)驗(yàn)和理解,如不認(rèn)同,請(qǐng)直接忽視。
主要代碼
串口接收事件代碼:
protected void Sp_DataReceived(object sender, SerialDataReceivedEventArgs e) { int canReadBytesLen = 0; if (ReceiveTimeoutEnable) { while (sp.BytesToRead > 0) { canReadBytesLen = sp.BytesToRead; if (receiveDatalen + canReadBytesLen > BufSize) { receiveDatalen = 0; throw new Exception("Serial port receives buffer overflow!"); } var receiveLen = sp.Read(recviceBuffer, receiveDatalen, canReadBytesLen); if (receiveLen != canReadBytesLen) { receiveDatalen = 0; throw new Exception("Serial port receives exception!"); } //Array.Copy(recviceBuffer, 0, receivedBytes, receiveDatalen, receiveLen); receiveDatalen += receiveLen; lastReceiveTick = Environment.TickCount; if (!TimeoutCheckThreadIsWork) { TimeoutCheckThreadIsWork = true; Thread thread = new Thread(ReceiveTimeoutCheckFunc) { Name = "ComReceiveTimeoutCheckThread" }; thread.Start(); } } } else { if (ReceivedEvent != null) { // 獲取字節(jié)長(zhǎng)度 int bytesNum = sp.BytesToRead; if (bytesNum == 0) return; // 創(chuàng)建字節(jié)數(shù)組 byte[] resultBuffer = new byte[bytesNum]; int i = 0; while (i < bytesNum) { // 讀取數(shù)據(jù)到緩沖區(qū) int j = sp.Read(recviceBuffer, i, bytesNum - i); i += j; } Array.Copy(recviceBuffer, 0, resultBuffer, 0, i); ReceivedEvent(this, resultBuffer); //System.Diagnostics.Debug.WriteLine("len " + i.ToString() + " " + ByteToHexStr(resultBuffer)); } //Array.Clear (receivedBytes,0,receivedBytes.Length ); receiveDatalen = 0; } }
接收超時(shí)處理線程代碼:
/// <summary> /// 超時(shí)返回?cái)?shù)據(jù)處理線程方法 /// </summary> protected void ReceiveTimeoutCheckFunc() { while (TimeoutCheckThreadIsWork) { if (Environment.TickCount - lastReceiveTick > ReceiveTimeout) { if (ReceivedEvent != null) { byte[] returnBytes = new byte[receiveDatalen]; Array.Copy(recviceBuffer, 0, returnBytes, 0, receiveDatalen); ReceivedEvent(this, returnBytes); } //Array.Clear (receivedBytes,0,receivedBytes.Length ); receiveDatalen = 0; TimeoutCheckThreadIsWork = false; } else Thread.Sleep(16); } }
創(chuàng)建.net core控制臺(tái)程序
為驗(yàn)證我們的類庫(kù)是否能夠正常工作,我們創(chuàng)建一個(gè)使用類庫(kù)的.net core控制臺(tái)程序。
為啥選擇dotnet core,原因很簡(jiǎn)單,跨平臺(tái)。本程序分別需在windows和linux系統(tǒng)下進(jìn)行運(yùn)行測(cè)試。
- 顯示系統(tǒng)信息(系統(tǒng)標(biāo)識(shí)、程序標(biāo)識(shí)等)
- 列舉系統(tǒng)可用串口資源
- 選擇串口
- 打開(kāi)串口/關(guān)閉串口
- 串口測(cè)試(打開(kāi)/發(fā)送/關(guān)閉)
static void Main(string[] args) { SetLibPath(); ShowWelcome(); GetPortNames(); ShowPortNames(); if (serailports.Length == 0) { Console.WriteLine($"Press any key to exit"); Console.ReadKey(); return; } #if RunIsService RunService(); #endif bool quit = false; while (!quit) { Console.WriteLine("\r\nPlease Input command Key\r\n"); Console.WriteLine("p:Show SerialPort List"); Console.WriteLine($"t:Test Uart:\"{selectedComPort}\""); Console.WriteLine($"o:Open Uart:\"{selectedComPort}\""); Console.WriteLine($"c:Close Uart:\"{selectedComPort}\""); Console.WriteLine("n:select next serial port"); Console.WriteLine("q:exit app"); Console.WriteLine(); var key = Console.ReadKey().KeyChar; Console.WriteLine(); switch (key) { case (Char)27: case 'q': case 'Q': quit = true; break; case 's': ShowWelcome(); break; case 'p': ShowPortNames(); break; case 'n': SelectSerialPort(); break; case 't': TestUart(selectedComPort); break; case 'w': TestWinUart(selectedComPort); break; case 'o': OpenUart(selectedComPort); break; case 'c': CloseUart(); break; } } }
筆者使用類庫(kù)是直接引用類庫(kù)項(xiàng)目,大家需要使用的話,可在解決方案資源管理器中,項(xiàng)目的依賴項(xiàng)上點(diǎn)擊右鍵
在NuGet包管理器中,搜索SerialPort或flyfire即可找到并安裝本類庫(kù)。
類庫(kù)地址
類庫(kù)地址:https://www.nuget.org/packages/flyfire.CustomSerialPort
跨平臺(tái)測(cè)試
Windows測(cè)試輸出界面
ubuntu測(cè)試輸出界面
源碼地址
類庫(kù)源碼與例程地址:https://github.com/flyfire-cn/flyfire.CustomSerialPort
有需要的同學(xué),請(qǐng)自行獲取。
到此這篇關(guān)于.Net Core應(yīng)用增強(qiáng)型跨平臺(tái)串口類庫(kù)CustomSerialPort()的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
asp.net中一個(gè)linq分頁(yè)實(shí)現(xiàn)代碼
asp.net中一個(gè)linq分頁(yè)實(shí)現(xiàn)代碼,需要的朋友可以參考下。2011-12-12排除JQuery通過(guò)HttpGet調(diào)用WebService返回Json時(shí)“parserror”錯(cuò)誤
排除JQuery通過(guò)HttpGet調(diào)用WebService返回Json時(shí)“parserror”錯(cuò)誤的解決方法。2011-10-10解析利用wsdl.exe生成webservice代理類的詳解
本篇文章是對(duì)利用wsdl.exe生成webservice代理類進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05ASP.NET?Core?MVC控制器請(qǐng)求依賴注入
這篇文章介紹了ASP.NET?Core?MVC控制器請(qǐng)求依賴注入的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04asp.net MVC利用自定義ModelBinder過(guò)濾關(guān)鍵字的方法(附demo源碼下載)
這篇文章主要介紹了MVC利用自定義ModelBinder過(guò)濾關(guān)鍵字的方法,結(jié)合實(shí)例形式詳細(xì)分析了自定義ModelBinder過(guò)濾關(guān)鍵字的原理與具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-03-03asp.net MVC實(shí)現(xiàn)無(wú)組件上傳圖片實(shí)例介紹
無(wú)組件實(shí)現(xiàn)上傳圖片使用input的file作為上傳選擇文件,具體實(shí)現(xiàn)如下:前后臺(tái)代碼很詳細(xì),感興趣的朋友們可不要錯(cuò)過(guò)了哈2013-05-05.net?core利用PdfSharpCore操作PDF實(shí)例教程
操作pdf是我們?nèi)粘i_(kāi)發(fā)中經(jīng)常遇到的功能,下面這篇文章主要給大家介紹了關(guān)于.net?core利用PdfSharpCore操作PDF實(shí)例的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12