0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

lhl545545 ? 來源:玩轉(zhuǎn)單片機(jī) ? 作者:玩轉(zhuǎn)單片機(jī) ? 2020-06-09 09:34 ? 次閱讀

本篇文章包含以下內(nèi)容,很長,但干貨滿滿,就看你能吸收多少了(這將是魚鷹本階段公眾號技術(shù)分享的最后一篇收尾文章):

傳入參數(shù)指針

互斥鎖釋放順序

數(shù)據(jù)幀檢查

串口空閑

通信吞吐量

內(nèi)容很多,魚鷹慢慢寫,道友您也請慢慢看。

為了更好的理解接下來的知識點(diǎn),魚鷹將設(shè)計(jì)一個(gè)串口框架,讓道友心中有一個(gè)參考方向。

本篇重點(diǎn)在于解決如何寫一個(gè)健壯、高效的串口接收數(shù)據(jù),發(fā)送與接收處理過程略講。

幀格式

先聊聊幀格式,一般來說,一個(gè)數(shù)據(jù)幀有以下幾部分內(nèi)容:

幀頭

幀頭用于分辨一個(gè)數(shù)據(jù)幀的起始,這個(gè)幀頭必須足夠特殊才行,因?yàn)樗欠直嬉粋€(gè)幀的起始,那么什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?保證這個(gè)數(shù)據(jù)在一個(gè)幀內(nèi)最好只出現(xiàn)一次的數(shù)據(jù),那就是幀頭,比如0x55、0xAA之類的。而且最好有兩個(gè)字節(jié)以上,這樣幀頭才更加獨(dú)一無二。

但是數(shù)據(jù)域內(nèi)的數(shù)據(jù)你是沒辦法保障不包含和幀頭一樣的數(shù)據(jù)。

那么如果不湊巧,除了幀頭外其他部分也有這樣的兩個(gè)字節(jié)的幀頭,那會(huì)出現(xiàn)什么問題?

幾乎不會(huì)出現(xiàn)問題。因?yàn)橐话銇碚f數(shù)據(jù)都是一幀一幀發(fā)送的,只要你前面的數(shù)據(jù)幀傳輸正確,那么即使下一幀的數(shù)據(jù)中有和幀頭一樣的數(shù)據(jù)(包括幀頭)也沒有問題,因?yàn)閹^判斷已經(jīng)在開始就判斷成功了,就不會(huì)繼續(xù)判斷后面的數(shù)據(jù)是否是幀頭了。

那么為什么說是幾乎,因?yàn)槿绻弦粠瑪?shù)據(jù)接收錯(cuò)誤,那么程序必須再找一次幀頭才行(單字節(jié)接收時(shí)是如此,采用空閑中斷的話就不需要這么麻煩),這就導(dǎo)致找?guī)^的時(shí)候在幀頭數(shù)據(jù)之外尋找了,很可能這些數(shù)據(jù)就有幀頭。

但是即使幀頭數(shù)據(jù)之外的假幀頭真的存在,也沒關(guān)系,還有第二重保障,那就是校驗(yàn),即使找到了一個(gè)錯(cuò)誤的幀頭,那么數(shù)據(jù)校驗(yàn)這一關(guān)也很難過去,所以放寬心。

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

又要上一幀數(shù)據(jù)接收錯(cuò)誤,還要當(dāng)前幀除了幀頭之外還有幀頭,另外你還能跳過校驗(yàn)的檢查(還有功能字、長度信息的檢查),太難了。所以只要通過了這些檢查,你就可以認(rèn)為這個(gè)數(shù)據(jù)幀是可用的了。所以一幀數(shù)據(jù)接收錯(cuò)誤,導(dǎo)致的問題最多只是丟失了這幀數(shù)據(jù),對后續(xù)接收是不會(huì)有影響的(前提是你這個(gè)接收程序設(shè)計(jì)的足夠好),發(fā)送端在發(fā)送超時(shí)后再發(fā)送一次即可,所以重發(fā)機(jī)制很重要。

事實(shí)上,如果你采用串口空閑中斷,幀頭、幀尾都可以不用,但一般來說,幀頭都會(huì)保留,幀尾可以不需要,這是為了當(dāng)單片機(jī)沒有串口空閑中斷時(shí)考慮,當(dāng)然也可能有其他考慮,所以幀頭得保留。

功能字

功能字主要用于說明該數(shù)據(jù)幀的功能,當(dāng)然也可以作為函數(shù)指針的索引,一個(gè)索引值代表了一個(gè)具體功能,據(jù)此可找到對應(yīng)的功能函數(shù)。

比如,設(shè)計(jì)一個(gè)函數(shù)指針數(shù)組,通過功能字進(jìn)行索引,進(jìn)而跳轉(zhuǎn)到對應(yīng)的功能函數(shù)中處理。

特別注意的是,設(shè)計(jì)功能字的時(shí)候,要考慮兼容性,對數(shù)據(jù)幀的功能進(jìn)行劃分,不要想到一個(gè)算一個(gè),功能字也不要隨便安排,不然在以后增加數(shù)據(jù)幀的時(shí)候會(huì)很麻煩。

比如說,只有一個(gè)字節(jié)的功能字,前四位作為一個(gè)大類,后四位作為大類中具體類。這樣就可以將系統(tǒng)數(shù)據(jù)通信幀分為16個(gè)大類,每個(gè)大類下有16個(gè)可用的具體類,當(dāng)你增加功能字的時(shí)候,就可以根據(jù)你的設(shè)計(jì)來確定屬于哪個(gè)大類了,然后再插入進(jìn)去。這樣在管理、維護(hù)這些通信數(shù)據(jù)時(shí)你會(huì)發(fā)現(xiàn)很方便。

這個(gè)思想其實(shí)在ARM內(nèi)核的中斷系統(tǒng)和設(shè)計(jì) uCOS II 任務(wù)優(yōu)先級的時(shí)候都有,而魚鷹在設(shè)計(jì)項(xiàng)目的通信協(xié)議的時(shí)候就是運(yùn)用了這些思想。

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

(圖片來源于《權(quán)威指南》)

長度

長度信息也是一個(gè)非常關(guān)鍵的數(shù)據(jù),別小看了它,因?yàn)樗?,魚鷹用了將近一個(gè)星期的時(shí)間才把一個(gè)HardFaul問題解決了,雖然這個(gè)程序bug不是我寫的(魚鷹一直用的是串口空閑接收方式,這個(gè)bug自然而然就跳過了),但確實(shí)很容易出錯(cuò)。

因?yàn)樗菦Q定了你這個(gè)數(shù)據(jù)域長度的關(guān)鍵信息(一般長度信息代表數(shù)據(jù)域的長度,而不包含其它部分長度),也是這個(gè)數(shù)據(jù)幀的長度信息(加上固定字節(jié)長度就是幀長度了),更是接收程序還要接收多少數(shù)據(jù)的關(guān)鍵信息(對于空閑中斷接收方式不算關(guān)鍵,這里的不關(guān)鍵是指不會(huì)造成程序異常問題)。

比如說你的程序剛好將幀頭、幀尾、功能字判斷完畢,然后中斷程序因?yàn)榉N種原因?qū)е聸]有及時(shí)接收串口數(shù)據(jù),那么你可能得到的就是錯(cuò)誤的數(shù)據(jù),然后這個(gè)錯(cuò)誤的長度數(shù)據(jù)就可能導(dǎo)致你的棧幀或者全局變量被破壞(單字節(jié)接收情況下就可能出現(xiàn),因?yàn)轸~鷹碰到過),這是很嚴(yán)重的事情。所以在接收數(shù)據(jù)域的數(shù)據(jù)之前一定一定要判斷這個(gè)長度信息(空閑中斷除外)是否合法,不合法的話及時(shí)扔掉這幀數(shù)據(jù),開始下一幀的數(shù)據(jù)檢查。

所以為了保證及時(shí)接收數(shù)據(jù),最好采用DMA傳輸。

數(shù)據(jù)域

這個(gè)沒啥好說的,就是整個(gè)幀你真正需要發(fā)送的數(shù)據(jù)。而為了讓你的發(fā)送函數(shù)能接收各種類型的數(shù)據(jù),那么把參數(shù)類型設(shè)置為 void * 會(huì)是不錯(cuò)的選擇。

校驗(yàn)

一個(gè)數(shù)據(jù)在接收過程中可能會(huì)被干擾,導(dǎo)致接收到錯(cuò)誤的數(shù)據(jù),那么如何保證這幀數(shù)據(jù)的完整與準(zhǔn)確性呢,就在校驗(yàn)這一關(guān)了。

校驗(yàn)有很多方式,和校驗(yàn)、CRC校驗(yàn)等(奇偶校驗(yàn)是針對一個(gè)字節(jié)的,不是數(shù)據(jù)幀)。

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

CRC校驗(yàn),這個(gè)算法復(fù)雜,理解起來比較困難,但一般來說可以直接拿來用,因?yàn)樗菍γ恳晃唬╞it)進(jìn)行校驗(yàn),所以糾錯(cuò)率很高,幾乎不存在發(fā)現(xiàn)不了的數(shù)據(jù)錯(cuò)誤,但正因?yàn)閷γ恳晃贿M(jìn)行檢查,所以CPU運(yùn)算量較大,但是有的單片機(jī)是可以硬件計(jì)算CRC校驗(yàn)值的(比如stm32)。不過現(xiàn)在CPU運(yùn)算速度都挺快的,軟件運(yùn)算也是可以接受的。

那么該怎么校驗(yàn)?zāi)??是從幀頭開始到數(shù)據(jù)域部分,還是說直接校驗(yàn)數(shù)據(jù)部分?其實(shí)都可以,區(qū)別就是運(yùn)算量問題,不過問題不大(最好是從頭開始校驗(yàn),以保證整幀數(shù)據(jù)的準(zhǔn)確性)。

幀尾

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

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

傳輸過程

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

當(dāng)發(fā)送端發(fā)送第一幀數(shù)據(jù)包時(shí),接收端通過某種方式接收(串口接收非空RXNE中斷、串口空閑IDLE中斷),為了讓串口能夠觸發(fā)空閑中斷,必須在發(fā)送端兩個(gè)發(fā)送幀之間插入一段空閑時(shí)間(就是在此時(shí)間內(nèi)不發(fā)數(shù)據(jù),紅色部分),保證空閑中斷的準(zhǔn)確觸發(fā)。

同理,為了讓發(fā)送端也能正常接收接收端的數(shù)據(jù),也需要控制接收端的發(fā)送,不能在返回一幀數(shù)據(jù)時(shí)立馬發(fā)送下一幀數(shù)據(jù),不然觸發(fā)不了發(fā)送端的空閑中斷。

事實(shí)上,有些程序員設(shè)計(jì)的發(fā)送、接收過程比這個(gè)簡單一些。即只有當(dāng)接收端接收到一幀數(shù)據(jù)并返回一幀數(shù)據(jù)之后,發(fā)送端才能繼續(xù)發(fā)送數(shù)據(jù),這樣一來,我們只需要控制好接收端的頻率,就可以控制整個(gè)通信過程,也能控制通信頻率。

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

另外,不知道你是否觀察到圖中的每個(gè)數(shù)據(jù)幀占用的時(shí)間是不一樣的,這是因?yàn)槊總€(gè)數(shù)據(jù)幀不可能都是一樣長的,它們是不定長的數(shù)據(jù)包,所以你的定時(shí)不能從發(fā)送開始定時(shí),而是從發(fā)送完成后開始定時(shí)控制空閑時(shí)間。

軟件設(shè)計(jì)

上面所有的內(nèi)容都是設(shè)定一些條件、需求,那么該如何實(shí)現(xiàn)軟件設(shè)計(jì)呢?畢竟說的再多,如果不能實(shí)現(xiàn)這些,又有什么用呢?talk is cheap, show me the code。

下圖設(shè)計(jì)了三個(gè)數(shù)據(jù)幀:GetVision(),GetSN(),GetMsg()。

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

GetVision()用于獲取硬件版本號、軟件版本號。

GetSN()用于獲取產(chǎn)品序列號,用于識別唯一設(shè)備。

GetMsg()用于獲取消息,可以獲取各種傳感器數(shù)據(jù),事實(shí)上,如果數(shù)據(jù)量多的話,根據(jù)傳感器的不同,會(huì)根據(jù)需要設(shè)計(jì)各種不同的數(shù)據(jù)幀(功能字不同)。

在軟件設(shè)計(jì)上一般都會(huì)對這些函數(shù)設(shè)計(jì)一個(gè)統(tǒng)一的函數(shù)類型,用函數(shù)指針數(shù)組統(tǒng)一管理。

既要統(tǒng)一,又要體現(xiàn)差異性,函數(shù)參數(shù)就顯得很有必要了。

這里設(shè)計(jì)了兩個(gè)參數(shù),一個(gè)是void* (無類型指針),一個(gè)是length(長度)。

無類型指針主要是用于傳遞數(shù)據(jù)域的數(shù)據(jù)地址,而數(shù)據(jù)域的數(shù)據(jù)可能是整型、浮點(diǎn)型、結(jié)構(gòu)體、枚舉、聯(lián)合體等,為了保證傳入的各種數(shù)據(jù)類型在不通過強(qiáng)制轉(zhuǎn)化情況下都能兼容,設(shè)計(jì)為 void * 就顯得很有必要了。

實(shí)際上為了顯得更專業(yè)性,加上 const 修飾會(huì)是不錯(cuò)的選擇,因?yàn)檫@可以保證緩存數(shù)據(jù)不被修改(事實(shí)上只能保證不被程序員修改,而不能保證程序本身,這個(gè)后面會(huì)解釋)。

長度,長度參數(shù)是一個(gè)很關(guān)鍵的參數(shù),為了保證長度的準(zhǔn)確性,建議使用sizeof 獲取。

有人覺得sizeof 好像一個(gè)函數(shù),會(huì)不會(huì)導(dǎo)致效率低下啊,畢竟每次通信都要計(jì)算一次長度,那你就大特大錯(cuò)了。事實(shí)上,只要你的類型定義定義好了(不管是內(nèi)置的類型定義還是自定義的結(jié)構(gòu)體、枚舉、聯(lián)合體),編譯器都能確定 sizeof 最終的數(shù)據(jù)長度,根本不存在計(jì)算過程。

用 sizeof 的兩個(gè)好處:

1、可以忽略字節(jié)對齊問題(不同平臺(tái)不能忽略,比如window和單片機(jī)通信)。因?yàn)榫幾g器為了數(shù)據(jù)讀寫效率更高,一般會(huì)對數(shù)據(jù)進(jìn)行地址對齊,這樣一來手工計(jì)算一個(gè)數(shù)據(jù)類型的長度變得麻煩(當(dāng)然你可以說使用某些手段讓數(shù)據(jù)不進(jìn)行對齊,這個(gè)另說),而 sizeof 將智能且準(zhǔn)確計(jì)算數(shù)據(jù)大小。

2、當(dāng)你使用 sizeof 時(shí),兼容性更強(qiáng),也顯得更專業(yè)。程序修修改改很正常,一個(gè)數(shù)據(jù)結(jié)構(gòu)改來改去也很正常,特別是開發(fā)初期更是如此。但是不管你怎么改,只要在編譯器看來是固定長度的數(shù)據(jù)類型,那么 sizeof 就能在鏈接程序前計(jì)算出來;并且即使你后來加了數(shù)據(jù)不對齊的限制(加了這個(gè)限制后,很可能數(shù)據(jù)大小變?。?,也能準(zhǔn)確計(jì)算。別問為什么,就是這么任性。

所以為了減小出錯(cuò)的可能性、減少勞動(dòng)量,sizeof 是不錯(cuò)的選擇。

當(dāng)接收到數(shù)據(jù)地址和長度信息后,就可以進(jìn)行發(fā)送了。

因?yàn)橹挥袛?shù)據(jù)域的數(shù)據(jù),為了組成一幀完整的數(shù)據(jù),就必須加入打包過程。加上數(shù)據(jù)幀頭、功能字、數(shù)據(jù)長度、校驗(yàn)等數(shù)據(jù)。

當(dāng)一幀數(shù)據(jù)打包好之后,就可以進(jìn)行發(fā)送了,發(fā)送可以采用循環(huán)查詢發(fā)送,也可使用發(fā)送空TXE中斷,當(dāng)然還是建議使用DMA發(fā)送,這樣你可以還沒等它發(fā)送完就可處理其它事情了。

以上就是發(fā)送過程,接收過程也是同理,根據(jù)功能字來調(diào)用相應(yīng)的函數(shù)進(jìn)行回復(fù)。

事實(shí)上,如果只是數(shù)據(jù)的傳輸過程,完全可以使用一個(gè)發(fā)送函數(shù)實(shí)現(xiàn)數(shù)據(jù)的特異性傳輸,這樣就可以減少一層數(shù)據(jù)傳遞,但是有些通信幀不只是數(shù)據(jù)的傳輸,可能在接收、發(fā)送時(shí)作一些其他處理(比如清除、設(shè)置某些標(biāo)志位),所以需要再增加一層,用于進(jìn)行差異性處理。

以上就是本篇內(nèi)容的基礎(chǔ)內(nèi)容了,你以為快完了?你錯(cuò)了,現(xiàn)在只是剛開始而已,魚鷹寫本篇筆記的最終目的還在后面。

這只是前菜,正文才剛開始。

串口接收遇到的那些問題

以下內(nèi)容不會(huì)用太多的筆墨描述如何寫發(fā)送、接收函數(shù),而是重點(diǎn)關(guān)注串口接收過程中可能遇到的一些問題,如果說描述到了發(fā)送、接收函數(shù),別會(huì)錯(cuò)意,順帶的。

以下大部分問題都是因?yàn)椴捎肦XNE(接收不為空)中斷方式導(dǎo)致的問題,只有一個(gè)問題是魚鷹從前沒有考慮到,也是IDLE + DMA方式不可忽略的問題。

這就是為什么魚鷹建議采用IDLE + DMA 的原因,不僅是因?yàn)樾蕟栴},更因?yàn)樗鼙苊夂芏鄦栴},當(dāng)然水平足夠高的話,采用RXNE也是完全(“完全”就未必,里面有一個(gè)問題是RXNE方式難以避免的問題)沒有問題。

事實(shí)上,即使魚鷹采用RXNE方式接收數(shù)據(jù),也能避免以下大部分的問題,因?yàn)轸~鷹的基礎(chǔ)足夠扎實(shí),會(huì)在一開始編寫代碼的時(shí)候自然而然避免一些問題的出現(xiàn)。

但是看完以下內(nèi)容后,相信各位道友寫出一個(gè)高效且健壯的串口接收程序根本不是問題,因?yàn)檫@就是所謂的經(jīng)驗(yàn)啊。

傳入?yún)?shù)指針

前面魚鷹已經(jīng)提到了需要一個(gè)指針作為函數(shù)的參數(shù),這里說說這個(gè)指針問題。

我們知道,為了維護(hù)方便,也是為了節(jié)省空間,一般都會(huì)將類似的功能整合成一個(gè)函數(shù),比如串口經(jīng)常要用的發(fā)送、接收功能,但是所發(fā)送的數(shù)據(jù)內(nèi)存空間可能就處于五湖四海了,他們通過指針來指向?qū)⒁l(fā)送的數(shù)據(jù)。

為了節(jié)省 RAM 空間或者其它不為人知的原因,傳遞給發(fā)送函數(shù)的指針就是實(shí)際發(fā)送數(shù)據(jù)的地址,并且在計(jì)算校驗(yàn)值的時(shí)候也是直接使用這個(gè)地址進(jìn)行校驗(yàn)計(jì)算,然后采用循環(huán)查詢的方式發(fā)送數(shù)據(jù),這樣一來,就不必拷貝一個(gè)數(shù)據(jù)的副本進(jìn)行發(fā)送,而是直接從數(shù)據(jù)源的地址進(jìn)行發(fā)送,節(jié)省了部分 RAM 空間。

但是這樣真的好嗎?

你是否考慮過在計(jì)算數(shù)據(jù)幀校驗(yàn)值的時(shí)候,數(shù)據(jù)源改變了的問題呢?

比如說你采用和校驗(yàn),數(shù)據(jù)一開始是0x55,計(jì)算數(shù)據(jù)幀的校驗(yàn)和值為tx_sum,然后被中斷程序或者DMA修改了這個(gè)數(shù)據(jù)源,變成了 0xAA,此時(shí)你再使用這個(gè)數(shù)據(jù)地址進(jìn)行發(fā)送,接收端接收到了0xAA,接收端計(jì)算校驗(yàn)和的時(shí)候是 rx_sum,那么rx_sum 必然不等于 tx_sum,然后接收端就認(rèn)為該數(shù)據(jù)幀是錯(cuò)誤的,然后丟失這幀數(shù)據(jù),而這種情況是比較少見的,但確實(shí)是會(huì)偶爾出現(xiàn)接收錯(cuò)誤的情況(當(dāng)時(shí)發(fā)現(xiàn)這個(gè)問題時(shí)始終不得其解,明明我發(fā)送的是這個(gè)校驗(yàn)值,為什么你計(jì)算的校驗(yàn)值是另一個(gè)?開始懷疑是校驗(yàn)函數(shù)的問題,但其他數(shù)據(jù)幀計(jì)算時(shí)沒有問題,只有一種數(shù)據(jù)幀會(huì)出現(xiàn)問題,然后魚鷹懷疑是數(shù)據(jù)源的問題,是的,魚鷹很快就懷疑數(shù)據(jù)源的問題,但當(dāng)時(shí)驗(yàn)證時(shí),只改了校驗(yàn)?zāi)遣糠值刂?,發(fā)送時(shí)的地址還是使用源地址,導(dǎo)致問題還是沒解決,過了好久之后才發(fā)現(xiàn)這個(gè)發(fā)送地址沒改,囧。所以說,即使你的思路是對的,但如果你解決時(shí)錯(cuò)了,問題也很嚴(yán)重)。

如果說接收端(從機(jī))具有重發(fā)機(jī)制,那么問題不是很大,丟失一幀數(shù)據(jù)而已,再重發(fā)就是,但事實(shí)是,一般串口設(shè)計(jì)成主從模式,主機(jī)會(huì)在沒有接收到從機(jī)的應(yīng)答數(shù)據(jù)時(shí)會(huì)進(jìn)行重發(fā),但是從機(jī)一般不會(huì)主動(dòng)重發(fā)數(shù)據(jù)的,它無法判斷主機(jī)是否成功接收,而從機(jī)一般會(huì)在成功發(fā)送完數(shù)據(jù)后開始清除一些標(biāo)志位(比如鍵盤按鍵數(shù)據(jù)清空,不然主機(jī)下次獲取按鍵信息時(shí)還是同一個(gè)按鍵數(shù)據(jù)),事實(shí)上這個(gè)動(dòng)作必須在對方成功接收才能進(jìn)行(否則這次按鍵信息就丟失了),從這個(gè)角度來看,我們必須設(shè)計(jì)一個(gè)機(jī)制用于判斷主機(jī)是否成功接收。

I2C、CAN總線都有應(yīng)答信號,但這是這些是總線自帶的特性,我們不可能在接收到一個(gè)字節(jié)后發(fā)送一個(gè)應(yīng)答信號給主機(jī),那么是否有其他辦法呢。

人們很容易想到的一個(gè)辦法就是在主機(jī)收到正確數(shù)據(jù)后,主動(dòng)發(fā)送一幀專用數(shù)據(jù)幀用于清除這個(gè)標(biāo)志(這個(gè)幀和普通幀一樣,所以可以確保主機(jī)數(shù)據(jù)能準(zhǔn)確送達(dá)從機(jī),因?yàn)槿绻瑫r(shí)沒有送達(dá),會(huì)觸發(fā)重發(fā)機(jī)制)。這樣只要在獲取完這幀數(shù)據(jù)后,再額外的發(fā)送一幀數(shù)據(jù)用于對方確認(rèn)即可,從機(jī)接收到后,即可開始著手清除一些標(biāo)志位。

但這樣會(huì)有一個(gè)問題,因?yàn)檫@種特殊的需要從機(jī)確認(rèn)的數(shù)據(jù)包(其他類型數(shù)據(jù)不需要確認(rèn)是因?yàn)槿绻鳈C(jī)沒有正確收到數(shù)據(jù)還可以繼續(xù)獲取,獲取的數(shù)據(jù)是一樣并沒有關(guān)系,但這種需要從機(jī)確認(rèn),一旦從機(jī)認(rèn)為發(fā)送成功了,數(shù)據(jù)就被清除了這種情況就需要確認(rèn),典型的就是按鍵信息了),我們需要額外處理并占用發(fā)送帶寬。這是魚鷹不愿忍受的。

那么是否有更好的辦法?

或許我們可以從 USB 協(xié)議中獲得啟發(fā)(這是在寫這篇筆記的時(shí)候想到的,當(dāng)時(shí)寫按鍵板代碼的時(shí)候沒有想到過,但因?yàn)楫?dāng)時(shí)測試時(shí)傳輸成功率100%,所以就放棄處理這種情況了)。

USB協(xié)議是典型的主從機(jī)制,主機(jī)不主動(dòng)獲取數(shù)據(jù),從機(jī)是無法主動(dòng)發(fā)送數(shù)據(jù)的。那么從機(jī)是如何確定對方成功接收數(shù)據(jù)了呢?

一個(gè)bit的翻轉(zhuǎn)位。

每當(dāng)主機(jī)成功發(fā)送一幀數(shù)據(jù)后再發(fā)送下一幀數(shù)據(jù)時(shí),就會(huì)翻轉(zhuǎn)這個(gè)位,從機(jī)就可以根據(jù)這個(gè)位判斷主機(jī)是屬于重發(fā)數(shù)據(jù)(重發(fā)數(shù)據(jù)表示主機(jī)接收失敗了)還是新數(shù)據(jù)了,這樣從機(jī)就能從下一幀數(shù)據(jù)確定上一幀數(shù)據(jù)是否成功發(fā)送了。

而主機(jī)發(fā)送的數(shù)據(jù)是由從機(jī)發(fā)送應(yīng)答包確定的,和上面的串口協(xié)議類似,所以這個(gè)方向的數(shù)據(jù)是沒有問題的。

那么我們該如何重新設(shè)計(jì)這個(gè)協(xié)議呢?可以盡可能的不改變原來協(xié)議的情況下實(shí)現(xiàn)嗎?

或許可以從功能字出發(fā)。

為了保證對功能字的原有定義保持基本不變,使用最高 bit 作為這個(gè)特殊的位,這個(gè) bit 開始是 0,之后主機(jī)每接收一個(gè)從機(jī)應(yīng)答數(shù)據(jù)就進(jìn)行翻轉(zhuǎn),如果因?yàn)闆]有接收到從機(jī)的應(yīng)答數(shù)據(jù),就會(huì)使用相同的翻轉(zhuǎn)位重復(fù)發(fā)送;而從機(jī)也根據(jù)這個(gè)bit來確定自己的上一幀數(shù)據(jù)對方是否接收(對比上一幀數(shù)據(jù)的翻轉(zhuǎn)位),如果主機(jī)沒有成功接收,就不清除標(biāo)志位(之后主機(jī)會(huì)重發(fā)數(shù)據(jù)再次獲?。?,否則清除標(biāo)志位,。

因?yàn)槭囚~鷹剛想到的,就不多說了,僅提供一個(gè)思路。

現(xiàn)在回到指針那塊的問題。

現(xiàn)在已經(jīng)知道,如果你在計(jì)算校驗(yàn)和與發(fā)送的過程中出現(xiàn)源數(shù)據(jù)改變的情況,就會(huì)導(dǎo)致數(shù)據(jù)幀校驗(yàn)失敗,那么有什么解決辦法?

如果說你堅(jiān)持使用查詢方式發(fā)送來節(jié)省部分空間,那么只要在計(jì)算校驗(yàn)值之前拷貝一份源數(shù)據(jù),然后用這份數(shù)據(jù)計(jì)算并發(fā)送即可。

另一種方法就是,直接把整幀數(shù)據(jù)拷貝到一個(gè)數(shù)據(jù)緩存中,使用DMA發(fā)送。

現(xiàn)在還有一個(gè)問題,那就是如果我想發(fā)送一個(gè)數(shù)據(jù)域?yàn)榭盏臄?shù)據(jù)該怎么發(fā)送?

一般來說,在使用指針的時(shí)候,不會(huì)使用 NULL 空指針,但是在數(shù)據(jù)為空的情況下,就需要使用 NULL 指針了,并長度設(shè)置為 0,這個(gè)時(shí)候在檢查指針的時(shí)候,不能看到空指針就退出函數(shù),還要判斷長度信息,當(dāng)長度為0時(shí)在打包時(shí)就不拷貝源數(shù)據(jù),但最終還是要發(fā)送數(shù)據(jù)幀的(當(dāng)時(shí)別人寫的代碼將指針和長度判斷同時(shí)放到了 for 循環(huán)的條件里面,魚鷹覺得效率太低,導(dǎo)致修改代碼是沒考慮指針為空的情況,所以導(dǎo)致了一個(gè)小bug)。

互斥鎖釋放順序

現(xiàn)在考慮第二個(gè)問題:互斥鎖釋放順序問題。

如果沒有采用隊(duì)列方式接收數(shù)據(jù),而是主機(jī)發(fā)送完成后等待接收從機(jī)數(shù)據(jù)后再發(fā)送下一幀數(shù)據(jù),那么該如何處理互斥鎖的問題?

我們知道為了保證數(shù)據(jù)的同步,保證在接收到一幀數(shù)據(jù)進(jìn)行處理時(shí),不能被新的數(shù)據(jù)幀沖掉,這時(shí)就要加入一個(gè)互斥鎖,表示我正在處理數(shù)據(jù),下面的數(shù)據(jù)我接收不了,這樣就能保證你正在處理的數(shù)據(jù)不會(huì)被新來的數(shù)據(jù)修改掉,從而進(jìn)行正確的處理。

那么這個(gè)標(biāo)志位(互斥鎖)該什么時(shí)候清掉(釋放掉)呢?

一般來說標(biāo)志位,一般越早清掉越好,比如外部中斷標(biāo)志位,進(jìn)入中斷后,一般首先會(huì)清理標(biāo)志位,這樣即使你正在處理本次中斷的程序,那么即使這時(shí)再來了中斷,也不會(huì)丟失中斷信息(有懸起標(biāo)志位),這樣就可以在處理完這次中斷后,立馬進(jìn)行下一次中斷的處理了(前提是優(yōu)先級足夠高)。

但是如果你清理太早或者清理太晚會(huì)怎樣?

比如說你在接收到一幀數(shù)據(jù)后(數(shù)據(jù)幀所有檢查完成),開始設(shè)置標(biāo)志位,當(dāng)主程序查詢到這個(gè)標(biāo)志時(shí)(一般數(shù)據(jù)處理不會(huì)放在中斷中),如果馬上開始清除這個(gè)標(biāo)志……嗯,一般來說不會(huì)有問題。

那么什么時(shí)候會(huì)出現(xiàn)問題?當(dāng)你的主程序查詢到這這個(gè)標(biāo)志時(shí)開始清除標(biāo)志,然后處理、返回?cái)?shù)據(jù)給主機(jī),如果此時(shí)主機(jī)超時(shí)重發(fā)數(shù)據(jù)時(shí),,因?yàn)檫@個(gè)時(shí)候你雖然在處理數(shù)據(jù),但是因?yàn)槟愕臉?biāo)志位已經(jīng)被清除了,所以接收程序就會(huì)開始往接收緩存區(qū)存數(shù)據(jù)了,當(dāng)你存完之后再回到數(shù)據(jù)處理那里,你的緩存區(qū)可能就不是你想要的數(shù)據(jù)了。

可能你會(huì)說,既然是重發(fā),那么數(shù)據(jù)應(yīng)該是相同的吧?好吧,你贏了,魚鷹編不下去了,這種情況(有重發(fā)機(jī)制)下清理太早好像是不會(huì)出現(xiàn)問題,但你怎么知道對方是采用副本進(jìn)行重發(fā)數(shù)據(jù)的呢,如果重發(fā)時(shí)它又從源數(shù)據(jù)中拷貝后再進(jìn)行重發(fā)會(huì)出現(xiàn)什么問題?比如時(shí)間信息,開始第一幀數(shù)據(jù)是11:59,CPU剛把11拷貝到用戶空間,被串口中斷程序打斷,導(dǎo)致下一幀接收的數(shù)據(jù)是12:00,此時(shí)回到主程序繼續(xù)拷貝,拷貝00,數(shù)據(jù)的完整性被破壞,這樣導(dǎo)致的結(jié)果就是11:00,而實(shí)際上時(shí)間是12:00,這就是你打斷數(shù)據(jù)處理過程的后果。

現(xiàn)在再說說清理太晚會(huì)怎么樣。

當(dāng)你的主程序查詢到這個(gè)標(biāo)志后,暫時(shí)不清除,而是等到從機(jī)發(fā)送完應(yīng)答數(shù)據(jù)之后再清除標(biāo)志,此時(shí)因?yàn)閺臋C(jī)采用查詢方式(查詢方式表明從機(jī)發(fā)送完最后一個(gè)字節(jié)后后開始清除標(biāo)志位,也就代表了主機(jī)就差最后一個(gè)字節(jié)沒有接收了,這樣發(fā)送和清除之間間隔時(shí)間較短,而采用 DMA 方式的話,發(fā)送和清除的間隔時(shí)間更短,因?yàn)榭赡蹹MA還沒開始發(fā)送第一幀數(shù)據(jù),清除工作就已經(jīng)完成了),或者因?yàn)槠渌颍ū热缰袛嗵幚恚?dǎo)致發(fā)送和清除之間的時(shí)間很長這種特殊情況,這樣可能主機(jī)已經(jīng)開始下發(fā)下一幀數(shù)據(jù)了,但是因?yàn)榇藭r(shí)標(biāo)志還沒有清除,不能接收數(shù)據(jù),所以主機(jī)這一幀數(shù)據(jù)就這樣丟失了。

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

那么這個(gè)清除標(biāo)志位最合適的時(shí)機(jī)是什么時(shí)候?

你鎖定資源利用完的時(shí)候。

現(xiàn)在來看看,這個(gè)互斥鎖鎖定的是什么資源?對,就是接收緩存,那么接收緩存什么時(shí)候用完?當(dāng)然是在數(shù)據(jù)處理完成之時(shí)。也就是說在數(shù)據(jù)處理完之前、發(fā)送數(shù)據(jù)之前清除最合適。

這樣就不會(huì)因?yàn)樘幚砥渌虑槎鴮?dǎo)致清除操作過晚而丟失下一幀數(shù)據(jù)了,因?yàn)榇藭r(shí)主機(jī)還沒收到從機(jī)上傳的數(shù)據(jù),也就不會(huì)馬上開始下一幀數(shù)據(jù)的傳輸了。

說到清除,你覺得,需要把整個(gè)緩沖區(qū)進(jìn)行清零操作嗎?這個(gè)問題在以往的文章解釋過,就看你是否看完了。

數(shù)據(jù)幀檢查

你是否會(huì)對接收的數(shù)據(jù)進(jìn)行檢查?如果不進(jìn)行檢查會(huì)發(fā)生什么?

我們知道一幀數(shù)據(jù)中,每個(gè)部分都有各自的含義,甚至有些部分可能在某些數(shù)據(jù)幀中不存在,比如數(shù)據(jù)域部分,我們需要根據(jù)長度信息來判斷數(shù)據(jù)域部分是否存在。

但是你能保證你所接收的數(shù)據(jù)都是準(zhǔn)確的嗎?你能確保在工作環(huán)境下不會(huì)因?yàn)楦鞣N干擾導(dǎo)致數(shù)據(jù)長度信息由0x05變成0x85(最高位翻轉(zhuǎn))嗎?,如果出現(xiàn)了會(huì)導(dǎo)致什么后果?

假設(shè)你采用RXNE中斷方式來一個(gè)、一個(gè)字節(jié)的接收數(shù)據(jù),分析如下:

因?yàn)槭菃巫止?jié)接收數(shù)據(jù),所以你需要把所有接收的數(shù)據(jù)當(dāng)成數(shù)據(jù)流,根據(jù)幀頭信息來確定幀的開始,一旦確定幀頭信息之后,你就可以根據(jù)接下來的一系列數(shù)據(jù)保證一幀數(shù)據(jù)的結(jié)束,同時(shí)開始新幀的接收……

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

初看這個(gè)接收流程沒有問題,但是真的如此嗎?

但是就像前面所說,你能保證你的數(shù)據(jù)沒有問題嗎?如果說你接收到一個(gè)長度信息,本來是0x05,但是最終接收的數(shù)據(jù)是0x85,這就意味著你接下來的數(shù)據(jù)域的長度是0x85,根據(jù)你的接收流程,你需要再接收0x85個(gè)字節(jié)之后,才能判斷校驗(yàn)字節(jié)是否正確。

可能你會(huì)說,雖然你的長度信息由0x05變成了0x85,之后接收校驗(yàn)過程肯定是失敗的,那么這幀數(shù)據(jù)就會(huì)被接收程序丟棄,從而導(dǎo)致接收程序進(jìn)入重新尋找?guī)^的流程,這個(gè)過程不是挺正常的嗎?按理說上述異常情況是能被接收流程處理掉的。

那么首先確認(rèn)一點(diǎn),上述異常能被接收流程處理嗎?

答案是能!

既然上述異常是能被接收狀態(tài)機(jī)處理的,那么還會(huì)有什么問題?

問題就出在這個(gè)錯(cuò)誤數(shù)據(jù)本身!

因?yàn)槟闶歉鶕?jù)錯(cuò)誤數(shù)據(jù)來決定接下來需要接收多少數(shù)據(jù),而一般來說,接收緩存大小設(shè)置為最大幀的長度,那么就出現(xiàn)一個(gè)問題,你的緩存夠你接收0x85個(gè)字節(jié)嗎?

如果說你開辟的接收緩存空間很大,足夠接收這么多數(shù)據(jù),那么就算遇上以上情況,也是沒有任何問題,但是萬一你比較節(jié)省資源,緩存不夠大會(huì)出現(xiàn)什么情況?

這就涉及到內(nèi)存分配問題:

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

你的串口緩存一般在 Data區(qū)域,一旦你接收的數(shù)據(jù)超出了你開辟的空間,那么必然導(dǎo)致緩存空間溢出!

那么緩存空間溢出會(huì)導(dǎo)致什么危害?

我們通過上圖可以知道,一旦緩存溢出,必然導(dǎo)致該緩存周圍的數(shù)據(jù)出現(xiàn)異常(數(shù)據(jù)被篡改),如果你的其它代碼剛好需要這個(gè)數(shù)據(jù)作為重要參考,而你在使用的時(shí)候又沒有對這個(gè)數(shù)據(jù)的有效性進(jìn)行檢查,那么可能導(dǎo)致另一個(gè)災(zāi)難性后果,而這個(gè)后果又導(dǎo)致了其他后果,從而導(dǎo)致雪崩效應(yīng)。

而你修復(fù)這個(gè)bug時(shí),你以為修復(fù)了,但你只修復(fù)了表面,真正內(nèi)在bug還存在!

所以,千萬別太相信內(nèi)存中的數(shù)據(jù),每一個(gè)數(shù)據(jù)的輸入都要進(jìn)行嚴(yán)格檢查,這個(gè)數(shù)據(jù)可以錯(cuò)誤,但是不能導(dǎo)致程序崩潰!

所以千萬別寫能篡改別人數(shù)據(jù)的代碼,這是很危險(xiǎn)的事情,也是很難解決的bug,因?yàn)槟悴恢浪鼤?huì)在什么時(shí)候篡改哪里的數(shù)據(jù)!

再假如你的接收緩存放在棧中了呢(稍微有C語言常識的程序員都不會(huì)把串口接收緩存放在棧中,但魚鷹偏偏遇到過這種代碼,而為了解決這個(gè)bug整整花了一星期,這還是在bug復(fù)現(xiàn)率高的情況下)?

根據(jù)前面的圖可知,棧一般存放在高地址,并且一般棧生長方向?yàn)橄虻偷刂飞L。如果出現(xiàn)上述情況(接收的數(shù)據(jù)大于開辟的棧緩存空間)會(huì)發(fā)生什么?

棧幀被破壞!

灰色部分因?yàn)榻邮盏臄?shù)據(jù)太多,導(dǎo)致原本存在的棧數(shù)據(jù)被串口的接收的數(shù)據(jù)修改了(注意篡改的數(shù)據(jù)可能不是連續(xù)的,因?yàn)槊恳淮芜M(jìn)入時(shí),開辟的那部分??臻g可能都不在同一個(gè)地址),假如這個(gè)數(shù)據(jù)是保存返回寄存器LR的,那么必然導(dǎo)致返回錯(cuò)誤,極可能觸發(fā)HardFault中斷!

那么有什么辦法解決棧被破壞的問題?

最有效的方式魚鷹覺得是使用ITM,如果無法在線調(diào)試,可以嘗試DMA循環(huán)傳輸PC指針值(但是如何得到這個(gè)值?畢竟這個(gè)寄存器本身是沒有地址概念的)到一塊內(nèi)存中,這樣就可以得到最后正常執(zhí)行的代碼地址,從而定位錯(cuò)誤代碼的位置。

如果單片機(jī)不支持這些功能呢?魚鷹現(xiàn)有的知識體系好像無法解決,只能佛系調(diào)bug了(看和bug之間的緣分),囧。

前面說了由于外部工作環(huán)境導(dǎo)致數(shù)據(jù)長度信息錯(cuò)誤從而出現(xiàn)數(shù)組溢出這種情況,如果說你保證工作環(huán)境非常好,不可能出現(xiàn)這種干擾,是否還會(huì)出現(xiàn)問題?

當(dāng)然會(huì)!

前面分析了外在原因,現(xiàn)在分析內(nèi)在原因,你的接收程序能保證及時(shí)接收發(fā)送端發(fā)送過來的數(shù)據(jù)嗎?如果不及時(shí)接收數(shù)據(jù)會(huì)出現(xiàn)什么問題?

我們知道,一個(gè)系統(tǒng)一般都有很多中斷需要處理,如果說你的接收程序的中斷優(yōu)先級不是最高的,那么很可能出現(xiàn)接收程序無法及時(shí)接收的情況,即RXNE中斷來臨時(shí),因更高優(yōu)先級中斷需要處理,而且處理時(shí)間較長,那么就會(huì)出現(xiàn)當(dāng)前接收的字節(jié)因?yàn)闆]有接收完成而被后續(xù)的數(shù)據(jù)沖掉,即出現(xiàn)ORE(溢出錯(cuò)誤)。

這樣會(huì)導(dǎo)致什么問題?

數(shù)據(jù)域信息(也可能是校驗(yàn)值等數(shù)據(jù))當(dāng)成了長度信息(為什么只討論長度,而不討論功能字之類的數(shù)據(jù),難道他們不會(huì)出現(xiàn)ORE的情況嗎?),這樣一來,如果這個(gè)數(shù)據(jù)很大,接收程序就會(huì)以為接下來還需要接收很多數(shù)據(jù)才能完成一幀的接收,導(dǎo)致后果和前面分析的數(shù)據(jù)干擾一樣嚴(yán)重。

那么采用RXNE接收方式時(shí)該怎么解決這種問題?

檢查長度信息的合理性,只要長度信息不會(huì)導(dǎo)致緩存溢出即可。

但是上面的解決方案會(huì)導(dǎo)致什么問題?

因?yàn)槟愕某绦蛟O(shè)計(jì)問題(采用RXNE接收導(dǎo)致不能及時(shí)處理),使得原本能接收的數(shù)據(jù)無法及時(shí)處理(DMA可以及時(shí)處理),最終使得當(dāng)前這一幀數(shù)據(jù)無法正常接收(如果錯(cuò)誤的長度信息夠大的話,還有可能接下來很多幀數(shù)據(jù)都無法接收),這你能接受嗎?

但是采用DMA為什么就不會(huì)有上述問題,除了DMA能自動(dòng)接收數(shù)據(jù)提高效率之外,還有一點(diǎn)就是它不根據(jù)接收的數(shù)據(jù)來判斷接下來還需要接收多少數(shù)據(jù),而是根據(jù)設(shè)定的接收數(shù)據(jù)長度來接收的(如果加入IDLE中斷,可以提前結(jié)束接收工作),這就避免了上述的緩存溢出和接收不及時(shí)問題。

最后再分析上述接收的另一個(gè)問題,那就是一幀數(shù)據(jù)中可能出現(xiàn)沒有數(shù)據(jù)域的情況,這種情況該怎么處理?

只要根據(jù)長度信息分開處理即可。如果不對沒有數(shù)據(jù)域的情況分開處理,那么你接收的下一個(gè)數(shù)據(jù)直接就是校驗(yàn)值,而你的接收流程卻認(rèn)為這是數(shù)據(jù)域的數(shù)據(jù),必然導(dǎo)致校驗(yàn)失敗。

現(xiàn)在總結(jié)使用RXNE方式接收的幾個(gè)問題:

1、緩存溢出。

緩存溢出有兩種可能,第一種就是環(huán)境干擾導(dǎo)致長度信息出錯(cuò),從而出現(xiàn)緩存溢出情況;第二種情況就是因?yàn)榻邮詹患皶r(shí),導(dǎo)致數(shù)據(jù)錯(cuò)位,如果剛好是長度信息錯(cuò)誤,并且這個(gè)長度信息太大,而你的代碼未對長度進(jìn)行檢查,那么也會(huì)出現(xiàn)緩存溢出bug,而這種bug一旦出現(xiàn),很難發(fā)現(xiàn)。所以在代碼中對數(shù)據(jù)的合理性檢查是非常有必要的一件事。

2、中斷及時(shí)處理。

如果中斷不及時(shí)處理,會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)位,輕則丟失至少一幀數(shù)據(jù),重則緩存溢出!

3、狀態(tài)機(jī)是否需要接收數(shù)據(jù)部分。

由于數(shù)據(jù)幀有可能沒有數(shù)據(jù)域的情況,所以必須區(qū)別處理,保證代碼接收的準(zhǔn)確性,否則有可能把校驗(yàn)值當(dāng)成數(shù)據(jù)了,這樣必然無法通過校驗(yàn),這一幀數(shù)據(jù)必然會(huì)丟失!

串口空閑

前面一直提到串口空閑,也大概明白串口的作用,但是一些細(xì)節(jié)問題還是需要好好說一下的。

第一個(gè)問題,如何清除串口空閑中斷標(biāo)志位?

很多人會(huì)使用USART_ClearFlag標(biāo)準(zhǔn)庫函數(shù)進(jìn)行清除,但是當(dāng)你跳轉(zhuǎn)到該函數(shù)原型時(shí),你會(huì)看到如下說明:

什么樣的幀頭是足夠特殊的數(shù)據(jù)呢?

你會(huì)看到很多標(biāo)志位是無法通過該函數(shù)清除的。

那么該如何清除IDLE標(biāo)志呢?其實(shí)上面的注釋已經(jīng)進(jìn)行了說明。

PE、FE、NE、ORE、IDLE標(biāo)志位的清除是通過一個(gè)軟件序列進(jìn)行清除的:首先通過USART_GetFlagStatus讀取USART_SR寄存器的值,然后通過USART_ReceiveData函數(shù)讀取USART_DR的值即可。

那么這里就有一個(gè)問題,是否這些標(biāo)志問題的清除都要單獨(dú)編寫清除序列呢?

答案是否定的。

因?yàn)檫@些標(biāo)志位都是由同一種序列進(jìn)行清除的,所以只要一個(gè)清除序列就會(huì)把所有的標(biāo)志位都進(jìn)行清除了(同樣一旦執(zhí)行了這個(gè)序列,也就意味著你無法再通過USART_SR寄存器獲得標(biāo)志位了)。

為了保證獲取標(biāo)志位,我們可以在清除序列之前把USART_SR寄存器的值保存到副本中,然后再讀取USART_DR寄存器的值保存到副本來實(shí)現(xiàn)清除功能,注意該序列應(yīng)該無條件執(zhí)行(不在某個(gè)判斷語句中)。這樣后續(xù)我們就可以使用這個(gè) USART_SR 的副本判斷哪一個(gè)標(biāo)志置位了,同樣也可以使用 USART_DR 的副本獲取串口數(shù)據(jù),而為了實(shí)現(xiàn)以上效果,USART_GetFlagStatus這個(gè)函數(shù)就不合適了,只能直接操作寄存器去實(shí)現(xiàn)。

第二個(gè)問題,在線調(diào)試時(shí)對空閑中斷會(huì)有影響嗎?

我們知道,KEIL能夠?qū)⒁粋€(gè)結(jié)構(gòu)體的數(shù)據(jù)全部讀取出來,而庫函數(shù)將串口模塊的所有寄存器都封裝在一個(gè)結(jié)構(gòu)體中,這樣就會(huì)出現(xiàn)一個(gè)問題,如果你的窗口是實(shí)時(shí)刷新的,當(dāng)你使用KEIL讀取串口模塊寄存器的時(shí)候(不管是使用peripheral窗口還是Watch窗口),就會(huì)出現(xiàn)先讀取SR再讀取DR的情況, 這樣就有可能出現(xiàn)KEIL和單片機(jī)CPU讀取這兩個(gè)寄存器沖突的情況。

如果全速運(yùn)行時(shí),KEIL 先執(zhí)行了這個(gè)序列(通過調(diào)試器讀取這兩個(gè)寄存器的值),單片機(jī)CPU再讀取SR寄存值,必然是無法讀取到正確標(biāo)志位的,因?yàn)檫@些標(biāo)志位已經(jīng)被KEIL的讀取序列清除了(這個(gè)情況魚鷹確實(shí)碰到過,當(dāng)時(shí)明明下發(fā)了數(shù)據(jù),但是單片機(jī)無法獲取標(biāo)志位),所以在調(diào)試串口時(shí),注意不要讓 KEIL 去讀取這些寄存器(即關(guān)閉這些窗口,只有在必須的情況下才開啟),防止出現(xiàn)莫名其妙的情況。

第三個(gè)問題,空閑中斷能準(zhǔn)確觸發(fā)嗎?

如果從接收端考慮的話,如果觸發(fā)了空閑中斷,那么必然滿足了條件才觸發(fā)的,而不是意外觸發(fā)的(嗯,我們要相信STM32),但從發(fā)送端考慮的話,有可能出現(xiàn)一幀數(shù)據(jù)斷續(xù)發(fā)送,導(dǎo)致一幀數(shù)據(jù)觸發(fā)多次空閑中斷,所以如果是簡單的DMA+空閑中斷方式接收是很有問題的(空閑出現(xiàn)就認(rèn)為一幀結(jié)束了,就會(huì)把一幀數(shù)據(jù)當(dāng)成兩幀處理,這樣肯定無法通過數(shù)據(jù)檢查的)。

那么先來分析為什么會(huì)出現(xiàn)一幀數(shù)據(jù)多次觸發(fā)空閑中斷情況。我們知道linux、windows系統(tǒng)并不是實(shí)時(shí)系統(tǒng),當(dāng)應(yīng)用程序需要發(fā)送一幀數(shù)據(jù)時(shí),可能并沒有連續(xù)發(fā)送,而是發(fā)送完一個(gè)字節(jié)后去處理其他事情后才發(fā)送下一個(gè)字節(jié),這樣一來,如果耽誤的時(shí)間夠長,就會(huì)觸發(fā)串口的空閑中斷,從而一幀數(shù)據(jù)當(dāng)成兩幀處理了。

那有什么方法可以解決呢?魚鷹提供兩種解決思路。

第一種,使用兩個(gè)緩存空間,一個(gè)緩存空間專門用于接收串口數(shù)據(jù),將接收到的數(shù)據(jù)存放到另一個(gè)緩存,這個(gè)緩存采用字節(jié)隊(duì)列的方式進(jìn)行管理,應(yīng)用程序從緩存隊(duì)列中一個(gè)字節(jié)一個(gè)字節(jié)的取出數(shù)據(jù)進(jìn)行處理(注意檢查數(shù)據(jù)有效性),這樣就能保證及時(shí)處理。但是因?yàn)榭臻e中斷不再可靠,所以空閑中斷不再作為判斷一幀數(shù)據(jù)結(jié)束的依據(jù)(根據(jù)長度信息判斷),而是只在空閑中斷中將已接收數(shù)據(jù)復(fù)制到字節(jié)隊(duì)列緩存中,這樣就可以處理意外的空閑中斷。

第二種,還是一個(gè)緩存空間,還是DMA+空閑方式處理,但是需要增加額外的條件。就是當(dāng)進(jìn)入空閑中斷后,不再直接處理,而是獲取當(dāng)前接收時(shí)刻,然后在處理數(shù)據(jù)的時(shí)候根據(jù)這個(gè)時(shí)刻來判斷是否達(dá)到足夠的空閑時(shí)間,只有在進(jìn)入空閑中斷后并達(dá)到一定延時(shí)之后才認(rèn)為一幀數(shù)據(jù)結(jié)束了,這樣可以避免一些非常短的空閑時(shí)間(魚鷹公眾號提供過的代碼使用這種方式)。

以上問題是就是魚鷹以前使用空閑中斷從未考慮的問題,魚鷹并不知道使用空閑中斷還可能出現(xiàn)誤觸發(fā)的情況,但是既然知道了,就要想辦法解決。但是為什么以前使用空閑中斷時(shí)沒有出現(xiàn)通信問題呢?

事實(shí)上不是沒有問題,而是有可能把分散的一幀數(shù)據(jù)的兩部分直接丟棄了而已,因?yàn)橛兄匕l(fā)機(jī)制,所以即使丟棄一幀數(shù)據(jù),也能通信正常,而且這種一幀數(shù)據(jù)分散成兩部分的概率還是挺低的,ubuntu(linux系統(tǒng))下大概千分之三左右的樣子。

第四個(gè)問題,如果單片機(jī)沒有空閑中斷又該如何做?

當(dāng)我們使用 RXNE 的同時(shí)其實(shí)我們也可以使用空閑中斷,這樣也能確定一幀數(shù)據(jù)的結(jié)束(但是要注意前面的誤觸發(fā)問題)。但是如果有些低端單片機(jī)(如 51 )沒有空閑中斷又該怎么辦?

其實(shí)我們可以從 stm32 的空閑中斷得到相應(yīng)的啟發(fā)。

所謂空閑中斷,就是當(dāng)串口接收到數(shù)據(jù)后,在應(yīng)該接收數(shù)據(jù)的時(shí)刻,發(fā)送方并沒有發(fā)送數(shù)據(jù),所以串口模塊置位空閑標(biāo)志位,從而引起空閑中斷。

那么我們是否可以軟件模擬串口模塊的這個(gè)功能,從而確定一幀數(shù)據(jù)的結(jié)束呢?

答案是肯定的(前提是每一幀數(shù)據(jù)之間有空閑時(shí)間)。

我們可以使用一個(gè)定時(shí)器,定時(shí)器向上計(jì)數(shù)。當(dāng)接收到一個(gè)字節(jié)數(shù)據(jù)后,初始化計(jì)數(shù)器并啟動(dòng)定時(shí)器,這樣一旦有一段時(shí)間沒有接收到串口數(shù)據(jù)(也就不再初始化計(jì)數(shù)器),那么定時(shí)器溢出,進(jìn)入溢出中斷,而這個(gè)溢出中斷就類似于串口的空閑中斷(在溢出中斷中關(guān)閉定時(shí)器以達(dá)到清除空閑中斷標(biāo)志的作用),這樣就達(dá)到了串口空閑中斷的效果(和前面問題的第二種解決方案類似)。

通信吞吐量

在以上分析過程中,都是采用主機(jī)發(fā)送,從機(jī)接收后再回復(fù)主機(jī)的方式進(jìn)行通信,雖然通信正常,但實(shí)際上效率比較低下,單位時(shí)間傳輸?shù)臄?shù)據(jù)量較少,如下圖所示:

紅色部分就是必要的空閑時(shí)間,可以看到左右兩張圖的通信頻率是有差異的,右圖中從機(jī)必須等待前一幀數(shù)據(jù)發(fā)送完畢才能處理數(shù)據(jù),而左圖可以在接收當(dāng)前幀時(shí)處理上一幀數(shù)據(jù),類似CPU的指令執(zhí)行流水線。
責(zé)任編輯:pj

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 設(shè)計(jì)功能
    +關(guān)注

    關(guān)注

    0

    文章

    2

    瀏覽量

    6138
  • 幀格式
    +關(guān)注

    關(guān)注

    0

    文章

    7

    瀏覽量

    5472
收藏 人收藏

    評論

    相關(guān)推薦

    代碼整潔之道-大師眼中的整潔代碼是什么樣

    幾個(gè)月前寫了一篇文章“如何寫出難以維護(hù)的代碼”,從中能大概了解到不好維護(hù)的代碼是什么樣,有哪些壞味道,那肯定有人會(huì)反問,難以維護(hù)的代碼見的太多了,也知道長什么樣,但是對于好維護(hù)的代碼是什么樣的比較
    的頭像 發(fā)表于 09-09 16:30 ?136次閱讀
    代碼整潔之道-大師眼中的整潔代碼是<b class='flag-5'>什么樣</b>

    CAN數(shù)據(jù)的各個(gè)域及其作用

    CAN數(shù)據(jù)由多個(gè)域組成,包括起始、仲裁域、控制域、數(shù)據(jù)域和結(jié)束。每個(gè)域都有其特定的作用,共同構(gòu)成了一個(gè)完整的
    的頭像 發(fā)表于 07-24 15:10 ?434次閱讀

    快充充電哪個(gè)品牌好?什么樣的充電用著“香”?

    在探討快充充電哪個(gè)品牌好,以及什么樣的充電使用起來更令人滿意的議題時(shí),我們不僅要考慮充電速度,還要關(guān)注充電的散熱性能、兼容性和便攜性。 充電
    的頭像 發(fā)表于 05-20 17:27 ?682次閱讀

    基于HTTP/3構(gòu)建SSH協(xié)議會(huì)是什么樣?

    來自UCLouvain的Fran?ois Michel 和Olivier Bonaventure在研究中思考了一個(gè)問題:如果使用最新的網(wǎng)絡(luò)技術(shù)來重新設(shè)計(jì)SSH協(xié)議,那新協(xié)議會(huì)是什么樣
    的頭像 發(fā)表于 02-20 17:07 ?506次閱讀
    基于HTTP/3構(gòu)建SSH協(xié)議會(huì)是<b class='flag-5'>什么樣</b><b class='flag-5'>呢</b>?

    請問用CAN存儲(chǔ)器存儲(chǔ)的CAN總線數(shù)據(jù)格式有哪些?

    請問用CAN存儲(chǔ)器存儲(chǔ)的CAN總線數(shù)據(jù)格式有哪些? CAN總線是一種常用于汽車、工業(yè)控制和其他應(yīng)用領(lǐng)域的串行通信協(xié)議。在CAN總線中,數(shù)據(jù)通過數(shù)據(jù)
    的頭像 發(fā)表于 01-31 13:46 ?990次閱讀

    工業(yè)控制通信協(xié)議的報(bào)文分別是什么樣的?

    那樣先存儲(chǔ)起來,不知道我對同步異步通信的理解是否正確? 2、這些協(xié)議下的通信設(shè)備發(fā)送數(shù)據(jù)的格式多種多樣,請問每種協(xié)議的通信發(fā)送的結(jié)構(gòu)是怎么的,想總結(jié)一下這些通信協(xié)議的發(fā)送類型結(jié)構(gòu)
    發(fā)表于 01-19 14:46

    什么樣的電子元件才是車規(guī)級的器件?車規(guī)級芯片到底有哪些要求?

    汽車電子產(chǎn)品的價(jià)格普遍比較貴,其中的主要原因之一就是使用了車規(guī)級的電子元件,但什么樣的電子元件才是車規(guī)級的器件?我們先來看看電子元件在汽車上的應(yīng)用和一般的消費(fèi)電子在應(yīng)用有什么差異。
    的頭像 發(fā)表于 01-13 16:24 ?2065次閱讀
    <b class='flag-5'>什么樣</b>的電子元件才是車規(guī)級的器件<b class='flag-5'>呢</b>?車規(guī)級芯片到底有哪些要求?

    請問AD2S1200的串行數(shù)據(jù)輸出波形及處理的角度波形是什么樣的?

    AD2S1200的串行數(shù)據(jù)輸出波形及處理的角度波形是什么樣的?
    發(fā)表于 12-20 06:07

    變頻器一般是控制什么樣的電機(jī),是不是需要特殊的電機(jī)才能與之匹配?

    變頻器一般是控制什么樣的電機(jī),是不是需要特殊的電機(jī)才能與之匹配?普通的電機(jī)行不?直流電機(jī)行不?是不是非得是某種類型的電機(jī)?這個(gè)東東還沒有過!請教!
    發(fā)表于 12-19 06:57

    什么樣的變壓器可以做升壓器

    什么樣的變壓器可以做升壓器? 變壓器可以通過改變輸入電壓和輸出電壓的繞組匝數(shù)比來實(shí)現(xiàn)升壓操作。根據(jù)變壓器的類型和設(shè)計(jì),有多種變壓器可以用作升壓器。下面將詳細(xì)介紹幾種常見的變壓器類型及其應(yīng)用。 1.
    的頭像 發(fā)表于 11-20 14:44 ?814次閱讀

    環(huán)路增益是什么?什么樣的環(huán)路增益特性是理想的

    環(huán)路增益是什么?什么樣的環(huán)路增益特性是理想的? 環(huán)路增益(loop gain)是同相反饋系統(tǒng)中的一個(gè)重要概念,用于描述反饋回路的閉環(huán)增益。在電子電路和控制系統(tǒng)中,環(huán)路增益對系統(tǒng)的穩(wěn)定性、幅頻特性
    的頭像 發(fā)表于 11-08 17:46 ?2783次閱讀

    倒裝芯片封裝選擇什么樣的錫膏?

    介紹倒裝芯片封裝選擇什么樣的錫膏?
    的頭像 發(fā)表于 10-31 13:16 ?695次閱讀
    倒裝芯片封裝選擇<b class='flag-5'>什么樣</b>的錫膏?

    環(huán)路增益是什么?什么樣的環(huán)路增益特性是理想的

    環(huán)路增益是什么?什么樣的環(huán)路增益特性是理想的? 環(huán)路增益是指信號在一個(gè)系統(tǒng)中形成閉環(huán)后,從輸入到輸出經(jīng)過一圈的增益。具體來說,環(huán)路增益是指信號在經(jīng)過系統(tǒng)的反饋回路后,被加強(qiáng)了多少倍。這個(gè)概念
    的頭像 發(fā)表于 10-25 11:39 ?2142次閱讀

    單片機(jī)接收數(shù)據(jù)尾校驗(yàn)數(shù)據(jù)解析

    單片機(jī)接收數(shù)據(jù)尾校驗(yàn)數(shù)據(jù)解析
    的頭像 發(fā)表于 10-17 17:23 ?1210次閱讀
    單片機(jī)接收<b class='flag-5'>數(shù)據(jù)</b><b class='flag-5'>幀</b><b class='flag-5'>幀</b><b class='flag-5'>頭</b><b class='flag-5'>幀</b>尾校驗(yàn)<b class='flag-5'>數(shù)據(jù)</b>解析

    高壓開關(guān)長什么樣?有什么種類?

    一、高壓開關(guān)長什么樣?有什么種類? 二.高壓開關(guān)有什么功能? 三、為什么斷路器是高壓開關(guān)中的No.1?
    發(fā)表于 10-13 10:29 ?538次閱讀
    高壓開關(guān)長<b class='flag-5'>什么樣</b>?有什么種類?