1. 程式人生 > >STM32:GPS解析

STM32:GPS解析

做為現在的物聯網行業,手持裝置中,缺少不了的就是GPS定位功能。GPS模組和STM32的串列埠進行通訊,將GPS的資料傳送給M3的串列埠,由M3進行GPS協議的解碼。解析出來後儲存在響應的結構體中。在進行顯示。
這裡分別介紹2中解析協議的方法,第一種就是自己寫解析協議函式,第二種便是採用別人寫好的GPS解析協議庫:NMEALIB庫,將這個庫移植到M3中,直接呼叫API函式,就可以解析出GPS資訊,同樣的也儲存在一個結構體中。
下面分析一下這兩種解析協議的演算法,第一種,採用的是正點原子寫的GPS解析演算法(感謝原子哥)

//從buf裡面得到第cx個逗號所在的位置
//返回值:0~0XFE,代表逗號所在位置的偏移.
//       0XFF,代表不存在第cx個逗號                             
u8 NMEA_Comma_Pos(u8 *buf,u8 cx)
{               
    u8 *p=buf;
    while(cx)
    {        
        if(*buf=='*'||*buf<' '||*buf>'z')return 0XFF;//遇到'*'或者非法字元,則不存在第cx個逗號
        if(*buf==',')cx--;
        buf++;
    }
    return buf-p;   //返回差值,
}

從GPS中得到的一串資料是這樣的:GPRMC,083559.00,A,4717.11437,N,00833.91522,E,0.004,77.52,091202,,,A∗57因此,我們可以呼叫這個函式,得到第幾個逗號所距離第一個字元的位置,例如:NMEACommaPos(buf,2),我們的到的是,第二個逗號距離

的位置,也就是17

//m^n函式
//返回值:m^n次方.
u32 NMEA_Pow(u8 m,u8 n)
{
    u32 result=1;    
    while(n--)result*=m;    
    return result;
}

這個就不用多說了,都看的懂,

//str轉換為數字,以','或者'*'結束
//buf:數字儲存區
//dx:小數點位數,返回給呼叫函式
//返回值:轉換後的數值
int NMEA_Str2num(u8 *buf,u8*dx)
{
    u8 *p=buf;
    u32 ires=0,fres=0;
    u8 ilen=0,flen=0,i;
    u8 mask=0;
    int res;
    while(1) //得到整數和小數的長度
    {
        if(*p=='-'){mask|=0X02;p++;}//是負數
        if(*p==','||(*p=='*'))break;//遇到結束了
        if(*p=='.'){mask|=0X01;p++;}//遇到小數點了
        else if(*p>'9'||(*p<'0'))   //有非法字元
        {   
            ilen=0;
            flen=0;
            break;
        }   
        if(mask&0X01)flen++;
        else ilen++;
        p++;
    }
    if(mask&0X02)buf++; //去掉負號
    for(i=0;i<ilen;i++) //得到整數部分資料
    {  
        ires+=NMEA_Pow(10,ilen-1-i)*(buf[i]-'0');
    }
    if(flen>5)flen=5;   //最多取5位小數
    *dx=flen;           //小數點位數
    for(i=0;i<flen;i++) //得到小數部分資料
    {  
        fres+=NMEA_Pow(10,flen-1-i)*(buf[ilen+1+i]-'0');
    }
    res=ires*NMEA_Pow(10,flen)+fres;
    if(mask&0X02)res=-res;         
    return res;
}  
 

這個函式便是將兩個逗號之間的字串數字,變成整數,既將字串“235”變成int(整型)數字,235

//分析GPGSV資訊
//gpsx:nmea資訊結構體
//buf:接收到的GPS資料緩衝區首地址
void NMEA_GPGSV_Analysis(nmea_msg *gpsx,u8 *buf)
{
    u8 *p,*p1,dx;
    u8 len,i,j,slx=0;
    u8 posx;     
    p=buf;
    p1=(u8*)strstr((const char *)p,"$GPGSV");//strstr判斷$GPGSV是否是p陣列的子串,是則返回$GPGSV中首先出現的地址,
    len=p1[7]-'0';                              //得到GPGSV的條數,p1[7]表示,後面的第一個字元。
    posx=NMEA_Comma_Pos(p1,3);                  //得到可見衛星總數,既將‘,’後面的字元裡第一個字元的差值的到。
    if(posx!=0XFF)gpsx->svnum=NMEA_Str2num(p1+posx,&dx);//p1+posx 得到可見衛星總數的指標,
    for(i=0;i<len;i++)
    {    
        p1=(u8*)strstr((const char *)p,"$GPGSV");  
        for(j=0;j<4;j++)
        {     
            posx=NMEA_Comma_Pos(p1,4+j*4);
            if(posx!=0XFF)gpsx->slmsg[slx].num=NMEA_Str2num(p1+posx,&dx);   //得到衛星編號
            else break;
            posx=NMEA_Comma_Pos(p1,5+j*4);
            if(posx!=0XFF)gpsx->slmsg[slx].eledeg=NMEA_Str2num(p1+posx,&dx);//得到衛星仰角
            else break;
            posx=NMEA_Comma_Pos(p1,6+j*4);
            if(posx!=0XFF)gpsx->slmsg[slx].azideg=NMEA_Str2num(p1+posx,&dx);//得到衛星方位角
            else break;
            posx=NMEA_Comma_Pos(p1,7+j*4);
            if(posx!=0XFF)gpsx->slmsg[slx].sn=NMEA_Str2num(p1+posx,&dx);    //得到衛星信噪比
            else break;
            slx++;     
        }   
        p=p1+1;//切換到下一個GPGSV資訊
    }   
}

這個便是解析GPGSV資訊,GPGSV協議如下:
這裡寫圖片描述

//分析GPGGA資訊
//gpsx:nmea資訊結構體
//buf:接收到的GPS資料緩衝區首地址
void NMEA_GPGGA_Analysis(nmea_msg *gpsx,u8 *buf)
{
    u8 *p1,dx;           
    u8 posx;    
    p1=(u8*)strstr((const char *)buf,"$GPGGA");
    posx=NMEA_Comma_Pos(p1,6);                              //得到GPS狀態
    if(posx!=0XFF)gpsx->gpssta=NMEA_Str2num(p1+posx,&dx);   
    posx=NMEA_Comma_Pos(p1,7);                              //得到用於定位的衛星數
    if(posx!=0XFF)gpsx->posslnum=NMEA_Str2num(p1+posx,&dx);
    posx=NMEA_Comma_Pos(p1,9);                              //得到海拔高度
    if(posx!=0XFF)gpsx->altitude=NMEA_Str2num(p1+posx,&dx);  
}

這個是解析GPGGA資訊,GPGGA協議如下:
這裡寫圖片描述

//分析GPGSA資訊
//gpsx:nmea資訊結構體
//buf:接收到的GPS資料緩衝區首地址
void NMEA_GPGSA_Analysis(nmea_msg *gpsx,u8 *buf)
{
    u8 *p1,dx;           
    u8 posx;
    u8 i;   
    p1=(u8*)strstr((const char *)buf,"$GPGSA");
    posx=NMEA_Comma_Pos(p1,2);                              //得到定位型別
    if(posx!=0XFF)gpsx->fixmode=NMEA_Str2num(p1+posx,&dx);  
    for(i=0;i<12;i++)                                       //得到定位衛星編號
    {
        posx=NMEA_Comma_Pos(p1,3+i);                     
        if(posx!=0XFF)gpsx->possl[i]=NMEA_Str2num(p1+posx,&dx);
        else break;
    }                 
    posx=NMEA_Comma_Pos(p1,15);                             //得到PDOP位置精度因子
    if(posx!=0XFF)gpsx->pdop=NMEA_Str2num(p1+posx,&dx);  
    posx=NMEA_Comma_Pos(p1,16);                             //得到HDOP位置精度因子
    if(posx!=0XFF)gpsx->hdop=NMEA_Str2num(p1+posx,&dx);  
    posx=NMEA_Comma_Pos(p1,17);                             //得到VDOP位置精度因子
    if(posx!=0XFF)gpsx->vdop=NMEA_Str2num(p1+posx,&dx);  
}

這個是解析GPGSA資訊,GPGSA協議定義如下:
這裡寫圖片描述
這裡寫圖片描述
接下來就是我們通常要用到的一個協議了:GPRMC資訊

//分析GPRMC資訊
//gpsx:nmea資訊結構體
//buf:接收到的GPS資料緩衝區首地址
void NMEA_GPRMC_Analysis(nmea_msg *gpsx,u8 *buf)
{
    u8 *p1,dx;           
    u8 posx;     
    u32 temp;      
    float rs;  
    p1=(u8*)strstr((const char *)buf,"GPRMC");//"$GPRMC",經常有&和GPRMC分開的情況,故只判斷GPRMC.
    posx=NMEA_Comma_Pos(p1,1);                              //得到UTC時間
    if(posx!=0XFF)
    {
        temp=NMEA_Str2num(p1+posx,&dx)/NMEA_Pow(10,dx);     //得到UTC時間,去掉ms
        gpsx->utc.hour=temp/10000;
        gpsx->utc.min=(temp/100)%100;
        gpsx->utc.sec=temp%100;      
    }   
    posx=NMEA_Comma_Pos(p1,3);                              //得到緯度
    if(posx!=0XFF)
    {
        temp=NMEA_Str2num(p1+posx,&dx);          
        gpsx->latitude=temp/NMEA_Pow(10,dx+2);  //得到°
        rs=temp%NMEA_Pow(10,dx+2);              //得到'        
        gpsx->latitude=gpsx->latitude*NMEA_Pow(10,5)+(rs*NMEA_Pow(10,5-dx))/60;//轉換為°
    }
    posx=NMEA_Comma_Pos(p1,4);                              //南緯還是北緯
    if(posx!=0XFF)gpsx->nshemi=*(p1+posx);                   
    posx=NMEA_Comma_Pos(p1,5);                              //得到經度
    if(posx!=0XFF)
    {                                                 
        temp=NMEA_Str2num(p1+posx,&dx);          
        gpsx->longitude=temp/NMEA_Pow(10,dx+2); //得到°
        rs=temp%NMEA_Pow(10,dx+2);              //得到'        
        gpsx->longitude=gpsx->longitude*NMEA_Pow(10,5)+(rs*NMEA_Pow(10,5-dx))/60;//轉換為°
    }
    posx=NMEA_Comma_Pos(p1,6);                              //東經還是西經
    if(posx!=0XFF)gpsx->ewhemi=*(p1+posx);       
    posx=NMEA_Comma_Pos(p1,9);                              //得到UTC日期
    if(posx!=0XFF)
    {
        temp=NMEA_Str2num(p1+posx,&dx);                     //得到UTC日期
        gpsx->utc.date=temp/10000;
        gpsx->utc.month=(temp/100)%100;
        gpsx->utc.year=2000+temp%100;        
    }
}

GPRMC協議如下:
這裡寫圖片描述
這裡寫圖片描述

//分析GPVTG資訊
//gpsx:nmea資訊結構體
//buf:接收到的GPS資料緩衝區首地址
void NMEA_GPVTG_Analysis(nmea_msg *gpsx,u8 *buf)
{
    u8 *p1,dx;           
    u8 posx;    
    p1=(u8*)strstr((const char *)buf,"$GPVTG");                             
    posx=NMEA_Comma_Pos(p1,7);                              //得到地面速率
    if(posx!=0XFF)
    {
        gpsx->speed=NMEA_Str2num(p1+posx,&dx);
        if(dx<3)gpsx->speed*=NMEA_Pow(10,3-dx);             //確保擴大1000倍
    }

這個是GPVTG資訊解析,協議如下:
這裡寫圖片描述
到這裡,一些常用的,和我們需要的都解析出來了,
注意:這裡並不是每條協議都解析,解析的是我們需要什麼解析什麼,,當然在實際專案中要根據自己的需求解析。
GPS資訊我們是通過串列埠3中斷接收,將接收到的資料放在一個BUF中,

//通過判斷接收連續2個字元之間的時間差不大於10ms來決定是不是一次連續的資料.
//如果2個字元接收間隔超過10ms,則認為不是1次連續資料.也就是超過10ms沒有接收到
//任何資料,則表示此次接收完畢.
//接收到的資料狀態
//[15]:0,沒有接收到資料;1,接收到了一批資料.
//[14:0]:接收到的資料長度
vu16 USART3_RX_STA=0;       


void USART3_IRQHandler(void)
{
    u8 res;       
    if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)//接收到資料
    {    
        res =USART_ReceiveData(USART3);      
        if((USART3_RX_STA&(1<<15))==0)//接收完的一批資料,還沒有被處理,則不再接收其他資料
        {
            if(USART3_RX_STA<USART3_MAX_RECV_LEN)   //還可以接收資料  USART3_MAX_RECV_LEN = 600    最大接收快取位元組數
            {
                TIM_SetCounter(TIM7,0);//計數器清空                          //計數器清空
                if(USART3_RX_STA==0)                //使能定時器7的中斷
                {
                    TIM_Cmd(TIM7,ENABLE);//使能定時器7
                }
                USART3_RX_BUF[USART3_RX_STA++]=res; //記錄接收到的值    
            }else
            {
                USART3_RX_STA|=1<<15;               //強制標記接收完成
            }
        }
    }                                                            
}   


 

USART3_RX_STA是原子自己定義的一個最高位標誌位,當資料接收完成時,USART3_RX_STA|=1<<15
將最高位標誌位置1,
這裡便是定義瞭解析後資料儲存的結構體:

//GPS NMEA-0183協議重要引數結構體定義
//衛星資訊
__packed typedef struct  
{                                           
    u8 num;     //衛星編號
    u8 eledeg;  //衛星仰角
    u16 azideg; //衛星方位角
    u8 sn;      //信噪比          
}nmea_slmsg;  
//UTC時間資訊
__packed typedef struct  
{                                           
    u16 year;   //年份
    u8 month;   //月份
    u8 date;    //日期
    u8 hour;    //小時
    u8 min;     //分鐘
    u8 sec;     //秒鐘
}nmea_utc_time;        
//NMEA 0183 協議解析後資料存放結構體
__packed typedef struct  
{                                           
    u8 svnum;                   //可見衛星數
    nmea_slmsg slmsg[12];       //最多12顆衛星
    nmea_utc_time utc;          //UTC時間
    u32 latitude;               //緯度 分擴大100000倍,實際要除以100000
    u8 nshemi;                  //北緯/南緯,N:北緯;S:南緯                 
    u32 longitude;              //經度 分擴大100000倍,實際要除以100000
    u8 ewhemi;                  //東經/西經,E:東經;W:西經
    u8 gpssta;                  //GPS狀態:0,未定位;1,非差分定位;2,差分定位;6,正在估算.                  
    u8 posslnum;                //用於定位的衛星數,0~12.
    u8 possl[12];               //用於定位的衛星編號
    u8 fixmode;                 //定位型別:1,沒有定位;2,2D定位;3,3D定位
    u16 pdop;                   //位置精度因子 0~500,對應實際值0~50.0
    u16 hdop;                   //水平精度因子 0~500,對應實際值0~50.0
    u16 vdop;                   //垂直精度因子 0~500,對應實際值0~50.0

    int altitude;               //海拔高度,放大了10倍,實際除以10.單位:0.1m     
    u16 speed;                  //地面速率,放大了1000倍,實際除以10.單位:0.001公里/小時     
}nmea_msg;
 

到這裡,採用第一種方式解析協議已經分析完了,接下來就是採用NMEALIB庫解析協議,
瞭解了NMEA格式有之後,我們就可以編寫相應的解碼程式了,而程式設計師Tim ([email protected])提供了一個非常完善的NMEA解碼庫,在以下網址可以下載到:http://nmea.sourceforge.net/ ,直接使用該解碼庫,可以避免重複發明輪子的工作。在野火提供的GPS模組資料的“NMEA0183解碼庫原始碼”資料夾中也包含了該解碼庫的原始碼,野火提供的STM32程式就是使用該庫來解碼NMEA語句的。
該解碼庫目前最新為0.5.3版本,它使用純C語言編寫,支援windows、winCE 、UNIX平臺,支援解析GPGGA,GPGSA,GPGSV,GPRMC,GPVTG這五種語句(這五種語句已經提供足夠多的GPS資訊),解析得的GPS資料資訊以結構體儲存,附加了地理學相關功能,可支援導航等資料工作,除了解析NMEA語句,它還可以根據隨機數產生NMEA語句,方便模擬。

將nmealib庫中的src和include這兩個資料夾複製到工程,在新增進工程中,包含編譯的標頭檔案,結果如下:
這裡寫圖片描述
(這裡採用的是野火所提供的例程,感謝fire)
利用nmealib解析GPS模組的輸出結果大致可以分為三步,
第一步定義和初始化GPS資訊結構體和解析載體結構體,
第二步呼叫nmea_parse函式完成解析工作,
第三步釋放解析載體所佔用的記憶體空間。
具體的程式碼如下注釋中包含了程式碼的分析:

/**
  * @brief  nmea_decode_test 解碼GPS模組資訊
  * @param  無
  * @retval 無
                            利用nmealib解析GPS模組的輸出結果大致可以分為三步,
                                第一步定義和初始化GPS資訊結構體和解析載體結構體,
                                第二步呼叫nmea_parse函式完成解析工作,
                                第三步釋放解析載體所佔用的記憶體空間。
*/
int nmea_decode_test(void)
{

    nmeaINFO info;          //GPS解碼後得到的資訊
    nmeaPARSER parser;      //解碼時使用的資料結構
                                                        //nmeaPARSER是解析nmea所需要的一個結構。
    uint8_t new_parse=0;    //是否有新的解碼資料標誌

    nmeaTIME beiJingTime;    //北京時間

    /* 設定用於輸出除錯資訊的函式 */
    nmea_property()->trace_func = &trace;
    nmea_property()->error_func = &error;

    /* 初始化GPS資料結構 */
    nmea_zero_INFO(&info);/*對nmeaINFO這個結構中資料進行清零操作,
                                                            使用nmea_time_now函式對其中utc時間賦一個初值,初值就是當前的系統時間,
                                                            如果沒有從nmea中解析出時間資訊,那麼最後的結果就是你當前的系統時間。
                                                                而nmeaINFO中的sig、fix分別是定位狀態和定位型別
                                                        */
        nmea_parser_init(&parser);//nmeaPARSER結構做初始化,以nmea_parser_init和nmea_parser_destroy需要成對出現。

    while(1)
    {
      if(GPS_HalfTransferEnd)     /* 設定半傳輸完成標誌位
                                                                        接收到GPS_RBUFF_SIZE一半的資料 */
      {
        /* 進行nmea格式解碼 */
                /*
                呼叫nmea_parse函式對nmea語句進行解析
                原型:
                    int nmea_parse(      
                                nmeaPARSER *parser,  
                                const char *buff,
                                int buff_sz,  
                                nmeaINFO *info  
                            )  
                這個函式有四個引數,分別是nmeaPARSER指標,buff對應需要解析的nmea語句,buff_sz為nmea語句的長度,nmeaINFO指標
                */
        nmea_parse(&parser, (const char*)&gps_rbuff[0], HALF_GPS_RBUFF_SIZE, &info);
                                    //nmeaPARSER指標,需要解析的BUFF,      串列埠接收緩衝區一半512/2,nmeaINFO指標

        GPS_HalfTransferEnd = 0;   //清空標誌位
        new_parse = 1;             //設定解碼訊息標誌
      }
      else if(GPS_TransferEnd)    /* 接收到另一半資料 */
      {

        nmea_parse(&parser, (const char*)&gps_rbuff[HALF_GPS_RBUFF_SIZE], HALF_GPS_RBUFF_SIZE, &info);

        GPS_TransferEnd = 0;
        new_parse =1;
      }

      if(new_parse )                //有新的解碼訊息   
      {    
        /* 對解碼後的時間進行轉換,轉換成北京時間 */
        GMTconvert(&info.utc,&beiJingTime,8,1);

        /* 輸出解碼得到的資訊 */
        printf("\r\n時間%d,%d,%d,%d,%d,%d\r\n", beiJingTime.year+1900, beiJingTime.mon+1,beiJingTime.day,beiJingTime.hour,beiJingTime.min,beiJingTime.sec);
        printf("\r\n緯度:%f,經度%f\r\n",info.lat,info.lon);
        printf("\r\n正在使用的衛星:%d,可見衛星:%d",info.satinfo.inuse,info.satinfo.inview);
        printf("\r\n海拔高度:%f 米 ", info.elv);
        printf("\r\n速度:%f km/h ", info.speed);
        printf("\r\n航向:%f 度", info.direction);

        new_parse = 0;
      }

    }

    /* 釋放GPS資料結構 */
    // nmea_parser_destroy(&parser);


    //  return 0;
}

儲存解析後的結構體:
NMEA解碼庫良好的封裝特性使我們無需關注更深入的內部實現,只需要再瞭解一下nmeaINFO資料結構即可,所有GPS解碼得到的結果都儲存在這個結構中

typedef struct _nmeaTIME
{
    int     year;       /**< Years since 1900 */
    int     mon;        /**< Months since January - [0,11] */
    int     day;        /**< Day of the month - [1,31] */
    int     hour;       /**< Hours since midnight - [0,23] */
    int     min;        /**< Minutes after the hour - [0,59] */
    int     sec;        /**< Seconds after the minute - [0,59] */
    int     hsec;       /**< Hundredth part of second - [0,99] */

} nmeaTIME;

typedef struct _nmeaINFO
{
    int     smask;      /**< Mask specifying types of packages from which data have been obtained */

    nmeaTIME utc;       /**< UTC of position */

    int     sig;        /**< GPS quality indicator (0 = Invalid; 1 = Fix; 2 = Differential, 3 = Sensitive) */
    int     fix;        /**< Operating mode, used for navigation (1 = Fix not available; 2 = 2D; 3 = 3D) */

    double  PDOP;       /**< Position Dilution Of Precision */
    double  HDOP;       /**< Horizontal Dilution Of Precision */
    double  VDOP;       /**< Vertical Dilution Of Precision */

    double  lat;        /**< Latitude in NDEG - +/-[degree][min].[sec/60] */
    double  lon;        /**< Longitude in NDEG - +/-[degree][min].[sec/60] */
    double  elv;        /**< Antenna altitude above/below mean sea level (geoid) in meters */
    double  speed;      /**< Speed over the ground in kilometers/hour */
    double  direction;  /**< Track angle in degrees True */
    double  declination; /**< Magnetic variation degrees (Easterly var. subtracts from true course) */

    nmeaSATINFO satinfo; /**< Satellites information */

} nmeaINFO;

結構體的具體含義,
這裡寫圖片描述

typedef struct _nmeaPARSER
{
    void *top_node;
    void *end_node;
    unsigned char *buffer;
    int buff_size;
    int buff_use;

} nmeaPARSER;

可以看到,nmeaPARSER是一個連結串列,在解碼時,NMEA庫會把輸入的GPS原始資料壓入到nmeaPARSER結構的連結串列中,便於對資料管理及解碼。在使用該結構前,我們呼叫了nmea_parser_init函式分配動態空間,而解碼結束時,呼叫了nmea_parser_destroy函式釋放分配的空間

當然最重要的還是要:分配堆疊空間
由於NMEA解碼庫在進行解碼時需要動態分配較大的堆空間,所以我們需要在STM32的啟動檔案startup_stm32f10x_hd.s檔案中對堆空間進行修改,本工程中設定的堆空間大小設定為0x0000 1000,

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00001000

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

                PRESERVE8
                THUMB

 

當然,這裡也是通過串列埠接收資料儲存在這個陣列中,

/* DMA接收緩衝  */
uint8_t gps_rbuff[GPS_RBUFF_SIZE];//接收快取區512