💬 前言
手头很多电子模块啥的,但多数都不会时常使用,每次要用到的时候再寻找一边文档什么的略麻烦了些,故作此系列简记之。
本文略览的是 GPS 传感器。GPS 传感器一般采用串口通信与上位机进行通信,而且不论是什么型号的传感器,基本都符合一个标准——NMEA 0183,所以说不论你买到的是什么模块,基本都可以适用本文的方法。
📡 NMEA 0183 协议关于 GPS 的部分简述
格式
$[语句],[数据]*[校验和]<CR><LF>
而语句的部分,由两部分组成:
[数据来源][句子类型]
数据来源而言有GP
,BD
,GN
等,分别代表 GPS,北斗,多系统组合。
语句类型将在下方分别介绍。
GGA——GPS定位信息
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh<CR><LF>
- UTC 时间,hhmmss(时分秒)格式
- 纬度 ddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 纬度半球 N(北半球)或 S(南半球)
- 经度 dddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 经度半球 E(东经)或 W(西经)
- GPS 状态:0=未定位,1=非差分定位,2=差分定位,6=正在估算
- 正在使用解算位置的卫星数量(00~12)(前面的 0 也将被传输)
- HDOP 水平精度因子(0.5~99.9)
- 相对于 WGS84 椭球面的高度(HAE, Height Above Ellipsoid)(-9999.9~99999.9)
- 地球椭球面(前者的基准)相对大地水准面(地球重力场等势面,近似的)的高度(N, Geoid Heigh)
- 差分时间(从最近一次接收到差分信号开始的秒数,如果不是差分定位将为空
- 差分站 ID 号 0000~1023(前面的 0 也将被传输,如果不是差分定位将为空)
比如这么一句语句:
$GPGGA,183457.00,4545.34205,N,00450.60258,E,1,05,2.13,164.1,M,47.4,M,,*58
意思就是 18:34:57 (UTC) 的时候,在北纬 45°45.34205′,东经 4°50.60258′,定位方式是非差分定位,使用到 5 颗卫星,水平精度因子为 2.13,HAE为 164.1 米,大地水准面高度为 47.4 米(模块根据内置超高阶重力场模型代入经纬度计算得出),经过计算可以得知本地的正常高(就8848代表的那个高度)为 $ H = HAE - N $,为 116.7 米,因为是非差分式定位,所以没有差分数据。收起你的好奇心,我直接告诉你这个地理位置是法国里昂第三区(ゝ∀・),下同。
GLL——定位地理信息
$GPGLL,<1>,<2>,<3>,<4>,<5>,<6>,<7>*hh<CR><LF>
- 纬度 ddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 纬度半球 N(北半球)或 S(南半球)
- 经度 dddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 经度半球 E(东经)或 W(西经)
- UTC 时间,hhmmss(时分秒)格式
- 定位状态,A=有效定位,V=无效定位
- 模式指示(仅 NMEA0183 3.00 版本以上输出,A=自主定位,D=差分,E=估算,N=数据无效,3.01 版本及以上新增了 M=手动输入,S=模拟模式)
比如这么一句语句:
$GPGLL,4545.34205,N,00450.60258,E,183457.00,A,A*6D
重复部分省略,这句话还告诉我们这是一个有效的定位数据,是通过自主定位得出的,说人话就是没用地面参考站修正误差进行差分定位。
RMC——推荐定位信息
$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*hh<CR><LF>
- UTC 时间,hhmmss(时分秒)格式
- 定位状态,A=有效定位,V=无效定位
- 纬度 ddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 纬度半球 N(北半球)或 S(南半球)
- 经度 dddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 经度半球 E(东经)或 W(西经)
- 地面速率(000.0~999.9 节,前面的 0 也将被传输)
- 地面航向(000.0~359.9 度,以真北为参考基准,前面的 0 也将被传输)
- UTC 日期,ddmmyy(日月年)格式
- 磁偏角(000.0~180.0 度,前面的 0 也将被传输)
- 磁偏角方向,E(东)或 W(西)
- 模式指示
比如这么一句语句:
$GPRMC,183457.00,A,4545.34205,N,00450.60258,E,1.159,,110524,,,A*7B
重复部分省略,换算过后地面速率约为 2.15 km/h(0.60 m/s),但其实我没动,所以这个属于误差。
VTG——地面速度信息
$GPVTG,<1>,T,<2>,M,<3>,N,<4>,K,<5>*hh<CR><LF>
- 以真北为参考基准的地面航向(000~359 度,前面的 0 也将被传输)
- 以磁北为参考基准的地面航向(000~359 度,前面的 0 也将被传输)
- 地面速率(000.0~999.9 节,前面的 0 也将被传输)
- 地面速率(0000.0~1851.8 公里/小时,前面的 0 也将被传输)
- 模式指示
比如这么一句语句:
$GPVTG,,T,,M,1.159,N,2.147,K,A*2F
可以发现,这里相较于上面一类语句还贴心地给你换算了一下。
GSA——当前卫星信息
`$GPGSA,<1>,<2>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<4>,<5>,<6>*hh
- 模式,M=手动,A=自动
- 定位类型,1=没有定位,2=2D 定位,3=3D 定位
- PRN 码(伪随机噪声码),即正在用于解算位置的卫星号(01~32,前面的 0 也将被传输)。
- PDOP 位置精度因子(0.5~99.9)
- HDOP 水平精度因子(0.5~99.9)
- VDOP 垂直精度因子(0.5~99.9)
比如这么一句语句:
$GPGSA,A,3,04,02,09,03,31,,,,,,,,3.14,2.13,2.31*0A
表示处于自动模式,自动选择了 3D 定位(在经纬度地基础上,还获取了高度,至少需要四颗卫星),正在用于定位地卫星编号为 04,02,09,03,31,水平精度略好于垂直精度,整体精度因子为 3.14。
GSV——可见卫星信息
$GPGSV,<1>,<2>,<3>,<4>,<5>,<6>,<7>,…<4>,<5>,<6>,<7>*hh<CR><LF>
- GSV 语句的总数
- 本句 GSV 的编号
- 可见卫星的总数(00~12,前面的 0 也将被传输)
- PRN 码(伪随机噪声码)(01~32,前面的 0 也将被传输)
- 卫星仰角(00~90 度,前面的 0 也将被传输)
- 卫星方位角(000~359 度,前面的 0 也将被传输)
- 信噪比(00~99dB,没有跟踪到卫星时为空,前面的 0 也将被传输)
注:4~7将按照每颗卫星进行循环显示,每条 GSV 语句最多可以显示 4 颗卫星的信息。其他卫星信息将在下一序列的 NMEA0183 语句中输出。
比如这么一组语句:
$GPGSV,3,1,12,02,18,148,34,03,52,073,18,04,79,096,13,06,44,309,09*72
$GPGSV,3,2,12,07,01,177,,09,52,218,32,11,06,313,,17,30,231,*7A
$GPGSV,3,3,12,19,37,261,20,21,05,145,,28,04,028,07,31,22,050,11*73
总共有三条语句,总共12颗卫星可见,以语句中第一颗卫星为例,卫星编号为 02,高度角为 18°,方位角为 148°,信噪比为 34 dB。
精度因子(DOP)
这里的精度因子是对卫星相对于接收机地几何分布的一个定量描述,这个几何分布如何影响定位精度呢?如图:
根据这个值可以区分几个等级:
DOP值 | 等级 | 含义 |
---|---|---|
1 | 理想 | 置信度水平高 |
2-4 | 优秀 | 置信度水平满足所有的应用需求 |
4-6 | 良好 | 置信度水平满足高精度应用需求 |
6-8 | 中等 | 置信度水平满足大部分应用需求 |
8-20 | 一般 | 置信度水平较低,应评估应用风险 |
20-50 | 很差 | 置信度水平很差,基本无法满足应用需求 |
那么如何通过精度因子推算出精度呢?我们有:$$
误差=\text{DOP}\times基准误差
$$问题就出在这个基准误差,我也不造啊,没查到啊◑﹏◐。不过万能的 ChatGPT 4 告诉我民用设备一般取值 5~10 米。
🧑💻 程序设计
上面说了那么多,是要从零开始造轮子吗?那指定不是,Introducing TinyGPS++!
这是一个可以用来解析 GPS 数据的库,我们只需要调用它就可以了。具体的文档可以参考 GitHub 仓库里贴的链接
这里简述一下通用的流程,程序启动串口(或者 SoftwareSerial),然后定期喂数据进TinyGPSPlus
类的对象,然后从这个对象里面取数据。也可以自己定义TinyGPSCustom
类,来读取并没有被这个库覆盖带的数据(比如卫星数据),这个可以参考官方给出的 Examples。此外,这个库还提供了计算两地大圆距离以及航向的函数。
一个聪明的喂数据的方法是重载(或者新定义)delay 函数。
现在给出一个极简版例程:
#include <TinyGPSPlus.h>
#include <SoftwareSerial.h>
static const int RXPin = 13, TXPin = 14;
static const uint32_t GPSBaud = 9600;
static const double ORIENTALPEARLTOWER_LAT = 121.50566572291831;
static const double ORIENTALPEARLTOWER_LON = 31.244890217260576;
SoftwareSerial ss(RXPin, TXPin);
TinyGPSPlus gps;
static void smartDelay(unsigned long ms);
void setup()
{
Serial.begin(115200);
ss.begin(GPSBaud);
}
void loop()
{
Serial.println("----------------------------------------");
Serial.println(String(gps.date.year()) + '/' +
String(gps.date.month()) + '/' +
String(gps.date.day()) + ' ' +
String(gps.time.hour()) + ':' +
String(gps.time.minute()) + ':' +
String(gps.time.second()) + " UTC");
Serial.println("Latitude: " + String(gps.location.lat()));
Serial.println("Longitude: " + String(gps.location.lng()));
Serial.println("To Oriental Pearl Tower (km): " +
String(TinyGPSPlus::distanceBetween(
gps.location.lat(),
gps.location.lng(),
ORIENTALPEARLTOWER_LAT,
ORIENTALPEARLTOWER_LON) /
1000.0));
smartDelay(1000);
if (millis() > 5000 && gps.charsProcessed() < 10)
Serial.println(F("No GPS data received: check wiring"));
}
static void smartDelay(unsigned long ms)
{
unsigned long start = millis();
do
{
while (ss.available())
gps.encode(ss.read());
} while (millis() - start < ms);
}
Comments NOTHING