分享一種有趣的數據解析方法
本片筆記是一篇開發小結,總結GPS數據的接收、解析示例,以實例為基礎分享一些思考過程:
GPS數據協議
常用的GPS模塊大多采用NMEA-0183 協議,目前業已成了GPS導航設備統一的RTCM(Radio Technical Commission for Maritime services)標準協議。
NMEA-0183 是美國國家海洋電子協會(National Marine Electronics Association)所指定的標準規格,這一標準制訂所有航海電子儀器間的通訊標準,其中包含傳輸資料的格式以及傳輸資料的通訊協議。
協議采用 ASCII 碼來傳遞 GPS 定位信息,我們稱之為幀。
幀格式形如:
- $aaccc,ddd,ddd,…,ddd*hh(CR)(LF)
GPS幀數據種類大致如下:
實際應用中,并不是所有數據都完全用得上,我們可以根據需要選擇所需要的數據。
下面我們以$GPGGA數據為例分享接收、解析方法。
$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數據接收
GPS模塊使用串口通信,在解析之前當然需要先接收數據。我這里是在嵌入式Linux平臺下做的接收,讀串口的接口如:
- int uart_read(void *data, int data_len, long time_out);
下面分享我在實際應用中的三種接收方法:
方法一:粗略法
為了能快速驗證數據解析、跑通整個過程,可以先使用粗略的方法獲取數據。粗略法我們可以先不用考慮一幀數據的實際字節數,我們先大致設置一個用于解析的緩沖數組,如:
- char rx_gps_data[512];
uart_read每次讀到的字節數與線程掛起時間有關,粗略法我們大致設置一個串口接收緩沖數組,如:
- char uart_rx_buf[64];
這時候需要把每次收到的uart_rx_buf里的內容自己拼接一下,存放到rx_gps_data中,再去做解析。
粗略法可以用于快速驗證數據解析、跑通整個過程,缺點就是uart_rx_buf、rx_gps_data設置得不夠合理的話可能會破壞掉大量的數據幀。
一般我都比較習慣地先快速調通整個流程,再慢慢做優化。
方法二:狀態機法
上面地粗略法可能會破壞掉一些數據幀,另外,代碼結構可能不夠清晰。針對這些問題做改進,使用狀態機來接收。一字節一字節地接收,接收完完整一幀數據之后再去做解析。
代碼如:
- // GGA所有狀態(GGA數據示例:$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數據,每次走到GGA_STATE_CHECK1狀態時的rx_gps_gga_data就是完整的gga數據,這時候就可以進行解析了,可以在這一步設置一個標志變量表明gga數據已經完全接收完畢,直到數據接收完畢了才做解析。
這種方法雖然可以比較好地接收數據,在單片機下很好用。但是在這里,相同的線程掛起時間情況下,每次uart_read只獲取一個字節,這樣會損耗一定的接收效率,有點拆東墻補西墻的感覺。
在我們這邊的應用中,與算法所需的時序要求有沖突了,所以只能再想想其它方法。下面看看方法三。
方法三:時間戳法
這種方法需要明確每一幀數據包含有什么數據,以及數據輸出的頻率是多少。在相同的線程掛起時間情況下,先把用于uart_read接收數據的buffer設置得稍微大一點,看每一次最多能讀取到多少個字節得數據以及讀完一幀數據需要讀幾次串口數據。
然后我們可以通過時間來區分每一幀數據及每一包串口數據,該重新組包地就重新組包。
例如:每幀數據間隔200ms,線程掛起時間10ms,一幀數據有130字節,一幀數據由1包、2包串口數據組成。
可以通過時間戳來判斷每一包之間是數據幀之間的間隔還是每一幀數據里的兩個數據包之間地間隔,再做相應的邏輯處理即可很好地接收數據。
GPS數據解析
gps數據怎么解析呢?
方法可能很多,我們先看一下正點原子的解析方法:
大概分為兩步,第一步先獲取逗號的位置確定某個需要解析地字段,然后再將相應字段的字符串數據轉換成數字。
這里分享一種簡單實用的解析方法,思路與上面差不多,但是相對比較簡單清晰些:
- 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);
- /* 數據校驗 */
- 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);
- }
- /* 字符串轉數字 */
- 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函數在做字符串相關解析時很好用,這里配合正則表達式來使用,上面這一句代碼的意思就是從p_gga中取逗號前面的數據存放到gps_gga_str[i]中,因為gga數據都是用逗號隔開的,循環幾次就可以把所有數據解析出來,很方便。
正則表達式學習資源如:
- 1、https://deerchao.cn/tutorials/regex/regex.htm
- 2、https://www.runoob.com/regexp/regexp-syntax.html
下面再看一下,sscanf+正則表達式的幾種簡單用法:
「1、取指定長度的字符串。」
如在下例中,取最大長度為4字節的字符串。
- sscanf("123456 ", "%4s", str);
「2、 取到指定字符為止的字符串。」
如在下例中,取遇到空格為止字符串。
- sscanf("123456 abcdedf", "%[^ ]", str);
「3、取僅包含指定字符集的字符串。」
如在下例中,取僅包含1到9和小寫字母的字符串。
- sscanf("123456abcdedfBCDEF", "%[1-9a-z]", str);
「4、取到指定字符集為止的字符串。」
如在下例中,取遇到大寫字母為止的字符串。
- scanf("123456abcdedfBCDEF", "%[^A-Z]", str);
sscanf+簡單、易理解的正則表達式的方法有時候可以幫助我們很方便地進行字符串數據地解析。sscanf+復雜的正則表達式不太建議使用,因為代碼可讀性太差了。
另外,使用sscanf+正則表達式時有必要寫點注釋,有見過這種方式還好,有些后面看你代碼的人可能沒接觸過正則表達式可能一時半會兒理解不了。
我之前大三出去實習的時候,在公司里就看到這樣的代碼,那時候知識儲備還不夠,第一次看到sscanf+正則表達式這種解析方法,但是搜索又搜索不到相關答案,很苦惱。所以,平時有必要寫一些注釋,利人利己。
參考:
1、正點原子《ATK-NEO-6M GPS模塊》資料。
2、https://blog.csdn.net/absurd/article/details/1177092
本文轉載自微信公眾號「嵌入式大雜燴」,可以通過以下二維碼關注。轉載本文請聯系嵌入式大雜燴公眾號。