4. NMEA-0183协议详解:GGA、RMC、GSA、GSV、VTG语句格式、校验和计算、数据解析
做GNSS定位开发,NMEA-0183协议是绕不开的坎。说白了,它就是GPS模块和主控芯片之间的“通用语言”。我刚开始接触这行时,对着串口打印的一堆乱码发懵——后来才发现,只要掌握了几个核心语句,解析定位数据其实很简单。
今天我们就来拆解最常用的5条语句:GGA、RMC、GSA、GSV、VTG。我会把每条语句的格式、校验和计算方法、解析技巧都讲透。
核心要点:NMEA-0183语句以$开头,以<CR><LF>结尾。字段之间用逗号分隔。校验和位于*之后,是$与*之间所有字符的异或值。
4.1 语句通用结构
先看一个完整的NMEA语句长什么样:
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
拆开来看:
- $:语句起始符
- GP:Talker ID,表示GPS系统(GLONASS用GL,北斗用BD)
- GGA:语句类型,这里是全球定位系统定位数据
- 数据字段:逗号分隔,每个语句的字段数量和含义不同
- *:校验和前缀
- 47:校验和,十六进制值
嗯,这里要注意:校验和是可选字段,但实际项目中几乎都会带。我建议你在解析时一定要校验,否则数据错了都不知道。
4.2 GGA语句——定位核心数据
GGA(Global Positioning System Fix Data)是最常用的语句。它包含了时间、经纬度、定位质量、卫星数、海拔高度等关键信息。
格式:
$GPGGA,hhmmss.ss,ddmm.mmmm,a,dddmm.mmmm,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh
字段详解:
| 字段序号 | 名称 | 说明 | 示例 |
|---|---|---|---|
| 1 | UTC时间 | hhmmss.ss格式 | 123519.00 |
| 2 | 纬度 | ddmm.mmmm,度分格式 | 4807.038 |
| 3 | 纬度方向 | N或S | N |
| 4 | 经度 | dddmm.mmmm,度分格式 | 01131.000 |
| 5 | 经度方向 | E或W | E |
| 6 | 定位质量 | 0=无效,1=单点定位,2=差分定位 | 1 |
| 7 | 使用卫星数 | 00-12 | 08 |
| 8 | 水平精度因子 | HDOP,越小越好 | 0.9 |
| 9 | 海拔高度 | 单位:米 | 545.4 |
| 10 | 海拔单位 | 固定为M | M |
| 11 | 大地水准面高度 | 单位:米 | 46.9 |
| 12 | 差分GPS数据龄期 | 无差分时为空白 | |
| 13 | 差分参考站ID | 0000-1023 |
我的经验:解析GGA时,我习惯先检查定位质量字段(第6字段)。如果为0,后面的数据直接丢弃,不用再解析了。这样可以避免无效数据污染业务逻辑。
4.3 RMC语句——推荐最小数据
RMC(Recommended Minimum Specific GNSS Data)是另一个核心语句。它包含了时间、日期、经纬度、速度、航向等信息。很多应用只解析RMC就够了。
格式:
$GPRMC,hhmmss.ss,A,ddmm.mmmm,a,dddmm.mmmm,a,x.x,x.x,ddmmyy,x.x,a*hh
字段详解:
| 字段序号 | 名称 | 说明 | 示例 |
|---|---|---|---|
| 1 | UTC时间 | hhmmss.ss | 123519.00 |
| 2 | 定位状态 | A=有效,V=无效 | A |
| 3 | 纬度 | ddmm.mmmm | 4807.038 |
| 4 | 纬度方向 | N或S | N |
| 5 | 经度 | dddmm.mmmm | 01131.000 |
| 6 | 经度方向 | E或W | E |
| 7 | 对地速度 | 单位:节 | 0.5 |
| 8 | 对地航向 | 单位:度,真北为参考 | 180.2 |
| 9 | UTC日期 | ddmmyy | 150324 |
| 10 | 磁偏角 | 单位:度 | 0.0 |
| 11 | 磁偏角方向 | E或W | E |
| 12 | 定位模式 | A=自主,D=差分,N=无效 | A |
避坑指南:我曾经在项目里只检查了GGA的定位质量,没检查RMC的定位状态。结果有一次模块输出GGA质量=1(有效),但RMC状态却是V(无效)。后来查手册才知道,RMC的A/V状态更严格——它要求定位解算收敛到一定精度才算有效。所以我现在两个都检查,取交集。
4.4 GSA语句——精度因子与卫星使用情况
GSA(GNSS DOP and Active Satellites)语句告诉你当前使用了哪些卫星,以及各种精度因子(DOP)的值。DOP值越小,定位精度越高。
格式:
$GPGSA,a,x,xx,xx,xx,...,xx,x.x,x.x,x.x*hh
字段详解:
| 字段序号 | 名称 | 说明 | 示例 |
|---|---|---|---|
| 1 | 定位模式 | M=手动,A=自动 | A |
| 2 | 定位类型 | 1=无定位,2=2D,3=3D | 3 |
| 3-14 | 使用卫星PRN号 | 最多12颗,空白表示未使用 | 01,02,03,... |
| 15 | PDOP | 位置精度因子 | 1.8 |
| 16 | HDOP | 水平精度因子 | 0.9 |
| 17 | VDOP | 垂直精度因子 | 1.5 |
你想想看,PDOP、HDOP、VDOP这三个值怎么用?我一般这样判断:
- PDOP < 2:优秀,可以用于高精度测量
- PDOP 2-4:良好,普通导航够用
- PDOP 4-6:一般,可能有些误差
- PDOP > 6:较差,建议等待或换位置
4.5 GSV语句——卫星可见信息
GSV(GNSS Satellites in View)语句告诉你天空中有哪些卫星可见,以及它们的仰角、方位角和信噪比。这个语句通常有多条,因为一条语句最多只能报4颗卫星。
格式:
$GPGSV,x,x,xx,xx,xx,xxx,xx,...,xx,xx,xxx,xx*hh
字段详解:
| 字段序号 | 名称 | 说明 | 示例 |
|---|---|---|---|
| 1 | 总条数 | GSV语句的总条数(1-3) | 2 |
| 2 | 当前条序号 | 当前是第几条(1-3) | 1 |
| 3 | 可见卫星总数 | 所有系统合计 | 10 |
| 4-7 | 卫星1信息 | PRN号,仰角,方位角,信噪比 | 01,45,180,35 |
| 8-11 | 卫星2信息 | 同上 | 02,30,200,40 |
| 12-15 | 卫星3信息 | 同上 | 03,60,90,42 |
| 16-19 | 卫星4信息 | 同上 | 04,10,300,28 |
我的习惯:解析GSV时,我会把信噪比低于30dBHz的卫星标记为“弱信号”,在后续定位解算中降低它们的权重。这个技巧在弱信号环境下特别有用。
4.6 VTG语句——对地速度与航向
VTG(Course Over Ground and Ground Speed)语句很简单,就是告诉你当前的运动速度和方向。
格式:
$GPVTG,x.x,T,x.x,M,x.x,N,x.x,K,a*hh
字段详解:
| 字段序号 | 名称 | 说明 | 示例 |
|---|---|---|---|
| 1 | 真北航向 | 单位:度 | 180.2 |
| 2 | 真北标识 | 固定为T | T |
| 3 | 磁北航向 | 单位:度 | 179.8 |
| 4 | 磁北标识 | 固定为M | M |
| 5 | 对地速度(节) | 单位:节 | 0.5 |
| 6 | 速度单位(节) | 固定为N | N |
| 7 | 对地速度(km/h) | 单位:公里/小时 | 0.9 |
| 8 | 速度单位(km/h) | 固定为K | K |
| 9 | 定位模式 | A=自主,D=差分,N=无效 | A |
4.7 校验和计算——不能跳过的一步
校验和是NMEA协议的“安全锁”。我见过太多人图省事跳过校验,结果被错误数据坑惨了。
计算方法:
- 从
$后面的第一个字符开始,到*之前结束 - 对每个字符做异或(XOR)运算
- 结果转换为两位十六进制大写
代码示例(C语言):
uint8_t calc_nmea_checksum(const char *nmea_str)
{
uint8_t checksum = 0;
const char *p = nmea_str;
// 跳过开头的 '$'
if (*p == '$') p++;
// 逐字符异或,直到 '*' 或字符串结束
while (*p != '\0' && *p != '*')
{
checksum ^= (uint8_t)(*p);
p++;
}
return checksum;
}
// 使用示例
int verify_nmea(const char *nmea_str)
{
// 找到 '*' 位置
const char *star = strchr(nmea_str, '*');
if (star == NULL) return 0; // 没有校验和
// 提取校验和字段
uint8_t expected = (uint8_t)strtol(star + 1, NULL, 16);
uint8_t calculated = calc_nmea_checksum(nmea_str);
return (expected == calculated) ? 1 : 0;
}
避坑指南:我曾经在一个项目中,发现某些模块输出的校验和字段是*00。一开始以为是模块坏了,后来查手册才知道——有些模块在未定位时,校验和字段会填充00。所以校验失败时,别急着报错,先看看定位状态是否有效。
4.8 数据解析实战——从字符串到结构体
解析NMEA语句,说白了就是字符串分割+类型转换。我一般用strtok或sscanf来做。下面是一个完整的GGA解析示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
uint8_t hour;
uint8_t minute;
uint8_t second;
double latitude; // 十进制度
double longitude; // 十进制度
uint8_t fix_quality;
uint8_t sat_count;
float hdop;
float altitude;
} GGA_t;
int parse_gga(const char *nmea_str, GGA_t *out)
{
char buf[128];
char *token;
int field = 0;
strncpy(buf, nmea_str, sizeof(buf) - 1);
token = strtok(buf, ",");
while (token != NULL)
{
switch (field)
{
case 1: // UTC时间
sscanf(token, "%2hhu%2hhu%2hhu",
&out->hour, &out->minute, &out->second);
break;
case 2: // 纬度 ddmm.mmmm
{
double ddmm = atof(token);
int degrees = (int)(ddmm / 100);
double minutes = ddmm - degrees * 100;
out->latitude = degrees + minutes / 60.0;
break;
}
case 3: // 纬度方向
if (token[0] == 'S') out->latitude = -out->latitude;
break;
case 4: // 经度 dddmm.mmmm
{
double ddmm = atof(token);
int degrees = (int)(ddmm / 100);
double minutes = ddmm - degrees * 100;
out->longitude = degrees + minutes / 60.0;
break;
}
case 5: // 经度方向
if (token[0] == 'W') out->longitude = -out->longitude;
break;
case 6: // 定位质量
out->fix_quality = atoi(token);
break;
case 7: // 卫星数
out->sat_count = atoi(token);
break;
case 8: // HDOP
out->hdop = atof(token);
break;
case 9: // 海拔
out->altitude = atof(token);
break;
}
token = strtok(NULL, ",");
field++;
}
return (out->fix_quality > 0) ? 1 : 0;
}
我的建议:解析经纬度时,记得把度分格式转成十进制度。公式很简单:十进制度 = 度 + 分/60。北纬和东经为正,南纬和西经为负。这样后续计算距离、面积都方便。
4.9 知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作快速参考:
4.10 总结与建议
好了,NMEA-0183的5条核心语句就讲完了。我个人觉得,掌握这些就够应付90%的定位项目了。最后给你几个实用建议:
- 优先用RMC:如果只需要位置和速度,RMC一条语句就够了,解析量最小
- GGA+GSA组合:需要评估定位质量时,GGA看定位类型,GSA看精度因子
- GSV用于诊断:定位效果不好时,看GSV能快速判断是卫星数量不够还是信号太弱
- 校验和必须做:别偷懒,串口通信随时可能丢字节,校验和是最后一道防线