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