如何寫一個健壯且高效的串口接收程序?

學單片機的大概最先、最常寫的通信程序應該就是串口程序了,但是如何寫出一個健壯且高效的串口接收程序呢?接下來魚鷹將根據多年的開發經驗教你如何編寫串口接收程序(可在公眾號獲取個人編寫的串口接收源碼)。

本篇文章包含以下內容,很長,但乾貨滿滿,就看你能吸收多少了:

1、 傳入參數指針

2、 互斥鎖釋放順序

3、 數據幀檢查

4、 串口空閒

5、 通信吞吐量

內容很多,魚鷹慢慢寫,道友您也請慢慢看。

為了更好的理解接下來的知識點,魚鷹將設計一個串口框架,讓道友心中有一個參考方向。

本篇重點在於解決如何寫一個健壯、高效的串口接收數據,發送與接收處理過程略講。

幀格式

先聊聊幀格式,一般來說,一個數據幀有以下幾部分內容:

如何寫一個健壯且高效的串口接收程序?

幀頭

幀頭用於分辨一個數據幀的起始,這個幀頭必須足夠特殊才行,因為它是分辨一個幀的起始,那麼什麼樣的幀頭是足夠特殊的數據呢?保證這個數據在一個幀內最好只出現一次的數據,那就是幀頭,比如 0x55、0xAA 之類的。而且最好有兩個字節以上,這樣幀頭才更加獨一無二。

但是數據域內的數據你是沒辦法保障不包含和幀頭一樣的數據。

那麼如果不湊巧,除了幀頭外其他部分也有這樣的兩個字節的幀頭,那會出現什麼問題?

幾乎不會出現問題。因為一般來說數據都是一幀一幀發送的,只要你前面的數據幀傳輸正確,那麼即使下一幀的數據中有和幀頭一樣的數據(包括幀頭)也沒有問題,因為幀頭判斷已經在開始就判斷成功了,就不會繼續判斷後面的數據是否是幀頭了。

那麼為什麼說是幾乎,因為如果上一幀數據接收錯誤,那麼程序必須再找一次幀頭才行(單字節接收時是如此,採用空閒中斷的話就不需要這麼麻煩),這就導致找幀頭的時候在幀頭數據之外尋找了,很可能這些數據就有幀頭。

但是即使幀頭數據之外的假幀頭真的存在,也沒關係,還有第二重保障,那就是校驗,即使找到了一個錯誤的幀頭,那麼數據校驗這一關也很難過去,所以放寬心。

如果校驗也湊巧通過了,那還有第三重保障:幀尾。應該到不了這裡吧,畢竟這比中彩票還難。

又要上一幀數據接收錯誤,還要當前幀除了幀頭之外還有幀頭,另外你還能跳過校驗的檢查(還有功能字、長度信息的檢查),太難了。所以只要通過了這些檢查,你就可以認為這個數據幀是可用的了。所以一幀數據接收錯誤,導致的問題最多隻是丟失了這幀數據,對後續接收是不會有影響的(前提是你這個接收程序設計的足夠好),發送端在發送超時後再發送一次即可,所以重發機制很重要

事實上,如果你採用串口空閒中斷,幀頭、幀尾都可以不用,但一般來說,幀頭都會保留,幀尾可以不需要,這是為了當單片機沒有串口空閒中斷時考慮,當然也可能有其他考慮,所以幀頭得保留。

功能字

功能字主要用於說明該數據幀的功能,當然也可以作為函數指針的索引,一個索引值代表了一個具體功能,據此可找到對應的功能函數。

比如,設計一個函數指針數組,通過功能字進行索引,進而跳轉到對應的功能函數中處理。

如何寫一個健壯且高效的串口接收程序?

特別注意的是,設計功能字的時候,要考慮兼容性,對數據幀的功能進行劃分,不要想到一個算一個,功能字也不要隨便安排,不然在以後增加數據幀的時候會很麻煩。

比如說,只有一個字節的功能字,前四位作為一個大類,後四位作為大類中具體類。這樣就可以將系統數據通信幀分為 16 個大類,每個大類下有 16 個可用的具體類,當你增加功能字的時候,就可以根據你的設計來確定屬於哪個大類了,然後再插入進去。這樣在管理、維護這些通信數據時你會發現很方便。

這個思想其實在 ARM 內核的中斷系統和設計 uCOS II 任務優先級的時候都有,而魚鷹在設計項目的通信協議的時候就是運用了這些思想。

如何寫一個健壯且高效的串口接收程序?

長度

長度信息也是一個非常關鍵的數據,別小看了它,因為它,魚鷹用了將近一個星期的時間才把一個 HardFaul 問題解決了,雖然這個程序 bug 不是我寫的(魚鷹一直用的是串口空閒接收方式,這個 bug 自然而然就跳過了),但確實很容易出錯。

因為它是決定了你這個數據域長度的關鍵信息(一般長度信息代表數據域的長度,而不包含其它部分長度),也是這個數據幀的長度信息(加上固定字節長度就是幀長度了),更是接收程序還要接收多少數據的關鍵信息(對於空閒中斷接收方式不算關鍵,這裡的不關鍵是指不會造成程序異常問題)。

比如說你的程序剛好將幀頭、幀尾、功能字判斷完畢,然後中斷程序因為種種原因導致沒有及時接收串口數據,那麼你可能得到的就是錯誤的數據,然後這個錯誤的長度數據就可能導致你的棧幀或者全局變量被破壞(單字節接收情況下就可能出現,因為魚鷹碰到過),這是很嚴重的事情。所以在接收數據域的數據之前一定一定要判斷這個長度信息(空閒中斷除外)是否合法,不合法的話及時扔掉這幀數據,開始下一幀的數據檢查。

所以為了保證及時接收數據,最好採用 DMA 傳輸。

數據域

這個沒啥好說的,就是整個幀你真正需要發送的數據。而為了讓你的發送函數能接收各種類型的數據,那麼把參數類型設置為 void * 會是不錯的選擇。

校驗

一個數據在接收過程中可能會被幹擾,導致接收到錯誤的數據,那麼如何保證這幀數據的完整與準確性呢,就在校驗這一關了。

校驗有很多方式,和校驗、CRC 校驗等(奇偶校驗是針對一個字節的,不是數據幀)。

和校驗算法簡單,CPU 運算量小,累加最後只取最低字節即可(注意不是高字節,想想為什麼),或者保存累加和的變量就是一個字節空間,這樣就不需要額外操作了。

CRC 校驗,這個算法複雜,理解起來比較困難,但一般來說可以直接拿來用,因為它是對每一位(bit)進行校驗,所以糾錯率很高,幾乎不存在發現不了的數據錯誤,但正因為對每一位進行檢查,所以 CPU 運算量較大,但是有的單片機是可以硬件計算 CRC 校驗值的(比如 stm32)。不過現在 CPU 運算速度都挺快的,軟件運算也是可以接受的。

那麼該怎麼校驗呢?是從幀頭開始到數據域部分,還是說直接校驗數據部分?其實都可以,區別就是運算量問題,不過問題不大(最好是從頭開始校驗,以保證整幀數據的準確性)。

幀尾

前面說了,幀尾在空閒中斷中可以不用,RXNE 中斷接收時其實也可以不用,當然也可以加上,好處就是當你用串口助手查看數據流時,可以觀察出一幀數據是否發送完整了。

最後再說說為什麼在數據域前面設計四個字節大小,除了協議本身需要外,還有一個原因就是強制類型轉化需要,我們知道,一般來說,賦值時都有字節對齊的限制(實際上有的 CPU 可以不對齊進行賦值),stm32 是 32 位的,那麼四字節對齊是最合適的,這樣就可以直接將我們收到的數據轉化為需要的數據類型了。

傳輸過程

聊完了幀格式,再從大的方向看串口的傳輸過程:

如何寫一個健壯且高效的串口接收程序?

當發送端發送第一幀數據包時,接收端通過某種方式接收(串口接收非空 RXNE 中斷、串口空閒 IDLE 中斷),為了讓串口能夠觸發空閒中斷,必須在發送端兩個發送幀之間插入一段空閒時間(就是在此時間內不發數據,紅色部分),保證空閒中斷的準確觸發。

同理,為了讓發送端也能正常接收接收端的數據,也需要控制接收端的發送,不能在返回一幀數據時立馬發送下一幀數據,不然觸發不了發送端的空閒中斷。

事實上,有些程序員設計的發送、接收過程比這個簡單一些。即只有當接收端接收到一幀數據並返回一幀數據之後,發送端才能繼續發送數據,這樣一來,我們只需要控制好接收端的頻率,就可以控制整個通信過程,也能控制通信頻率。

如何寫一個健壯且高效的串口接收程序?

但為什麼還要設計成第一種傳輸情況呢?這是為了充分利用串口,增大數據吞吐率(這個後面再說)。

另外,不知道你是否觀察到圖中的每個數據幀佔用的時間是不一樣的,這是因為每個數據幀不可能都是一樣長的,它們是不定長的數據包,所以你的定時不能從發送開始定時,而是從發送完成後開始定時控制空閒時間。

下集精彩,串口的軟件設計,喜歡的話記得關注魚鷹哦!


分享到:


相關文章: