在相当长的时间里,GNSS(全球卫星导航系统, Global Navigation Satellite System)是以美国的GPS(Global Positioning System, 全球定位系统)一家独大的。随着各国对定位系统的逐渐重视,都陆续推出自己的卫星导航系统,比如我国的北斗、俄罗斯的GLONASS还有欧盟的伽利略。
研究并建成一套拥有完全独立自主技术的卫星导航系统是相当不容易的,我国举全国之力为之奋斗了整整26年。今年的7月31号,北斗三号正式开通标志着中国可以摆脱GPS系统的掣肘,真正地走向全球,对中国来说意义非凡。
本篇文章里小编并不想详细的比较北斗和GPS系统之间的差异和优劣,网络上有很多专业的比较,感兴趣的同学可以自行搜索。我们更感兴趣的当然是作为民用级别的定位应用,怎么白嫖最方便和最精确?
比如说你想在自己DIY的项目中加入定位模块,一搜索你会发现市面上大多是多模(双模或者三模)定位模块,很少会有北斗单模系统。原因有以下几个:
虽然北斗也有民用频段,但普及程度没有GPS广,比如Arduino的北斗示例和资料都比较少。
其实北斗、GPS、伽利略和GLONASS的民用射频频段几乎是一样,模块厂家只需要一块射频接收器,后端稍作不同的处理就可以支持不同的系统并不需要增加额外的成本,所以厂家更愿意推广多模的模块,比如北斗/BDS和GPS双模。
民用级别的双模在精度上被证明的确优于单模。最主要的原因就是多一套系统,可用的卫星数就多了。下面三张图就是网友(知乎@侯燕青)实测的对比图(红色为实际轨迹)。
接下去我们就来实际操作一下,如何用Arduino读取北斗+GPS双模定位模块的数据。
我们使用的是型号为ATGM336H-5N模块,这是一款非常小尺寸的、支持全GNSS系统(北斗、GPS、GLONASS、伽利略、日本的QZSS以及卫星增强系统SBAS),ATGM336H-5N模块基于中科微AT6558单芯片,价格也很美丽。
首先把模块的引脚焊接上,并接好陶瓷天线。模块是通过UART跟Arduino交流的,如果对UART协议不了解,请参阅《Arduino常用的三种通信协议UART, I2C和SPI》。按照下图的对应关系,我们连接好模块和Arduino UNO,PPS可不接。
#include <SoftwareSerial.h> SoftwareSerial sSerial(12, 11); //RX,TXvoid setup(){ Serial.begin(115200); //Debug Serial sSerial.begin(9600); //Gps Serial }
void loop(){ while (sSerial.available() > 0) { byte gpsData = sSerial.read(); Serial.write(gpsData); }}
这里解释一下,因为我们需要创建一个软串口(SoftwareSerial)用于Arduino和模块之间的通信,而默认的硬串口Serial用于串口监视器查看具体的输出数据。
通上电,打开Arduino串口监视器,如果一切正常,你将会看到类似下面的一堆火星文。
这些火星文就是NMEA-0183协议的报文,大多数常见的GNSS接收机、GNSS数据处理软件、导航软件都遵守或者至少兼容这个协议。
数据里面我们看到三种数据类型 GN、GP、BD 分别代表双模模式、GPS 模式、北斗模式。那比如下面这条报文怎么理解呢?
$GNGGA,064954.000,3040.04684,N,10348.56480,E,1,09,1.7,491.2,M,0.0,M,,*71
根据NMEA-0183协议的规定,$XXGGA报文是关于时间、经纬度位置、解算状态、卫星颗数等相关信息。XX的不同代表模式的不同,比如$GPGGA表示单GPS模式,$BDGGA表示单北斗模式,$GNGGA表示多星联合定位。具体的含义我们对应查表就行了:
还有其他的报文都有对应的含义:
$XXGSA 显示的是当前的卫星信息:
$XXGSV 显示的是可见卫星的信息:
每条GSV语句最多可以显示4个可见卫星的信息,其他的卫星都会在下一条语句中输出显示。每种卫星系统都会单独显示,一般GPS有3条GSV报文,北斗有2条GSV报文。
$XXVTG包含地面速度信息:
$XXGLL包含基本的地理定位信息:
$XXRMC包含位置、速度、时间等最简定位信息:
如果我们不想把NMEA-0183协议的报文一股脑儿地输出,我们可以把之Arduino的代码再写复杂一点,把数据解析也做一下再输出:
#include <SoftwareSerial.h>SoftwareSerial sSerial(12, 11); //RX,TX
struct{ char GPS_Buffer[80]; bool isGetData; //是否获取到GPS数据 bool isParseData; //是否解析完成 char UTCTime[11]; //UTC时间 char latitude[11]; //纬度 char N_S[2]; //N/S char longitude[12]; //经度 char E_W[2]; //E/W bool isUsefull; //定位信息是否有效} Save_Data;
const unsigned int gpsRxBufferLength = 600;char gpsRxBuffer[gpsRxBufferLength];unsigned int ii = 0;
void setup() //初始化内容{ sSerial.begin(9600); //定义波特率9600 Serial.begin(115200); Serial.println("I Love Arduino"); Serial.println("Wating...");
Save_Data.isGetData = false; Save_Data.isParseData = false; Save_Data.isUsefull = false;}
void loop() //主循环{ gpsRead(); //获取GPS数据 parseGpsBuffer();//解析GPS数据 printGpsBuffer();//输出解析后的数据}
void errorLog(int num){ Serial.print("ERROR"); Serial.println(num); while (1) { digitalWrite(13, HIGH); delay(300); digitalWrite(13, LOW); delay(300); }}
void printGpsBuffer(){ if (Save_Data.isParseData) { Save_Data.isParseData = false; Serial.print("Save_Data.UTCTime = "); Serial.println(Save_Data.UTCTime);
if(Save_Data.isUsefull) { Save_Data.isUsefull = false; Serial.print("Save_Data.latitude = "); Serial.println(Save_Data.latitude); Serial.print("Save_Data.N_S = "); Serial.println(Save_Data.N_S); Serial.print("Save_Data.longitude = "); Serial.println(Save_Data.longitude); Serial.print("Save_Data.E_W = "); Serial.println(Save_Data.E_W); } else { Serial.println("GPS DATA is not usefull!"); } }}
void parseGpsBuffer(){ char *subString; char *subStringNext; if (Save_Data.isGetData) { Save_Data.isGetData = false; Serial.println("**************"); Serial.println(Save_Data.GPS_Buffer);
for (int i = 0 ; i <= 6 ; i++) { if (i == 0) { if ((subString = strstr(Save_Data.GPS_Buffer, ",")) == NULL) errorLog(1); //解析错误 } else { subString++; if ((subStringNext = strstr(subString, ",")) != NULL) { char usefullBuffer[2]; switch(i) { case 1:memcpy(Save_Data.UTCTime, subString, subStringNext - subString);break; //获取UTC时间 case 2:memcpy(usefullBuffer, subString, subStringNext - subString);break; //获取UTC时间 case 3:memcpy(Save_Data.latitude, subString, subStringNext - subString);break; //获取纬度信息 case 4:memcpy(Save_Data.N_S, subString, subStringNext - subString);break; //获取N/S case 5:memcpy(Save_Data.longitude, subString, subStringNext - subString);break; //获取纬度信息 case 6:memcpy(Save_Data.E_W, subString, subStringNext - subString);break; //获取E/W
default:break; }
subString = subStringNext; Save_Data.isParseData = true; if(usefullBuffer[0] == 'A') Save_Data.isUsefull = true; else if(usefullBuffer[0] == 'V') Save_Data.isUsefull = false;
} else { errorLog(2); //解析错误 } } } }}
void gpsRead() { while (sSerial.available()) { gpsRxBuffer[ii++] = sSerial.read(); if (ii == gpsRxBufferLength)clrGpsRxBuffer(); }
char* GPS_BufferHead; char* GPS_BufferTail; if ((GPS_BufferHead = strstr(gpsRxBuffer, "$GPRMC,")) != NULL || (GPS_BufferHead = strstr(gpsRxBuffer, "$GNRMC,")) != NULL ) { if (((GPS_BufferTail = strstr(GPS_BufferHead, "\r\n")) != NULL) && (GPS_BufferTail > GPS_BufferHead)) { memcpy(Save_Data.GPS_Buffer, GPS_BufferHead, GPS_BufferTail - GPS_BufferHead); Save_Data.isGetData = true;
clrGpsRxBuffer(); } }}
void clrGpsRxBuffer(void){ memset(gpsRxBuffer, 0, gpsRxBufferLength); //清空 ii = 0;}
这样编译上传后,我们就可以看见解析后的地理信息。是不是更加直观了?
这样我们就可以用双模定位模块获取更加精确定位数据,然后我们可以把这些数据再喂给一些GIS(地理信息系统)软件,或者可以利用python的一些库自己可视化数据。
在硬件方面,我们也可以增加电池和SD模块,让它长时间的保存定位信息,还可以添加GSM模块把数据通过移动网络传回服务器,这里我们就不深入了。