串口接收中斷斷幀是很重要的,那么該如何判斷一幀數據是否接受完成呢?常用的兩種方法是:
時間斷幀,我們可以通過在定時器里對兩幀數據的時間進行比較,一幀數據里邊兩個數據的時間差是很短的,如果超過一定時間之后仍舊無心的數據被接收到,那么我們就可以判斷為一幀數據結束。
協議斷幀,這就好比我么兩個人約定好數據大致長度,或某個字符出現即為一幀數據的結束,由此可以來進行斷幀,判斷為一幀數據的接收完成。
實現串口數據幀斷幀,有很多的方法,比如使用串口的IDLE中斷進行斷幀,使用定時器根據時間斷幀、使用特殊標識符進行斷幀等等,剛開始工作的時候,我自己就寫好幾個版本,基本上一個項目一個版本,但是現在我使用的基本上只有這一個版本。
空閑中斷斷幀
使用串口的IDLE中斷:比如STM32有串口的IDLE中斷,在數據發送完成,串口進入空閑的時候會產生一個IDLE中斷,在HAL中也使用到這種方式。但是,其他的單片機不一定有IDLE中斷,比如51單片機、MSP430等等,所以這個方式并不是萬能的,.方法二:使用特殊字符:給數據包的幀頭和幀尾使用一個或者一組特定的字符,也就是我們說的幀頭和幀尾。這個有一定的概率會造成數據包錯位或者數據包內容和幀頭尾內容一樣。如果數據幀幀尾丟失,著會這造成單片機一直等待數據幀幀尾,進入死等情況。方法三:使用串口+定時的方式進行斷幀。定時器主要用來計時數據幀與幀之間的時間,這要這個時間大于一個設定的閾值,超出這個時間就表示當前數據幀發送完畢,下一次來的數據,一定是一個新的數據幀。本次主要對方法三做一個說明。這也是我在項目中常用一種方式,這種方式可以適用于串口、RS485、RS422等通訊接口,其帶有循環緩存隊列的思想用在CAN總線通訊中也十分合適(CAN通訊不需要自己去判斷報文的幀頭幀尾),這樣的通訊設置方式能夠極大的降低數據掉包率。
使用資源串口 1個定時器 1個
實現基本原理采用循環隊列,將串口的數據流進行接收,根據定時器計時單個BYTE之間的時間,只要這個時間超過一個閾值,則認為下一個數據為新的數據幀開頭。(使用循環緩存隊列進行數據接收、采用定時器進行數據斷幀),數據接收采用的是單字節中斷接收方式。在查串口的中斷函數中,只進行數據接收,不對數據進行處理。在主函數中,才對數據幀的合法性進行判斷和數據幀內容進行處理和應答。
特點采用循環緩存數據隊列,有效的降低數據掉包風險適用于串行通訊要求發送數據必須連續,且幀與幀之間需要有一定的時間間隔。數據幀可以實現任意長度采用生產消費者模型的設計思路
下面我就舉個自己設計的項目例子吧。本次使用STM32F103RCT6單片機,當然我也將這種方式用到過MSP430、8051、NRF24LE1等單片機中。
數據幀規定一個完整的數據包由開始字符、設備類型碼、設備地址碼、命令字、數據長度、數據區、CRC校驗及結束字符組成。如下所示:
各區域含義如下:開始字符:1字節,表示一個數據包的開始。固定為0xAA。設備類型:1字節,表示設備類型,固定為0x01。所有設備都能接收廣播類型(0xFF)的命令請求設備地址:1字節,表示設備在系統中的地址,由用戶可以自行定義,默認值為0x01。所有設備都能接收廣播地址(0xFF)的命令請求。命令字:1字節,即功能命令。見下文,按照規定的格式訪問設備。數據長度:1字節,表示后面跟隨的有效數據區的字節數,范圍0 - 240。數據區:N字節,為有效數據,長度為前面數據長度定義的字節數。數據長度為0時沒有數據區。數據區的數值,參見命令描述。CRC校驗:2字節,從開始字符到數據區最后一個字節的所有字符的16位CRC校驗值。校驗值低位再前,高位在后。結束字符:1字節,表示數據包結束。固定為0x0E。
數據幀斷幀時間閾值在發送數據包的時候,要求先準備好所有要發送的字符,連續發送出去,中間不得有較長時間停頓。多個數據包間必須有3.5個BYTE傳送時間的時間間隔。例: RS232傳輸位如下:1 起始位8 數據位,首先發送最低有效位0 位作為奇偶校驗1 停止位
計算時間:T=3.5*( 1+數據位+奇偶校驗+停止位) / 波特率如:使用9600,則間隔時間為4ms。在波特率大于19200的時候,使用固定的時間1.75ms
注意:既然定義了這樣的時間間隔,那么就必須要要求發送端發過來的數據幀也要滿足這個時間間隔的標準。
eg:約定總線的波特率為115200,如果發送端需要發送3個數據幀的時候,那么就必須要要求這個三個數據幀與幀的時間間隔必須要要大于1.75MS,否者接收端就會把這三個數據幀認為是一個數據幀。
程序代碼實現首先需要對串口初始化:將單片機的串口配制成實際要求的串口波特率,和通訊格式,我這里默認配置為115200,8,N,1的方式。串口要打開接收中斷,每接收一個BYTE數據,就要進入中斷一次。定時器也需要進行初始化,將定時的時間間隔為50uS,并且啟動定時器,讓定時器間隔50uS進入中斷函數一次。串口與定時器初始化的代碼我就不公布了,這是單片機開發人員的常規操作。而且每款單片機的初始化的方式不一樣。
接下來就進入主題了,在進入主題之前,先介紹下我所定義的一些結構體類型,不然后面看起來會相當的吃力:首先,定義了一個T_FramCtlType的數據結構體。這個結構體用來記錄幀與幀之間的時間間隔。主要是用在定時器的中斷函數中,這里大家有個相關的映象即可:
然后我定義了一個消息和循環緩存的數據結構體:
ComMsgType單個數據幀的最大長度,我這里定義為為250個字節,大家在使用的時候,可以根據自己的協議設計要求,可以自行定義。MsgFifoType是串口消息的循環緩存隊列和幀時間的一個結構體,可以緩存10個數據幀,后面我們所有的操作都是圍繞著這個結構體進行操作。接下來是協議相關的,我這里宏定義了數據幀的幀頭,幀尾和數據幀類容的結構體。
下面開始看看代碼的編寫,首先是結構體數據的初始化。
注意:COMMsgFifo[_COMx]這個是我為了適配多路串口同時使用,所以定義了多個MsgFifoType類型的緩存,一路串口一個COMMsgFifo緩存池。所以這里需要稍微把思路轉換下。緊接著就是BandIndex的值更改。這個值主要為了定義我們幀時間間隔閾值。
比如,我這里使用的是115200的波特率,那么BandIndex的值為6.后面進行斷幀的時候只需要根據這個值去查找Tim3_5ByteTab數值,確定出斷幀閾值。
一切準備就緒的時候,就可以開始接收數據了,當串口中斷接收到一個BYTE的時候,會進入串口中斷函數。
中斷函數就只干了一件事行,把接收到的數據dat通過Pro_ComDataSaveToBuf()函數寫入到緩存中。
在Pro_ComDataSaveToBuf()函數中,首先要使能T_FramCtl結構體中的超時檢測機制,然后對Counter清零。接下來就是將這個數據寫到合適的位置,根據當前緩存數據幀的位置,以及數據幀Len的長度,同時Len要自加。COMMsgFifo[_COMx].Msg[COMMsgFifo[_COMx].AddWrite].Data[COMMsgFifo[_COMx].Msg[COMMsgFifo[_COMx].AddWrite].Len++] = _Dat;這句代碼很關鍵,需要多花點時間進行研究。雖然這里只寫了簡單的基于,但是卻反復的調用了MsgFifoType結構體中的成員。接下來的if語句,也僅僅是對單個數據幀的長度進行判斷,避免數據幀長度超過250個自己,而造成內存越界。
到了這里,我們開始接收數據了,超時機制檢測已經啟動了。在看看定時器中斷是怎么操作的?
定時器是按照50us周期進行中斷的,里面也有其他的功能計時,但是與我們串口斷幀相關的就一個函數,pro_ComFramTim50uS()。
在這個函數中,看以看到,當T_FramCtl.Enable使能之后,T_FramCtl.Counter開始自加,如果串口中斷中一直有數據來,這個T_FramCtl.Counter的值會一直被清零,T_FramCtl.Counter值一直達不到閾值條件。當串口中斷接收數據完畢之后,串口沒有數據了,由于T_FramCtl.Counter自加,當計數達到35之后,也就是我們之間設置的1.75ms,這時就滿足了if (COMMsgFifo[_COMx].T_FramCtl.Counter > Tim3_5ByteTab[COMMsgFifo[_COMx].BandIndex] )的條件。判斷條件if ( COMMsgFifo[_COMx].Remain < COM_RXTX_FIFO_SIZE )是為了判斷緩存池是否滿了,如果沒有滿COMMsgFifo[_COMx].Remain才自加,其表示當前緩存數據幀的數目,另外COMMsgFifo[_COMx].AddWrite也自加一次,為下一次數組幀寫的位置加1。最后還有個重要的事情COMMsgFifo[_COMx].T_FramCtl.Enable = 0關閉超時檢測機制。
這樣,一個數據幀就被完成的接收到數據緩存中了,生產者已經完成。后面所有的數據幀都是按照這個機制,進行數據產生;接下來就是數據消費者,數據處理了。
數據處理,我是放在主循環while(1)中
由DealUartProtocolEvent()函數進行數據處理。
在DealUartProtocolEvent()函數中,首先判讀COMMsgFifo[COMx].Remain 的值,只有大于0的時候,才表示緩存隊列中有數據幀需要處理。接下來數據幀需要通過pro_UartPacketAnalyse()函數進行數據幀合法性進行判斷,如果數據幀合法,則將數據幀的類容傳遞給InstructCMD_Uart結構體(當然包含設備類型、設備地址、命令字、數據內容、數據長度等參數)。由pro_UartFrameProtocolParse()函數對數據內容和功能進行解析。這里我就不對這個函數進行展示了。最后就是循環隊列的COMMsgFifo[COMx].AddRead自加,實現首位相連循環的讀,同時不要忘記了COMMsgFifo[COMx].Remain減一。這樣,一個數據幀在串口接收的時候就已經實現了數據斷幀,后面在主函數中處理的不過是后期的數據幀處理而已。
最后的最后,在讓我展示下,數據幀是如何進行合法性判斷的。
大致內容,就是先判斷了幀頭、然后是設備類型、設備ID、數據幀長度、幀尾,以及CRC校驗值。只有都通過了才將數據幀的內容傳遞出去,否者就傳遞固定的錯誤應答和應答代碼。
實際測試:說了這么多,那就實際測試一下吧。藍色的為發送數據幀,紅色的設備應答數據幀。
錯誤功能碼定義:
第一個數據幀 AA FF FF 23 00 0C 25 0E 為正常完整的數據幀。所以設備正常應答。第二個數據幀FF FF 22 00 9C 24 0E 缺少幀頭,應答數據幀應答錯誤功能碼2F,數據長度為1,數據內容為80(幀頭錯誤)第三個數據幀AA FF FF 24 01 3C 27 0E 缺功能碼為24,數據長度為1,但是數據區內容丟失,后面的兩個字節為CRC16校驗和幀尾,應答數據幀應答錯誤功能碼2F,數據長度為1,數據內容為83(長度錯誤)第四個數據幀AA FF FF 25 00 AC 26 缺少幀尾,應答數據幀應答錯誤功能碼2F,數據長度為1,數據內容為83(長度錯誤)
總結一下:這種斷幀方式,基本上是比較通用的一種方式,我也運用很多的項目設計中,特別是針對低端的單片機,當然STM32的IDLE中斷也是一個很優秀的方式。