一,概述
GNSS芯片选用了ublox的 UBX-M8030 系列,有3个型号:
可以到官网去下载相关资料,文档还挺齐的:
https://www.u-blox.com/zh/product/ubx-m8030-series#tab-product-selection
比较重要的几个文档有:
ReceiverDescrProtSpec | 芯片通讯协议配置相关,软件开发较关心 |
DataSheet | 不用说了吧 |
HardwareIntegrationManual | 硬件设计详解 |
PowerManagement_AppNote | 电源及低功耗 |
GNSS-Antennas_AppNote | 天线相关 |
把选型相关和我比较关心的一些特性列一下:
最多可并发接收 3 个 GNSS(GPS、伽利略、GLONASS、北斗)
定位精度2米
-148 dBm 到 -167 dBm 灵敏度
冷启动26秒定位时间,热启动只要1.5s
1个串口,默认9600,8n1,本项目只用这个通讯口,其它的USB, I2C, SPI不关心
尺寸封装:QFN, 5x5
支持低电压,支持低功耗模式(这个对我们很重要)
简单来说,如果硬件没问题,天线调试可以,那样芯片会自动每秒发送一次信息,如果已定位成功,就有位置信息了,软件只要解析报文就可以。状态机如下图,acquisition我们叫搜星,tracking就是跟踪。
Ublox还专门提供了一个调试用的软件,叫u-center,官网可下载。安装到PC上,通过串口与芯片通讯,分析芯片发过来的位置信息报文,图文展示卫星等信息,也支持下发配置,非常方便。
二,硬件
先说明,本人软件工程师,硬件水平渣。如果之前没有研发过GPS类芯片的硬件工程师可能要好好把文档看懂才动手,尤其是天线部分,后面会专门分析该部分。芯片主体框图如下:
我们项目中原理图如下(天线部分后面提供):
2.1 电源
上图把几个重要的外围接口都列了出来,芯片有几个供电pin 脚,硬件要怎么供电取决于你的需求,上图的Main Supply实际上有两路电源输入,V_CORE, 和 V_DCDC,几个电源接口有着不同的功能。
V_BCKP,给RTC,RAM供电,用来保存星历,下次定位更快,功耗只有15uA
V_CORE, 搜星时功耗,只有GPS时 33.6 mA, GNSS 43.2 mA
V_DCDC_IN, 只有GPS时18.3 mA, GNSS 24.2mA
VDD_IO,通讯pin 脚供电
详细功能看官方文档,这里只截个图:
2.2 低功耗
低功耗与性能是个矛盾关系,各人根据需求去配置,例如硬件上有以下做法可以省电:
主电源1.4v供电
使用晶振(Crystal)而不是TCXO
用UART and DDC接口通讯,而不是USB, SPI
不使用SQI Flash(ROM only version)
无源天线(性能很受影响)
在软件上芯片有两种工作模式,连续模式(Continuous Mode)和省电模式(Power Save Mode,PSM)。连续模式就是使用全部性能,全部通道去搜星,星历都下载OK后进入跟踪引擎,这时功耗会降低。
如上图最开始阶段,芯片(acquisition engine)尽最大能力搜星,星历下载完成,得到初始位置信息后,芯片关闭acquisition engine进入跟踪状态。检测到新的卫星信号又重新启动acquisition engine,也就是上图的fix阶段。很明显,要省电最好是加长”Update Period”的时间,也就是说省电模式只在跟踪阶段。
省电模式又分两种模式:”Cyclic tracking” 和 “ON/OFF operation”,从下图可以看出,只有设置”Update Period”超过10s “ON/OFF operation”才有省电优势,这也会影响到性能,看用户怎么平衡了。
要想进入低功耗模式,要满足以下条件:
不要使用USB接口
有RTC,或者使用"single crystal"模式
使用GPS-only 模式
芯片配置步骤:
1. disable Glonass, 只要GPS。 UBX-CFG-GNSS
2. UBX-CFG-PM2 或者 UBX-CFG-PMS,一般推荐使用Cyclic tracking
3. UBX-CFG-RXM, set the power mode to "1 - Power Save Mode"
2.3 时钟
从前面系统框图可知,这个芯片有两路时钟输入,分别是TCXO(或晶振)和RTC时钟,参考框图如下:
主时钟是一定要有的,而且对搜星性能有影响,温漂要控制在范围内。可以根据需求,成本,性能分析是使用TCXO还是晶振,频率是26 MHz,” HardwareIntegrationManual”文档会有专门的分析,这个文档很细,把外围电路各种器件的参考物料都列了出来,买文档推荐的物料肯定没错了。
主时钟是供信号采集用的,RTC是用来备份和热保持,当芯片主电源意外掉电,芯片热启动时会用到,频率是32.768 kHz。芯片也支持单晶振(Single Crystal)模式,就是主晶振也可以作RTC用,这需要配置芯片。这样V_BCKP电源也会给主晶振供电,主电源掉的时候主晶振依然能工作。这么做能省物料成本,但是增加了功耗。
2.4 IO配置
UBX-M8030芯片有17个IO口,从PIO0到PIO16,都是由VDD_IO供电,而且电平也跟它一致。PIO0到PIO5可以连SQI Flash,SQI Flash的作用是升级芯片固件,保存配置,保存log,保留辅助定位信息。这几个pin脚也可以通过配置作为LDO输出的功能。我们项目没用SQI Flash,也没用LDO输出,所以都悬空了。
三,天线
天线部分我们硬件参考了” HardwareIntegrationManual”文档里面的最佳性能电路,如下图:
我们的原理图:
如上图所示,最左边BWGNSCNX16-6W是天线座,U11的MAX2659是一个LNA(Low Noise Amplifier,低噪声放大器)芯片,用来放大信号。U9的SAFEB1G57KE0F00R15是一个声表面波滤波器(SAW),用来滤波,特别是我们的项目中用到了lte cat.1芯片,怕信号会干扰。经过这些简单处理后信号被送到芯片,整个过程如下图:
要求硬件把外围电路处理好,然后重点就是天线的调试了。Ublox有专门文档介绍天线相关知识(GNSS-Antennas_AppNote),建议先看看。其实如果硬件工程师射频相关经验不多的话,最好是找天线厂帮忙,我们就因为这个浪费了一,两星期的时间。GPS的信号很弱,-100dBm以上的,天线比2.4G,433M之类的天线要难搞,还是要请专业人士保险。本人这一块也薄弱,只提供几个建议:
天线分陶瓷天线和FPC天线,增益强度,指向,体积都不一样,看产品需求选用。
天线对PCB有要求,例如净空,铺地,阻抗之类的,最好先让射频工程师评估。
串口输出的报文要能看懂,u-center最好会使用,能了解和推算出信号强弱。
产品外壳也很重要,天线的位置会有关系
最好还是有专业人员参与
四,u-center
u-center 软件的功能非常强大,我只使用了其中小部分功能,主界面如下:
上图左边部分用来查看串口输出数据流,配置界面等,这些界面都可以通过菜单栏的view子菜单打开。右边会把数据解析后的实时结果展示出来,包括当前搜到的星,信号强度,如果定位到的话会有位置,海拔,速度等信息。几个view的作用:
Text console 打印串口上报的报文,如上图左边部分。
Messages View 上报的报文通过NMEA协议解析出来,是字符格式,可以直接查看。下发配置报文一般用UBX协议,是二进制格式,而且有窗口显示要发送的报文内容。因为我下发的配置都是固定的,我一般在这里把要配置的内容测试OK,然后直接把二进制内容复制到代码去,省去组织报文的代码,特别是检验码是要计算出来的。
另外还有configuration view 和 statistic view用得多一点。建议调试时先把硬件调OK了再写功能代码。
五,软件
直接贴别人开源项目的代码,侵删:
ublox.h
/*
Andrea Toscano
U-BLOX NEO M8M Parser
*/
#ifndef UBLOX_H_INCLUDED
#define UBLOX_H_INCLUDED
#include <Arduino.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
class Ublox
{
public:
class Tokeniser
{
public:
Tokeniser(char* str, char token);
bool next(char* out, int len);
private:
char* str;
char token;
};
struct satellite
{
uint8_t prn;
int16_t elevation;
int16_t azimuth;
uint8_t snr; //signal to noise ratio
};
struct _datetime
{
uint8_t day, month, year;
uint8_t hours, minutes, seconds;
uint16_t millis;
bool valid; //1 = yes, 0 = no
};
enum _fixtype { FIX_TYPE_NONE, FIX_TYPE_GPS, FIX_TYPE_DIFF };
enum _fix { FIX_NONE = 1, FIX_2D, FIX_3D };
enum _op_mode { MODE_MANUAL, MODE_AUTOMATIC };
bool encode(char c);
float latitude, longitude, altitude, vert_speed;
int latlng_age, alt_age;
//these units are in hundredths
//so a speed of 5260 means 52.60km/h
uint16_t speed, course, knots;
int speed_age, course_age, knots_age;
_fixtype fixtype; //0 = no fix, 1 = satellite only, 2 = differential fix
int fixtype_age;
_fix fix;
int fix_age;
float pdop, hdop, vdop; //positional, horizontal and vertical dilution of precision
int dop_age;
int8_t sats_in_use;
int8_t sats_in_view;
satellite sats[12];
int sats_age;
_datetime datetime;
int time_age, date_age;
_op_mode op_mode;
private:
bool check_checksum();
uint8_t parse_hex(char h);
bool process_buf();
char buf[120];
uint8_t pos;
void read_gga();
void read_gsa();
void read_gsv();
void read_rmc();
void read_vtg();
};
//extern Ublox gps;
#endif // UBLOX_H_INCLUDED
ublox.c
#include "Ublox.h"
Ublox::Tokeniser::Tokeniser(char* _str, char _token)
{
str = _str;
token = _token;
}
bool Ublox::Tokeniser::next(char* out, int len)
{
uint8_t count = 0;
if(str[0] == 0)
return false;
while(true)
{
if(str[count] == '\0')
{
out[count] = '\0';
str = &str[count];
return true;
}
if(str[count] == token)
{
out[count] = '\0';
count++;
str = &str[count];
return true;
}
if(count < len)
out[count] = str[count];
count++;
}
return false;
}
bool Ublox::encode(char c)
{
buf[pos] = c;
pos++;
if(c == '\n') //linefeed
{
bool ret = process_buf();
memset(buf, '\0', 120);
pos = 0;
return ret;
}
if(pos >= 120) //avoid a buffer overrun
{
memset(buf, '\0', 120);
pos = 0;
}
return false;
}
bool Ublox::process_buf()
{
if(!check_checksum()) //if checksum is bad
{
return false; //return
}
//otherwise, what sort of message is it
if(strncmp(buf, "$GNGGA", 6) == 0)
{
read_gga();
}
if(strncmp(buf, "$GNGSA", 6) == 0)
{
read_gsa();
}
if(strncmp(buf, "$GPGSV", 6) == 0)
{
read_gsv();
}
if(strncmp(buf, "$GNRMC", 6) == 0)
{
read_rmc();
}
if(strncmp(buf, "$GNVTG", 6) == 0)
{
read_vtg();
}
return true;
}
// GNGGA
void Ublox::read_gga()
{
int counter = 0;
char token[20];
Tokeniser tok(buf, ',');
while(tok.next(token, 20))
{
switch(counter)
{
case 1: //time
{
float time = atof(token);
int hms = int(time);
datetime.millis = time - hms;
datetime.seconds = fmod(hms, 100);
hms /= 100;
datetime.minutes = fmod(hms, 100);
hms /= 100;
datetime.hours = hms;
time_age = millis();
}
break;
case 2: //latitude
{
float llat = atof(token);
int ilat = llat/100;
double mins = fmod(llat, 100);
latitude = ilat + (mins/60);
}
break;
case 3: //north/south
{
if(token[0] == 'S')
latitude = -latitude;
}
break;
case 4: //longitude
{
float llong = atof(token);
int ilat = llong/100;
double mins = fmod(llong, 100);
longitude = ilat + (mins/60);
}
break;
case 5: //east/west
{
if(token[0] == 'W')
longitude = -longitude;
latlng_age = millis();
}
break;
case 6:
{
fixtype = _fixtype(atoi(token));
}
break;
case 7:
{
sats_in_use = atoi(token);
}
break;
case 8:
{
hdop = atoi(token);
}
break;
case 9:
{
float new_alt = atof(token);
vert_speed = (new_alt - altitude)/((millis()-alt_age)/1000.0);
altitude = atof(token);
alt_age = millis();
}
break;
}
counter++;
}
}
void Ublox::read_gsa()
{
int counter = 0;
char token[20];
Tokeniser tok(buf, ',');
while(tok.next(token, 20))
{
switch(counter)
{
case 1: //operating mode
{
if(token[0] == 'A')
op_mode = MODE_AUTOMATIC;
if(token[0] == 'M')
op_mode = MODE_MANUAL;
}
break;
case 2:
{
fix = _fix(atoi(token));
fix_age = millis();
}
break;
case 14:
{
pdop = atof(token);
}
break;
case 15:
{
hdop = atof(token);
}
break;
case 16:
{
vdop = atof(token);
dop_age = millis();
}
break;
}
counter++;
}
}
void Ublox::read_gsv()
{
char token[20];
Tokeniser tok(buf, ',');
tok.next(token, 20);
tok.next(token, 20);
tok.next(token, 20);
int mn = atoi(token); //msg number
tok.next(token, 20);
sats_in_view = atoi(token); //number of sats
int8_t j = (mn-1) * 4;
int8_t i;
for(i = 0; i <= 3; i++)
{
tok.next(token, 20);
sats[j+i].prn = atoi(token);
tok.next(token, 20);
sats[j+i].elevation = atoi(token);
tok.next(token, 20);
sats[j+i].azimuth = atoi(token);
tok.next(token, 20);
sats[j+i].snr = atoi(token);
}
sats_age = millis();
}
void Ublox::read_rmc()
{
int counter = 0;
char token[20];
Tokeniser tok(buf, ',');
while(tok.next(token, 20))
{
switch(counter)
{
case 1: //time
{
float time = atof(token);
int hms = int(time);
datetime.millis = time - hms;
datetime.seconds = fmod(hms, 100);
hms /= 100;
datetime.minutes = fmod(hms, 100);
hms /= 100;
datetime.hours = hms;
time_age = millis();
}
break;
case 2:
{
if(token[0] == 'A')
datetime.valid = true;
if(token[0] == 'V')
datetime.valid = false;
}
break;
/*
case 3:
{
float llat = atof(token);
int ilat = llat/100;
double latmins = fmod(llat, 100);
latitude = ilat + (latmins/60);
}
break;
case 4:
{
if(token[0] == 'S')
latitude = -latitude;
}
break;
case 5:
{
float llong = atof(token);
float ilat = llong/100;
double lonmins = fmod(llong, 100);
longitude = ilat + (lonmins/60);
}
break;
case 6:
{
if(token[0] == 'W')
longitude = -longitude;
latlng_age = millis();
}
break;
*/
case 8:
{
course = atof(token);
course_age = millis();
}
break;
case 9:
{
uint32_t date = atoi(token);
datetime.year = fmod(date, 100);
date /= 100;
datetime.month = fmod(date, 100);
datetime.day = date / 100;
date_age = millis();
}
break;
}
counter++;
}
}
void Ublox::read_vtg()
{
int counter = 0;
char token[20];
Tokeniser tok(buf, ',');
while(tok.next(token, 20))
{
switch(counter)
{
case 1:
{
course = (atof(token)*100);
course_age = millis();
}
break;
case 5:
{
knots = (atof(token)*100);
knots_age = millis();
}
break;
case 7:
{
speed = (atof(token)*100);
speed_age = millis();
}
break;
}
counter++;
}
}
bool Ublox::check_checksum()
{
if (buf[strlen(buf)-5] == '*')
{
uint16_t sum = parse_hex(buf[strlen(buf)-4]) * 16;
sum += parse_hex(buf[strlen(buf)-3]);
for (uint8_t i=1; i < (strlen(buf)-5); i++)
sum ^= buf[i];
if (sum != 0)
return false;
return true;
}
return false;
}
uint8_t Ublox::parse_hex(char c)
{
if (c < '0')
return 0;
if (c <= '9')
return c - '0';
if (c < 'A')
return 0;
if (c <= 'F')
return (c - 'A')+10;
return 0;
}
代码是c++写的,单片机不好支持,我修改为c了。代码的使用较简单,GPS串口发过来的每一个字节都丢到encode函数去,如果检测到换行符'\n',会调用process_buf函数解析,解析后得到时间,经纬度等值。
end