事實上串口實現了數據通信過程中的傳輸層,而應用層就系統功能的業務邏輯,應用層控制需要收發的各種數據內容。
數據解析的前提是通信雙方都是用統一的數據幀格式,因此在這里將設計一個簡單的起止式的數據幀格式,保證設備之間進行可靠的通信。
現在的很多無線模塊,為了使用簡單和易于集成,模塊對外使用UART接口,并采用AT指令來完成配置和使用,常見的有ESP8266的WiFi模塊、HC-05藍牙串口模塊。
AT指令的特點是易于人機交互,使用者對其發AT指令時,都是用ASCII字符發送,對于模塊的處理,也是以字符來處理。這樣的AT指令,它的起止式特點是以“AT”兩個字符開頭,并以回車換行“\\r\\n”字符結束。
HC-05藍牙模塊指令示例
但是項目工程中,數據在嵌入式設備是以HEX數據(16進制)運算和處理,如果參考AT指令去設計幀結構,那么在收發處理時候,必然要將收到的純數據(16進制)按照字符處理。
比如一個終端設備,其功能就是環境檢測,可能包含溫濕度、光照強度、二氧化碳濃度、PM2.5濃度等等,如果要發出一個溫度采集結果24℃數據,采集設備將數據24分成2個字節發送,因為ASCII字符’2’對應的16進制是0x32、ASCII字符’4’對應的16進制是0x34,這樣的一個溫度數據就需要2個字節來發送。接收端接收到的是0x32、0x34后,再以查表方式逆向換算出原溫度數據’24’,這個過程就是采用字符處理的麻煩之一。
因此不考慮使用ASCII字符來組幀結構。
精簡起止式結構
最簡單的幀,就是有開頭+結尾做起止標志。
比如 0x55 + 數據包 + 0xAA 。
在一長串的數據流中,接收端逐字節接收,并判斷是否存在0x55,如果存在則開始存入數據包緩沖器,直到接收了0xAA數據,認為完成一幀數據的接收。
這個方法確實相當簡單,不用太多的處理,只需要判斷開頭和結尾即可。
而這樣存在很大的問題,如果傳輸的內容也有0xAA這樣的數據,這個0xAA并非結尾標志,而程序接收過程就提前結束,這樣就不能保證完整接收一幀數據包了。
增加長度限制
在精簡起止式結構基礎上,增加一數據來標志數據包長度。
比如 0x55 + 長度 + 數據包 + 0xAA 。
這樣一來,接收端判斷接收到了0x55的開頭標志,緊接著再接收一個“長度”的字節,基于這個長度來繼續接收后續剩余的數據。
可見如果有了長度的約束,那么最后都不需要0xAA作為結尾標志了。
這樣的接口,即使有開頭、長度、結尾,還存在風險。比如傳輸數據時,物理線路受到未知干擾,導致數據內容出現了異常,那么接收端即使完整接收所有數量的數據下來,也是錯誤的內容。
增加校驗檢查
解決在發送過程中出現的未知錯誤問題,必然需要對數據進行校驗。再增加一字段來標志數據內容的校驗計算結果。
比如 0x55 + 長度 + 校驗值 + 數據內容 + 0xAA 。
校驗值是對數據包采用算法計算而得,接收方完整收下所有數量的數據,再對數據包采用同樣的算法計算出校驗值,從而對比校驗值來確定數據包的準確性。
對于校驗值的運算,采用CRC-16運算的方式,檢錯能力強,開銷小。
設計協議幀結構
綜上所述,基于起止式的幀結構可以設計成:**0x55 + 長度 + CRC校驗 + ** 數據包 。
在這里,幀頭標志采用0x55一個字節。
0x55二進制是01010101,這樣在UART物理線路上輸出的信號將會是占空比50%的方波,方波是最容易進行測量和診斷的,在實際波形觀測時可以確定穩定性、噪聲毛刺等。
要說0xAA(二進制10101010)也是可以,但是UART發送時候是有一個起始位0,并且是以LSB方式先發送bit0的最低位,0xAA的bit0已經是0,而0x55的bit0是1,因此想得到方波當然優先考慮用0x55。
長度采用一個字節表示,則后續的CRC校驗 + 數據包的總數量最多能放255個字節。
CRC校驗采用CRC-16算法,占2個字節,此時后續的數據包最多能放253個字節。
終上所述,得出最終的起止式幀結構:
接下來開始設計處理程序。
根據幀結構,可以定義如下的結構體:
typedef struct {
*uint8_t **head;* *uint8_t** len;* *uint8_t** crc16L;* *uint8_t** crc16H;* *uint8_t** packet[253];*
}sst_frame_t;
其中要特別說明的:
packet 數據包最大長度設為253 ,是因為len是uint8_t類型,len 最大255 ,而CRC校驗值占了2個字節,因此packet數據包最多可253個字節。
CRC校驗值采用的是CRC-16標準,校驗值是個uint16_t類型的數據,傳輸時采用的是LSB模式,因此將CRC校驗值設為兩個uint8_t類型的數據,這樣做便于在源碼移植過程中,不同平臺的大小端差異能夠得到正確處理。
簡述嵌入式設備內存大小端差異在結構體定義以及使用時存在的問題:
假如對幀結構定義了如下的結構體:
typedef struct {
*uint8_t **head;* *uint8_t** len;* *uint16_t **crc16;* *uint8_t** packet[253];*
}sst_frame_t;
計算后得到某一次的校驗值結果是 0xDC66 ,這是一個uint16_t**類型的數據,如果直接使用這個結構體來處理數據發送,那么:
在LSB的小端模式平臺下,數據的發送順序是 **
head 、len 、0x66 、0xDC 、packet[0] 、packet[1] 、...
反之在MSB大端模式的平臺里,數據的發送順序是 **
head 、len 、0xDC 、0x66 、packet[0] 、packet[1] 、...
因此采用2個字節uint8_t數據類型代替uint16_t來定義結構體中的CRC校驗值,使得在跨平臺收發數據時無需做差異化處理。
構建幀結構
使用起止式進行數據傳輸時,把應用層的數據包進行組幀,這樣可構造一個完整的數據幀,便于在應用層將完整的一幀數據傳遞給傳輸層發出。
這里的構造過程,事實上是對幀結構的“填充”過程。
首先是計算數據包的CRC校驗值,隨后就是“填充”的過程。
為了防止應用層調用接口時,傳進來的數據包的地址、組幀結果的首地址指向同一個內存地址,所以在組幀前需要將源數據內容單獨緩存,再進行“填充”的操作。
解析幀結構
解析幀結構其實就是對一長串的數據流進行解析處理,從而提取出數據包。
這里被解析的數據來源是一個循環緩沖區,對循環緩沖區內的可讀數據進行解析。因此需要使用循環緩沖區配合。
代碼截圖:
解析思路是:
1.確保環形緩沖區有足夠一個幀結構的數據量,否則返數據量不足的錯誤;
2.接著讀出一個字節判斷幀頭標志是否為0x55,否則返幀頭錯誤;
3.再次讀一個字節作為幀長度數據,且長度至少3個字節(2個CRC校驗值+至少1字節數據包),否則返幀長度錯誤;
4.讀出幀長度數據,如果此時環形緩沖區的可讀數量比長度數值小,出現這情況的原因可能是幀長度字段在發送期間出現異常,或是對端設備串口傳輸慢而未完整傳輸一幀,此時可做適當的延時等待,如果超時退出,且返幀長度錯誤;
5.繼續讀出2個字節作為CRC校驗值,且需要注意先收到的是crc16L,先收到小端數值;
6.緊接著把數據包讀出,此時讀的長度應該是第4步中的幀長度數據少2個字節;
7.最后對數據包計算一個CRC校驗值,對比接收到的校驗值,校驗值不一致則返錯誤校驗碼。
函數返回值符合以下枚舉的錯誤碼:
被解析數據源
看到這里也許仍有疑問,用于解析的數據源哪來?數據什么時候被寫進環形緩沖區內?
dclib_ringbuffer這個模塊屬于應用庫模塊層,而如果直接把dclib_rb_writebyte這一個接口放在串口接收中斷里執行,這就破壞了系統的架構層次,對工程代碼的維護和移植是個麻煩事,因此采用回調函數的方式。
嵌入式開發工程師都知道,一般在使用官方的庫時,經常會遇到需要自己實現一些回調函數,從而利用注冊接口把回調函數傳遞給庫或者驅動層,使庫或者驅動層在執行時調用該回調函數。
根據這個思路,同樣的這里也采用回調函數的形式,回調函數內完成了把串口接收到的數據寫入環形緩沖區內。
回調函數的實現源碼截圖:
事實上僅僅調用了dclib_ringbuffer功能里的寫一字節接口dclib_rb_writebyte,回調函數傳進來的參數dat就是串口接收到的數據。
有了回調函數,還要把這個回調函數的地址傳給底層驅動,這也就是常說的“注冊”的過程,注冊接口在固件板級接口層里串口模塊dcbsp_uart實現,注冊接口時dclib_uart_callback_reg函數:
又偏題了,關于回調函數在此不做深入論述。
簡而言之,環形緩沖區寫入一字節的執行過程,放在回調函數里,當串口接收中斷觸發后,中斷里會根據注冊的回調函數地址,進而執行回調函數,實現對環形緩沖區寫入一個字節數據。如此操作的理由是不改變工程代碼的分層架構,并且便于維護與移植!
為了縮減篇幅,最后貼上測試代碼的部分:
最后也附上調試期間串口打印的解析結果:
評論
查看更多