碎碎念:
這周的主要工作還是集中于FOC中,因為羨慕稚暉君做出的漂亮Qt面板,因此在利用MATLAB復刻過程中,學習了一下serialport的使用。FOC的GUI部分就在加班加點寫作中啦,同時最近打算開一個新坑,大家可以期待一下哈哈哈。
歡迎大佬們點贊+收藏+關注~ o(* ̄▽ ̄*)ブ
目錄
1 串口接收
2 串口發(fā)送
考慮到互聯(lián)網中對MATLAB中最新的serialport的使用案例有些混亂,并且很多都是基于已經被淘汰的serial庫,嚴重缺乏易用性,因此在本文中給出簡單的串口收發(fā)模板,特別是串口回調函數的使用案例。
1 串口接收
串口接收是指,開發(fā)板將數據發(fā)送給電腦,電腦讀取數據并進行數據分析處理的過程。
想弄清楚怎么接收串口的數據,那你首先就需要知道串口的數據是怎么發(fā)送出來的。
試想這樣的應用場景,我的開發(fā)板上安裝了一個溫度傳感器,溫度傳感器采集的數據長度是3字節(jié)(24比特);我需要將開發(fā)板采集到的溫度信息實時顯示在屏幕上,我需要怎么做?
這其中需要注意的有下面幾點:
- 溫度傳感器是3字節(jié)的,如何確定接收到的某一個字節(jié)位于三個字節(jié)中的哪個位置?
- 實時顯示要求我需要對每次發(fā)送過來的數據做出響應,這種響應需要怎么做?
針對問題1:
其實這也是初學者常遇到的問題,有時候串口發(fā)送的數據就像一個堵不住的水管,完全不知道要怎么處理。
由于串口協(xié)議的限定,導致其每次發(fā)送的只能是一個字節(jié),對于多字節(jié)的數據【ABC】來說,就只能通過三個字節(jié)【A】、【B】、【C】來發(fā)送,如下圖所示(最左端為最先接收到的字節(jié)):
這就顯然會遇到問題,在任意一個時刻,我沒辦法確定接收到的數據到底處于【ABC】的哪一個位置;更致命的是,由于物理介質的影響,甚至可能會造成數據的丟失,這就更給數據的接受造成了影響。
如何解決這一問題呢?人們開始想到了“打包”的方式,也可以理解為我們常說的“幀”的概念。只要在每組數據的開頭加一些標志,表示出這是數據的最開始位置不就好了,即為下圖所示(最左端為最先接收到的字節(jié)):
假設我們設置的這個標志為【FF、FF】,當上位機檢測到連續(xù)的兩個【FF】時,就表示之后的三個字節(jié)分別為【A】、【B】、【C】。
這其實就解決了這個問題1,實現(xiàn)了對一幀中每一個字節(jié)的位置確定。
針對問題2:
解決問題1后,我們當然可以利用順序執(zhí)行的方式,來實現(xiàn)對串口數據的一次讀取以及數據處理。但是如何實現(xiàn)當每一次檢測到特定信號,就調用一次數據處理函數呢?
這就要先理解一下MATLAB中serialportlist的使用邏輯了,整體來說serialportlist是對serial的升級版本(在幫助頁面也有提到),其通過構建SerialObject對象的方式,來實現(xiàn)串口參數的設置以及讀寫。
具體細節(jié)可以參考MATLAB文檔serialport,太全面的參數設置過于冗余,不在本文討論范圍內。這里主要介紹兩個比較重要的概念緩沖區(qū)以及回調函數。
緩沖區(qū):
在serialport中,緩沖區(qū)是自動存在于SerialObject對象中,但是有時使用時(如本文)不需要針對性設置緩沖區(qū)的大小??梢岳斫鉃橐粋€長度固定的FIFO隊列,當檢測到特定信號的時候,將串口傳入的每一個字節(jié)的數據,按順序保存在里面,當長度滿了之后,就不再繼續(xù)在里面添加新的數據了。
可能會使用到的函數為
flush(SerialObj)
可以用來清空緩沖區(qū),常常用在串口對象初始化的時候。
回調函數:
這個是解決問題2的關鍵,回調函數可以理解為一個開關被觸發(fā)后需要進行的操作(或者簡單理解為單片機的中斷處理函數);我們可以通過SerialObject的對象設置,來設置檢測到什么信號(這個信號是作為一幀的結尾)的時候,執(zhí)行回調函數。
舉個方案A作為例子,我們可以設置檢測到【FF FF】信號的時候,執(zhí)行三個字節(jié)的數據讀取。(盡管不這樣用,后面會說為什么)
如上圖所示,當我們按照上面方案A的方式,設置回調函數的觸發(fā)條件,有什么問題呢?每當檢測到【FF FF】的時候,就會觸發(fā)回調函數。
看似沒問題,但是此時一幀的組合已經從【FF FF A B C】變成了【A B C FF FF】,因為我們提到回調函數敏感的是一幀的結尾。檢測到【FF FF】時,下一個字節(jié)顯然就是【A】。這其實是不規(guī)范的,我們不能理所當然地認為每一幀都是傳輸正確的。
舉個例子:
【 A B C FF FF】【 A B C FF FF】【 A B C FF FF】【 A B C FF FF】【 A B C FF FF】
中間紅色的ABC表示因為數據線接觸不良導致的傳輸錯誤,如果具有固定幀頭的話,或許幀頭也會出現(xiàn)錯誤,從而直接跳過這一幀錯誤的信號【 A B C FF FF】。
因此必須通過固定的幀頭來確定此時傳輸的是否是完整的數據。
這就需要我們進一步對一幀的結構,進行修改了,讓其完整地包含“幀頭”與“幀尾”。在MATLAB中給出了configureTerminator的方法,可以編輯SerialObject需要檢測到的幀尾信號。詳細解釋可以看configureTerminator官方文檔,其中有這樣的介紹:
configureTerminator(t,terminator) defines the terminator for both read and write communications with the remote host specified by the TCP/IP client t. Allowed terminator values are "LF" (default), "CR", "CR/LF", and integer values from 0 to 255. The syntax sets the Terminator property of t.
這里提到,我們可以設置需要檢測幀尾信號為“LF”、“CR”、“CR/LF”或一個0-255的整數(剛好對應了8位無符號數,也就是一字節(jié))。
按照上面的說明,我們可以對之前的幀進行下圖的修改,加上幀尾(最左端為最先接收到的字節(jié)):
這樣,我們就可以利用檢測幀尾(橙色部分),來實現(xiàn)對回調函數的調用啦。但是新的疑問又誕生了:我理解0-255的數字怎么發(fā)送,但是這畢竟是單字節(jié)的,會不會造成數據讀取混亂?上文提到的“LF”、“CR”、“CR/LF”這三個又是什么?(這也是困擾了我一段時間的問題)
“LF”、“CR”、“CR/LF”概念解釋:
引用自:CR,LF詳解_Berwyn丶的博客-CSDN博客_cr的16進制
從起源上來說,在計算機還沒有出現(xiàn)之前,有一種叫做電傳打字機(Teletype Model 33,Linux/Unix下的tty概念也來自于此)的玩意,每秒鐘可以打10個字符。但是它有一個問題,就是打完一行換行的時候,要用去0.2秒,正好可以打兩個字符。要是在這0.2秒里面,又有新的字符傳過來,那么這個字符將丟失。
于是,研制人員想了個辦法解決這個問題,就是在每行后面加兩個表示結束的字符。一個叫做“回車”,告訴打字機把打印頭定位在左邊界;另一個叫做“換行”,告訴打字機把紙向下移一行。這就是“換行”和“回車”的來歷,從它們的英語名字上也可以看出一二。
后來,計算機發(fā)明了,這兩個概念也就被般到了計算機上。那時,存儲器很貴,一些科學家認為在每行結尾加兩個字符太浪費了,加一個就可以。于是,就出現(xiàn)了分歧。在不同的系統(tǒng)中,就出現(xiàn)了下面的狀況:
注:這里并不是說在Windows系統(tǒng)中只能使用CR/LF作為幀尾,表格里說的是對應系統(tǒng)本文編輯器中的默認換行符。
系統(tǒng) 符號 名稱 十六進制(ASCII) Linux ’\n’ LF 0x0A Mac ’\r’ CR 0x0D Windows ’\r\n’ CR/LF 【0x0D?0x0A】
是不是感覺豁然開朗?那我們就可以理所當然的將之前的圖改為下面的樣子(最左端為最先接收到的字節(jié)):
讀到這里,我想讀者朋友們已經逐漸理解了最開始所說的:想弄清楚怎么接收串口的數據,那你首先就需要知道串口的數據是怎么發(fā)送出來的?;叵胍幌挛覀兊乃悸?,因為要實現(xiàn)多字節(jié)讀取,所以需要給一個固定的幀頭用來確定每個字節(jié)的位置;為了提供一個可以激活回調函數的信號,并且不影響幀頭的存在,我們需要添加一個幀尾。結合configureTerminator中的設置信號,我們發(fā)現(xiàn)可以使用“LF”、“CR”、“CR/LF”或者0-255的數字作為幀尾讓回調函數激活,通過查閱原來前面的三個“LF”、“CR”、“CR/LF”說的是換行符的ASCII碼,我們可以使用開發(fā)板讓他們發(fā)出對應的十六進制數據來表示。
至此,我們知道了數據從開發(fā)板上發(fā)送出來時的結構。對比四種幀尾,只有“CR/LF”是兩個字節(jié)的,對于溫度這種未知的數據信號來說,是最穩(wěn)妥的,可以更好的避免出現(xiàn)雷同情況,導致讀取錯誤。
舉個例子:
當我們發(fā)送的數據是:【FF FF A B C 幀尾】。
當幀尾是1字節(jié)很有可能出現(xiàn)【C】與【幀尾】相同的情況,如果【幀尾】是兩字節(jié),【B C】與之雷同的情況則會概率減小很多。
因此我們選擇在開發(fā)板中按照下圖的方式來發(fā)送數據給上位機(最左端為最先接收到的字節(jié)),這需要先在開發(fā)板中定義好,本文默認讀者已經完成了這部分,如果有需要的話,讀者也可以留言給我,我會單獨出一篇文章進行講解:
那么現(xiàn)在就可以開始激動人心(bushi)的MATLAB編程環(huán)節(jié)啦,基于MATLAB文檔serialport,下面給出一個簡單的模板:
Port_List = serialportlist("available");
SerialObj = serialport("COM7",115200);
configureTerminator(SerialObj,"CR/LF");
flush(SerialObj);
SerialObj.UserData = struct("Data",[]);
configureCallback(SerialObj,"terminator",@readSerialData);
% 回調函數
function readSerialData(src, ~)
data = read(src,7,"uint8");
src.UserData.Data = data;
ShowTemp(src);
end
% 溫度數據處理與展示
function ShowTemp(src)
if(src.UserData.Data(1:2) == [0xFF 0xFF])
Temperature = src.UserData.Data(3)*256*256 + src.UserData.Data(4)*256 + src.UserData.Data(5);
disp(Temperature);
end
end
下面對代碼進行一下講解:
Port_List = serialportlist("available");
展示出當前系統(tǒng)中可用的串口列表,與電腦設備管理器中的端口是對應的。
SerialObj = serialport("COM7",115200);利用serialport函數來構造一個串口對象SerialObj,設定對應的端口是COM7端口,波特率是115200。
configureTerminator(SerialObj,"CR/LF");設置需要檢測到的幀尾是"CR/LF"。
flush(SerialObj);清空串口對象的接收緩沖區(qū)。
SerialObj.UserData = struct("Data",[]);通過查看SerialObj對象的屬性,可以看到其中存在一個屬性叫做UserData,可以用來存儲數據,這里我們將其定義為一個結構體,里面自行定義只有一個叫做Data的數據。
configureCallback(SerialObj,"terminator",@readSerialData);指定回調函數,也就是第三個屬性提到的readSerialData函數,表示檢測到幀尾后需要進行的操作?!皌erminator”參數的意思是檢測結束符,讀者只需要修改最后一個參數readSerialData即可。
function readSerialData(src, ~)定義回調函數,src表示自定傳入的對象,因此不需要進行修改。
? ? data = read(src,7,"uint8");read函數表示從串口對象中讀取7字節(jié)的數據,因為是從檢測到結束符后面開始的也就是【FF FF A B C 0D 0A】這7個字節(jié)的內容?!皍int8”表示讀取的是8位無符號數。值得注意的是,這部分還有其他的函數可以使用,例如用來讀取一行字符的readline函數,同樣在MATLAB文檔serialport有明確介紹。
這里其實就可以進一步理解CR/LF之所以是換行符的原因了,從一個換行符讀取到另一個換行符之間,不就是讀取一行(readline)的含義嗎?
? ? src.UserData.Data = data;將讀取到的數據data存儲到對象屬性UserData里面的結構體下的Data中,實現(xiàn)數據的存儲。數據的存儲方式是一個長度為7的數組,可以直接利用索引1-7進行調用。
????ShowTemp(src);
調用數據處理的函數,用來預處理和顯示接收到的數據。
end
function ShowTemp(src)定義數據處理函數
? ? if(src.UserData.Data(1:2) == [0xFF 0xFF])使用if語句,判斷數據頭是否是【FF FF】,確定是否有傳輸錯誤。
? ? ? ? Temperature = src.UserData.Data(3)*256*256 + src.UserData.Data(4)*256 + src.UserData.Data(5);之后的三個字節(jié)是【A B C】,每個是8比特,因此要乘以它們的權值進行計算,獲得原始的數據。
? ? ? ? disp(Temperature);展示當前的數據到控制臺。
? ? end
end
2 串口發(fā)送
串口發(fā)送是指,電腦將需要發(fā)送的數據(一般是指令或者參數設置信息)整合好,發(fā)送給開發(fā)板的過程。
相信有了前面串口接收的基礎,這對大家來說就非常簡單了,在這里,我們還是假設一個應用場景來進行講解,由于很對讀者會使用到GUI進行串口發(fā)送的測試,這里我們就以GUI中的文本輸入框的數據格式為例。
在GUI中,我需要將一個十六進制字符串“FF 01 02 03 04”發(fā)送給開發(fā)板,我需要怎么做?GUI如下圖所示,是“文本區(qū)域”類型的模塊:
這里需要注意下面的問題:
- 如何從GUI中獲取數據(僅限于GUI使用時,如果是腳本文件,則需要按照字符串來進行處理)。
- GUI中獲取到的數據,實際上是cell類型,而不是單純的字符串類型(僅限于GUI使用時,如果是腳本文件,則需要按照字符串來進行處理)。
- 如何將數據進行分割并發(fā)送。
這里由于三個問題相當明確且容易解決,因此我直接給出串口發(fā)送函數write的使用案例:
Port_List = serialportlist("available");
SerialObj = serialport("COM7",115200);
send_data = get(app.TextAreaTabSend, "value");
HEX = hex2dec(strsplit(cell2mat(send_data), " "));
write(app.SerialObject,HEX,"uint8");
下面對代碼進行一下講解:
Port_List = serialportlist("available");
展示出當前系統(tǒng)中可用的串口列表,與電腦設備管理器中的端口是對應的。
SerialObj = serialport("COM7",115200);利用serialport函數來構造一個串口對象SerialObj,設定對應的端口是COM7端口,波特率是115200。
send_data = get(app.TextAreaTabSend, "value");從GUI中獲取當前TextArea中的值信息,返回的時cell類型的數據。
HEX ? ? ? = hex2dec(strsplit(cell2mat(send_data), " "));從內層到外層,依次完成cell2mat()將cell類型轉為mat類型;strsplit()將mat類型按照空格進行分割;hex2dec()將字符串視為hex類型的數據轉為十進制進行傳輸。
如果是單純的字符串操作,則換為下面的函數即可:
HEX ? ? ? = hex2dec(strsplit(send_str, " "));
將字符串先進行分割,然后轉為十進制的數組。
注意,這兩種寫法我都是默認,發(fā)送的信息必須每個字節(jié)之間使用空格進行分割處理,因為使用的時write函數,并且是uint8類型。
write(SerialObj ,HEX,"uint8");將數據HEX發(fā)送給SerialObj對象,實現(xiàn)發(fā)送。這里使用的是write函數,其實還有另一個函數writeline,讀者可以參考MATLAB文檔serialport進行查閱。
至此,就完成了全部的數據收發(fā)任務啦,當需要關閉串口時,只需要使用下面的函數,刪除創(chuàng)建的對象即可。
delete(SerialObj); %通過刪除對象來斷開串口
最后再提及一下,為什么我都是使用的write以及read的uint8類型呢?一方面我們的應用環(huán)境還是數字的傳輸為主,字符串的傳輸這里并沒有怎么涉及到。另一方面,逐個字節(jié)的收發(fā),在我看來是更方便理解其中串口協(xié)議原理的,并且ASCII本身就是8位無符號數。
首次嘗試這樣的寫作方式,希望本篇文章能夠給讀者一些幫助,同時由于本人水平有限,如果有一些問題的話,請務必留言指出,我一定虛心接受!文章來源:http://www.zghlxwxcb.cn/news/detail-417068.html
這就是本期的全部內容啦,如果你喜歡我的文章,不要忘了點贊+收藏+關注,分享給身邊的朋友哇~文章來源地址http://www.zghlxwxcb.cn/news/detail-417068.html
到了這里,關于MATLAB :【11】一文帶你讀懂serialport串口收發(fā)原理與實現(xiàn)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!