做为现在的物联网行业,手持设备中,缺少不了的就是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
ires+=NMEA_Pow(10,ilen-1-i)*(buf[i]-'0');
}
if(flen>5)flen=5; //最多取5位小数
*dx=flen; //小数点位数
for(i=0;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
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
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
;
;
;
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