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