接著上一篇,咱們繼續說GPS數據解析的問題,GPS數據解析的核心問題可以歸結為如何解析以逗號作為分隔符的字符串問題。看似很簡單的一個功能,真正實現起來也那不是那麼容易,在調試的過程中,我就遇到了很多的小問題,在此做個完整的記錄與總結,希望對大家有幫助。
首先給大家介紹一下strtok函數,它是標準函數庫中的一員,標準函數庫是一個工具箱,它能極大地擴展C程式設計師的能力,我們需要熟悉並且靈活的應用。
char *strtok(char *str, const char *delim),功能是分解字符串str 為一組字符串,delim為分隔符。
該函數返回被分解的第一個子字符串,如果沒有可檢索的字符串,則返回一個空指針。
我們看一下這個函數的使用例子,
程序清單1: strtok函數使用示例1
include <stdlib.h>34;Apple,Pear,Potato,11&34;,&34;%s\r\n&34;,&include <stdio.h>include <string.h>int main(void){ char str[] =&34;; char *tokens = strtok (str,&34;); //iterate over tokens.. . while (tokens!= NULL) { printf (&34;,tokens); tokens = strtok (NULL,&34;); } return 0;}
輸出結果如下:
Apple
Pear
Potato
11
和第一個程序輸出的結果完全一致,起初我對這個結果很不理解,我本能的以為第一次調用strtok的返回值是&34;,第二次調用strtok的返回值為&34;,第三次調用後,由於2個逗號之間是空的,我以為返回值會是NULL,然後在第四次調用後,得到&34;。
事實證明我的想法是錯的,錯在第三次調用strok函數後的返回值,並不是我想的那樣返回NULL,實際上第三次調用後,返回值是&34;。也就說當檢索到兩個連續的逗號之間沒有字符串,它會自動往後檢索,把後面的下一個逗號前的字符串返回。
strtok熟悉後,我們需要思考一個重要的問題,就是如何判斷出逗號間為空的狀況。不然直接使用strtok循環的去解析,當出現逗號間為空時,就會出現欄位無法再一一對應的情況。什麼意思呢,看上面的代碼,就是程序並沒法知道第三個欄位是空,解析出來的&34;也不知道對應是第幾個欄位的。
可以考慮採用以下方式來解決,程序裡先去判斷是否有連續逗號(&34;),如果有則將&34;替換為&34;形式,其中@是一個正常情況下該欄位不會出現的字符。這樣操作之後逗號分隔的各個欄位就都有了內容,再進行解析就不會出現上述的問題了。那如何用程序實現字符串的替換功能呢?
即對於上述字符串:&34;
我們希望經過替換後字符串變為:
&34;
大家可以看一下下面的代碼(替換函數strrpl是直接谷哥出來的)
程序清單3:實現字符串替換功能
include <stdlib.h>34;cannot find string \n&34;Apple,Pear,,Potato,11&34;,,&34;,@,&34;%s\n&34;,&34;%s\r\n&34;,&34;Apple,Pear,,,Potato,,11&include <stdio.h>include <string.h>char* strrpl(char *str, char* find, char *replace){ int i; char *pt = strstr(str, find); char *firstStr; if(pt == NULL){ printf(&34;); return NULL; } firstStr = (char* )malloc(100 * sizeof(char)); // copy just until i find what i need to replace // i tried to specify the length of firstStr just with pt - str strncpy(firstStr, str, strlen(str) - strlen(pt)); strcat(firstStr, replace); strcat(firstStr, pt + strlen(find)); for(i = 0; i < strlen(firstStr); i++) str[i] = firstStr[i]; return str;}int main(void){ char str[] =&34;; while (strstr(str, &34;)) strrpl(str, &34;, &34;); printf(&34;,str); char *tokens = strtok (str,&34;); //iterate over tokens.. . while (tokens!= NULL) { printf (&34;,tokens); tokens = strtok (NULL,&34;); } return 0;}
這個代碼運行後出現了如下問題:
看起來像是數組越界了,經過分析可知是str數組越界導致的,由於&34;被替換成&34; ,導致數組長度變長從而產生越界。所以上述代碼不能那麼寫,我們可以通過定義一個新的更長長度的數組來解決。另外還有一點需要注意的是:strok函數執行任務時,它會修改它所處理的字符串,如果源字符串不能被修改,就必須得複製一份,將這份拷貝傳給strok函數。
改進後的代碼如下:
程序清單5:字符串操作時要防止越界
include <stdlib.h>34;cannot find string \n&34;Apple,Pear,,,Potato,,11&34;,,&34;,,&34;,@,&34;%s\n&34;,&34;%s\r\n&34;,&34;$GNRMC,051035.00,A,4000.74054,N,11628.03344,E,0.253,,020320,6.91,W,D*23\n\$GNVTG,,T,,M,0.253,N,0.468,K,D*36\n\$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D\n\$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F\n\$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F\n\$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B\n\$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77\n\$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77\n\$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D\n\$GPGSV,5,5,17,50,42,164,34*48\n\$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E\n\$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60\n\$GLGSV,3,3,10,87,26,128,32,88,00,163,*61\n\$GNGLL,4000.74054,N,11628.03344,E,051035.00,A,D*7A&include <stdio.h>include <string.h>char* strrpl(char *str, char* find, char *replace){ int i; char *pt = strstr(str, find); char *firstStr; if(pt == NULL){ printf(&34;); return NULL; } int len = strlen(str)+1+strlen(replace)-strlen(find); firstStr = (char* )malloc(len); memset(firstStr,0,len); // copy just until i find what i need to replace // i tried to specify the length of firstStr just with pt - str strncpy(firstStr, str, strlen(str) - strlen(pt)); strcat(firstStr, replace); strcat(firstStr, pt + strlen(find)); for(i = 0; i < strlen(firstStr); i++) str[i] = firstStr[i]; free(firstStr); return str;}int main(void){ char str[] = &34;; char *buff; buff = malloc(sizeof(str)+100); memset(buff, 0, sizeof(str)+100); memcpy(buff, str, sizeof(str)); while (strstr(buff, &34;)) strrpl(buff, &34;, &34;); printf(&34;,buff); char *tokens = strtok (buff,&34;); //iterate over tokens.. . while (tokens!= NULL) { printf (&34;,tokens); tokens = strtok (NULL,&34;); } free(buff); return 0;}
這樣再次運行代碼,就可以得到正確的結果了。
有了以上基礎,就可以實際來寫GPS數據解析的代碼了,在上一篇文章的基礎上,我對整個目錄結構做了調整,新的工程總共有5個文件,mian.c為主程序,gnss.c和gnss.h和GNSS數據解析相關,uart.c和uart.h對應串口配置。
運行後,會輸出如下信息:
上述代碼中重點是gnss.c文件中的gps_analyse函數,大家可以好好看看,
int gps_analyse(char *buff,int buff_len,GNSS *gps_data){ char *ptr = NULL; if(strlen(buff)<10) { return -1; } /* 如果buff字符串中包含字符&34;則將$GPRMC的地址賦值給ptr */ if( NULL==(ptr=strstr(buff,&34;)) && NULL==(ptr=strstr(buff,&34;)) ) { return -2; } if(check_nmea_message(ptr, 0, buff_len) <0 ) { printf(&34;); return -3; } char *tmpbuf; tmpbuf = (char *)malloc(strlen(ptr)+100); memset(tmpbuf, 0, strlen(ptr)+100); memcpy(tmpbuf, ptr, strlen(ptr)); while (strstr(tmpbuf, &34;)) strrpl(tmpbuf, &34;, &34;); printf(&34;,tmpbuf); char* pch = strtok(tmpbuf, &34;); // 1 time pch = strtok(NULL, &34;); nmea_get_time(pch, &gps_data->time); // 2 status pch = strtok(NULL, &34;); gps_data->pos_state = *pch; //3 latitude pch = strtok(NULL, &34;); nmea_lat_long_to_double(&gps_data->latitude, pch, strlen(pch)); //4 latitude direction pch = strtok(NULL, &34;); gps_data->NS = *pch; //5 longitude pch = strtok(NULL, &34;); nmea_lat_long_to_double(&gps_data->longitude, pch, strlen(pch)); //6 long direct pch = strtok(NULL, &34;); gps_data->EW = *pch; //7 speed pch = strtok(NULL, &34;); gps_data->speed = 1.852 * strtof(pch, (char **) NULL ) / 3.6; //8 direction pch = strtok(NULL, &34;); gps_data->direction = strtof(pch, (char**)NULL); //9 date pch = strtok(NULL, &34;); nmea_get_date(pch, &gps_data->time); //10 不處理 pch = strtok(NULL, &34;); //11 不處理 pch = strtok(NULL, &34;); //12 mode pch = strtok(NULL, &34;); gps_data->pos_mode = *pch; free(tmpbuf); return 0;}
我在調試過程中遇到了很多的問題,通過自己實際動手搬運、修改、調試代碼收穫了很多知識,主要有以下幾點:
1) 在使用strtof、strtod函數時,一定要加上頭文件39;\0&39;\0'。
3) 要養成初始化指針、內存空間後,立刻賦初值的習慣。
4)strok函數適合用來分割字符串,解析各個欄位。
5)操作字符串/字符數組時一定要注意越界的問題。