分享一種有趣的數(shù)據(jù)解析方法
本片筆記是一篇開發(fā)小結(jié),總結(jié)GPS數(shù)據(jù)的接收、解析示例,以實例為基礎分享一些思考過程:
GPS數(shù)據(jù)協(xié)議
常用的GPS模塊大多采用NMEA-0183 協(xié)議,目前業(yè)已成了GPS導航設備統(tǒng)一的RTCM(Radio Technical Commission for Maritime services)標準協(xié)議。
NMEA-0183 是美國國家海洋電子協(xié)會(National Marine Electronics Association)所指定的標準規(guī)格,這一標準制訂所有航海電子儀器間的通訊標準,其中包含傳輸資料的格式以及傳輸資料的通訊協(xié)議。
協(xié)議采用 ASCII 碼來傳遞 GPS 定位信息,我們稱之為幀。
幀格式形如:
- $aaccc,ddd,ddd,…,ddd*hh(CR)(LF)
GPS幀數(shù)據(jù)種類大致如下:
實際應用中,并不是所有數(shù)據(jù)都完全用得上,我們可以根據(jù)需要選擇所需要的數(shù)據(jù)。
下面我們以$GPGGA數(shù)據(jù)為例分享接收、解析方法。
$GPGGA 語句的基本格式如下:
- $GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>,<13>,<14>*hh<CR><LF>
舉例如下:
- $GPGGA,082006.000,3852.9276,N,11527.4283,E,1,08,1.0,20.6,M,,,,0000*35
GPS數(shù)據(jù)接收
GPS模塊使用串口通信,在解析之前當然需要先接收數(shù)據(jù)。我這里是在嵌入式Linux平臺下做的接收,讀串口的接口如:
- int uart_read(void *data, int data_len, long time_out);
下面分享我在實際應用中的三種接收方法:
方法一:粗略法
為了能快速驗證數(shù)據(jù)解析、跑通整個過程,可以先使用粗略的方法獲取數(shù)據(jù)。粗略法我們可以先不用考慮一幀數(shù)據(jù)的實際字節(jié)數(shù),我們先大致設置一個用于解析的緩沖數(shù)組,如:
- char rx_gps_data[512];
uart_read每次讀到的字節(jié)數(shù)與線程掛起時間有關,粗略法我們大致設置一個串口接收緩沖數(shù)組,如:
- char uart_rx_buf[64];
這時候需要把每次收到的uart_rx_buf里的內(nèi)容自己拼接一下,存放到rx_gps_data中,再去做解析。
粗略法可以用于快速驗證數(shù)據(jù)解析、跑通整個過程,缺點就是uart_rx_buf、rx_gps_data設置得不夠合理的話可能會破壞掉大量的數(shù)據(jù)幀。
一般我都比較習慣地先快速調(diào)通整個流程,再慢慢做優(yōu)化。
方法二:狀態(tài)機法
上面地粗略法可能會破壞掉一些數(shù)據(jù)幀,另外,代碼結(jié)構(gòu)可能不夠清晰。針對這些問題做改進,使用狀態(tài)機來接收。一字節(jié)一字節(jié)地接收,接收完完整一幀數(shù)據(jù)之后再去做解析。
代碼如:
- // GGA所有狀態(tài)(GGA數(shù)據(jù)示例:$GPGGA,023543.00,2308.28715,N,11322.09875,E,1,06,1.49,41.6,M,-5.3,M,,*7D)
- #define GGA_STATE_START 0 // $
- #define GGA_STATE_HEAD1_G 1 // G
- #define GGA_STATE_HEAD2_P 2 // P
- #define GGA_STATE_HEAD3_G 3 // G
- #define GGA_STATE_HEAD4_G 4 // G
- #define GGA_STATE_HEAD5_A 5 // A
- #define GGA_STATE_DATA 6 // ,023543.00,2308.28715,N,11322.09875,E,1,06,1.49,41.6,M,-5.3,M,,*
- #define GGA_STATE_CHECK0 7 // 7
- #define GGA_STATE_CHECK1 8 // D
- static uint16_t gga_len = 0;
- static uint8_t gga_state = GGA_STATE_START;
- static void gps_gga_data_get(char in_data)
- {
- switch (gga_state)
- {
- case GGA_STATE_START:
- if ('$' == in_data)
- {
- gga_len = 0;
- memset(rx_gps_gga_data, 0, GGA_DATA_MAX_LEN);
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_HEAD1_G;
- }
- else
- {
- gga_state = GGA_STATE_START;
- }
- break;
- case GGA_STATE_HEAD1_G:
- if ('G' == in_data)
- {
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_HEAD2_P;
- }
- else
- {
- gga_state = GGA_STATE_START;
- }
- break;
- case GGA_STATE_HEAD2_P:
- if ('P' == in_data)
- {
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_HEAD3_G;
- }
- else
- {
- gga_state = GGA_STATE_START;
- }
- break;
- case GGA_STATE_HEAD3_G:
- if ('G' == in_data)
- {
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_HEAD4_G;
- }
- else
- {
- gga_state = GGA_STATE_START;
- }
- break;
- case GGA_STATE_HEAD4_G:
- if ('G' == in_data)
- {
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_HEAD5_A;
- }
- else
- {
- gga_state = GGA_STATE_START;
- }
- break;
- case GGA_STATE_HEAD5_A:
- if ('A' == in_data)
- {
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_DATA;
- }
- else
- {
- gga_state = GGA_STATE_START;
- }
- break;
- case GGA_STATE_DATA:
- if ('*' == in_data)
- {
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_CHECK0;
- }
- else
- {
- rx_gps_gga_data[gga_len++] = in_data;
- if (gga_len > GGA_DATA_MAX_LEN)
- {
- gga_state = GGA_STATE_START;
- }
- else
- {
- gga_state = GGA_STATE_DATA;
- }
- }
- break;
- case GGA_STATE_CHECK0:
- rx_gps_gga_data[gga_len++] = in_data;
- gga_state = GGA_STATE_CHECK1;
- break;
- case GGA_STATE_CHECK1:
- rx_gps_gga_data[gga_len++] = in_data;
- printf("gga data : %s\n", rx_gps_gga_data);
- gga_state = GGA_STATE_START;
- break;
- default:
- break;
- }
- }
這樣就可以完整地接收到gga數(shù)據(jù),每次走到GGA_STATE_CHECK1狀態(tài)時的rx_gps_gga_data就是完整的gga數(shù)據(jù),這時候就可以進行解析了,可以在這一步設置一個標志變量表明gga數(shù)據(jù)已經(jīng)完全接收完畢,直到數(shù)據(jù)接收完畢了才做解析。
這種方法雖然可以比較好地接收數(shù)據(jù),在單片機下很好用。但是在這里,相同的線程掛起時間情況下,每次uart_read只獲取一個字節(jié),這樣會損耗一定的接收效率,有點拆東墻補西墻的感覺。
在我們這邊的應用中,與算法所需的時序要求有沖突了,所以只能再想想其它方法。下面看看方法三。
方法三:時間戳法
這種方法需要明確每一幀數(shù)據(jù)包含有什么數(shù)據(jù),以及數(shù)據(jù)輸出的頻率是多少。在相同的線程掛起時間情況下,先把用于uart_read接收數(shù)據(jù)的buffer設置得稍微大一點,看每一次最多能讀取到多少個字節(jié)得數(shù)據(jù)以及讀完一幀數(shù)據(jù)需要讀幾次串口數(shù)據(jù)。
然后我們可以通過時間來區(qū)分每一幀數(shù)據(jù)及每一包串口數(shù)據(jù),該重新組包地就重新組包。
例如:每幀數(shù)據(jù)間隔200ms,線程掛起時間10ms,一幀數(shù)據(jù)有130字節(jié),一幀數(shù)據(jù)由1包、2包串口數(shù)據(jù)組成。
可以通過時間戳來判斷每一包之間是數(shù)據(jù)幀之間的間隔還是每一幀數(shù)據(jù)里的兩個數(shù)據(jù)包之間地間隔,再做相應的邏輯處理即可很好地接收數(shù)據(jù)。
GPS數(shù)據(jù)解析
gps數(shù)據(jù)怎么解析呢?
方法可能很多,我們先看一下正點原子的解析方法:
大概分為兩步,第一步先獲取逗號的位置確定某個需要解析地字段,然后再將相應字段的字符串數(shù)據(jù)轉(zhuǎn)換成數(shù)字。
這里分享一種簡單實用的解析方法,思路與上面差不多,但是相對比較簡單清晰些:
- static bool gps_gga_data_parse(st_gps_gga_def *out_data, char *in_data)
- {
- bool ret = FALSE;
- char *p_gga = in_data;
- if (NULL == p_gga)
- {
- return ret;
- }
- if (NULL != (p_gga = strstr(p_gga, "$GNGGA")))
- {
- printf("gga data : %s\n", p_gga);
- /* 數(shù)據(jù)校驗 */
- if (TRUE == data_check(p_gga))
- {
- printf("gga data check success!\n");
- /* 解析出字符串 */
- printf("gga data parse: \n");
- for (int i = 0; i < GGA_STR_MAX; i++)
- {
- sscanf(p_gga, "%[^,]", gps_gga_str[i]);
- printf("%s\n", gps_gga_str[i]);
- p_gga = p_gga + (strlen(gps_gga_str[i]) + 1);
- }
- /* 字符串轉(zhuǎn)數(shù)字 */
- out_data->latitude = atof(gps_gga_str[STR_LATITUDE]);
- out_data->longitude = atof(gps_gga_str[STR_LONGITUDE]);
- out_data->time = atof(gps_gga_str[STR_TIME]);
- out_data->quality = atof(gps_gga_str[STR_QUALITY]);
- ret = TRUE;
- }
- else
- {
- printf("gga data check error!\n");
- }
- }
- return ret;
- }
這里使用sscanf+正則表達式來做解析。
- sscanf(p_gga, "%[^,]", gps_gga_str[i]);
sscanf函數(shù)在做字符串相關解析時很好用,這里配合正則表達式來使用,上面這一句代碼的意思就是從p_gga中取逗號前面的數(shù)據(jù)存放到gps_gga_str[i]中,因為gga數(shù)據(jù)都是用逗號隔開的,循環(huán)幾次就可以把所有數(shù)據(jù)解析出來,很方便。
正則表達式學習資源如:
- 1、https://deerchao.cn/tutorials/regex/regex.htm
- 2、https://www.runoob.com/regexp/regexp-syntax.html
下面再看一下,sscanf+正則表達式的幾種簡單用法:
「1、取指定長度的字符串。」
如在下例中,取最大長度為4字節(jié)的字符串。
- sscanf("123456 ", "%4s", str);
「2、 取到指定字符為止的字符串。」
如在下例中,取遇到空格為止字符串。
- sscanf("123456 abcdedf", "%[^ ]", str);
「3、取僅包含指定字符集的字符串。」
如在下例中,取僅包含1到9和小寫字母的字符串。
- sscanf("123456abcdedfBCDEF", "%[1-9a-z]", str);
「4、取到指定字符集為止的字符串?!?/strong>
如在下例中,取遇到大寫字母為止的字符串。
- scanf("123456abcdedfBCDEF", "%[^A-Z]", str);
sscanf+簡單、易理解的正則表達式的方法有時候可以幫助我們很方便地進行字符串數(shù)據(jù)地解析。sscanf+復雜的正則表達式不太建議使用,因為代碼可讀性太差了。
另外,使用sscanf+正則表達式時有必要寫點注釋,有見過這種方式還好,有些后面看你代碼的人可能沒接觸過正則表達式可能一時半會兒理解不了。
我之前大三出去實習的時候,在公司里就看到這樣的代碼,那時候知識儲備還不夠,第一次看到sscanf+正則表達式這種解析方法,但是搜索又搜索不到相關答案,很苦惱。所以,平時有必要寫一些注釋,利人利己。
參考:
1、正點原子《ATK-NEO-6M GPS模塊》資料。
2、https://blog.csdn.net/absurd/article/details/1177092
本文轉(zhuǎn)載自微信公眾號「嵌入式大雜燴」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系嵌入式大雜燴公眾號。