NMEA 0183 协议处理与 C++ 开源库选型指南
一、NMEA 0183 协议概述
1.1 协议简介
NMEA 0183 是美国国家海洋电子协会制定的航海电子设备间通信的串口数据协议标准,是目前全球定位系统(GPS)接收机最常用的数据输出格式。
1.2 串口通信参数
| 参数 | 标准值 | 备注 |
|---|---|---|
| 波特率 | 4800 bps | 部分高速设备为 38400 bps |
| 数据位 | 8 bit | 固定 |
| 起始位 | 1 bit | 固定 |
| 停止位 | 1 bit | 固定 |
| 奇偶校验 | 无 | 固定 |
| 数据格式 | ASCII 字符 | 可读文本 |
1.3 帧格式结构
每条 NMEA 语句遵循以下通用格式:
$aaacc,ddd,ddd,...,ddd*hh<CR><LF>
格式说明:
| 组成部分 | 长度 | 说明 | 示例 |
|---|---|---|---|
$ | 1 字符 | 语句起始标志 | $ |
aaa | 3 字符 | 谈话器 ID(设备标识) | GP |
cc | 2 字符 | 语句类型(2-3 字符) | RMC |
ddd... | 可变 | 数据字段,逗号分隔 | 024813.640,A,... |
* | 1 字符 | 校验和前缀 | * |
hh | 2 字符 | 校验和(十六进制) | 50 |
<CR><LF> | 2 字符 | 结束符(回车+换行) | 0x0D 0x0A |
1.4 谈话器 ID(源标识)
| 标识 | 含义 | 所属系统 |
|---|---|---|
GP | GPS | 全球定位系统(美国) |
GL | GLONASS | 格洛纳斯(俄罗斯) |
GA | Galileo | 伽利略(欧盟) |
BD | BeiDou | 北斗(中国) |
GN | GNSS | 多系统组合 |
1.5 校验和算法
校验和是对 $ 和 * 之间的所有字符进行异或(XOR)运算得到的结果:
uint8_t calculateChecksum(const char* data) {
uint8_t checksum = 0;
for (int i = 1; data[i] != '*' && data[i] != '\0'; i++) {
checksum ^= (uint8_t)data[i];
}
return checksum;
}
二、常用 NMEA 语句详解
2.1 $GPRMC —— 推荐最小定位信息(最常用)
用途:提供最基本的时间、位置、速度、航向和日期信息。
格式:
$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*hh
字段详解:
| 字段 | 名称 | 格式/单位 | 示例 | 说明 |
|---|---|---|---|---|
| 0 | 语句 ID | $GPRMC | $GPRMC | 固定 |
| 1 | UTC 时间 | hhmmss.sss | 024813.640 | 02:48:13.640 |
| 2 | 定位状态 | A/V | A | A=有效,V=无效 |
| 3 | 纬度 | ddmm.mmmm | 3158.4608 | 31°58.4608′ |
| 4 | 北纬/南纬 | N/S | N | |
| 5 | 经度 | dddmm.mmmm | 11848.3737 | 118°48.3737′ |
| 6 | 东经/西经 | E/W | E | |
| 7 | 对地速度 | 节 | 10.05 | 1 节 = 1.852 km/h |
| 8 | 对地航向 | 度 | 324.27 | 真北参考 |
| 9 | UTC 日期 | ddmmyy | 150706 | 2006 年 7 月 15 日 |
| 10 | 磁偏角 | 度 | 可选 | |
| 11 | 磁偏角方向 | E/W | 可选 | |
| 12 | 模式指示 | A/D/E/N | A | A=自主,D=差分,E=估算,N=无效 |
2.2 $GPGGA —— GPS 定位信息
用途:提供详细的定位信息,包括海拔、卫星数量、精度因子等。
格式:
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh
字段详解:
| 字段 | 名称 | 说明 |
|---|---|---|
| 0 | 语句 ID | $GPGGA |
| 1 | UTC 时间 | hhmmss.sss 格式 |
| 2 | 纬度 | ddmm.mmmm 格式 |
| 3 | 纬度方向 | N/S |
| 4 | 经度 | dddmm.mmmm 格式 |
| 5 | 经度方向 | E/W |
| 6 | GPS 状态 | 0=未定位,1=单点定位,2=差分定位 |
| 7 | 使用卫星数 | 00-12 |
| 8 | HDOP | 水平精度因子 |
| 9 | 海拔高度 | 米 |
| 10 | 大地水准面高度 | 米 |
| 11 | 差分时间 | 秒 |
| 12 | 差分站 ID | 0000-1023 |
2.3 $GPGSA —— 精度因子与卫星信息
用途:提供使用的卫星编号和精度因子(PDOP/HDOP/VDOP)。
格式:
$GPGSA,<1>,<2>,<3>,<3>,...,<3>,<4>,<5>,<6>*hh
| 字段 | 名称 | 说明 |
|---|---|---|
| 0 | 语句 ID | $GPGSA |
| 1 | 模式 | M=手动,A=自动 |
| 2 | 定位类型 | 1=无,2=2D,3=3D |
| 3 | PRN 卫星号 | 12 个字段,用于定位的卫星编号 |
| 4 | PDOP | 位置精度因子 |
| 5 | HDOP | 水平精度因子 |
| 6 | VDOP | 垂直精度因子 |
2.4 $GPGSV —— 可见卫星信息
用途:提供天空中的可见卫星信息(仰角、方位角、信噪比)。
格式:
$GPGSV,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<4>,<5>,<6>,<7>,...*hh
| 字段 | 名称 | 说明 |
|---|---|---|
| 0 | 语句 ID | $GPGSV |
| 1 | 总语句数 | 当前 GSV 语句总数 |
| 2 | 当前语句号 | 1-总语句数 |
| 3 | 卫星总数 | 00-12(可见卫星数) |
| 4 | PRN 号 | 卫星编号 01-32 |
| 5 | 仰角 | 00-90 度 |
| 6 | 方位角 | 000-359 度 |
| 7 | 信噪比 | 00-99 dB |
注意:每条 GSV 语句最多输出 4 颗卫星,超过的卫星在后续语句中输出。
2.5 $GPVTG —— 地面速度信息
用途:提供对地速度和航向(真北/磁北两种参考)。
格式:
$GPVTG,<1>,T,<2>,M,<3>,N,<4>,K,<5>*hh
| 字段 | 名称 | 说明 |
|---|---|---|
| 0 | 语句 ID | $GPVTG |
| 1 | 真北航向 | 000-359 度 |
| 2 | 磁北航向 | 000-359 度 |
| 3 | 速度 | 节 |
| 4 | 速度 | 公里/小时 |
| 5 | 模式指示 | A/D/E/N |
2.6 $GPGLL —— 地理经纬度信息
用途:仅提供地理位置信息(无速度/航向)。
格式:
$GPGLL,<1>,<2>,<3>,<4>,<5>,<6>,<7>*hh
| 字段 | 名称 | 说明 |
|---|---|---|
| 0 | 语句 ID | $GPGLL |
| 1 | 纬度 | ddmm.mmmm |
| 2 | 纬度方向 | N/S |
| 3 | 经度 | dddmm.mmmm |
| 4 | 经度方向 | E/W |
| 5 | UTC 时间 | hhmmss.sss |
| 6 | 状态 | A/V |
| 7 | 模式指示 | A/D/E/N |
三、坐标转换方法
NMEA 0183 协议中的经纬度采用度分(DDMM.MMMM)格式,使用时通常需要转换为十进制度(DD.DDDDDD)格式。
3.1 度分格式转十进制度
十进制度 = 度 + 分 / 60
示例:
- 输入纬度:
3158.4608(表示 31 度 58.4608 分) - 转换过程:
31 + 58.4608 / 60 = 31 + 0.9743467 = 31.9743467°
3.2 C++ 实现
double DM2DD(double dm) {
int degrees = static_cast<int>(dm / 100);
double minutes = dm - degrees * 100;
return degrees + minutes / 60.0;
}
// 带方向的处理(南纬/西经为负数)
double DM2DDWithDirection(double dm, char direction) {
double result = DM2DD(dm);
if (direction == 'S' || direction == 'W') {
result = -result;
}
return result;
}
四、C++ 开源库选型对比
4.1 总体对比表
| 特性 | libnmea | minmea | marnav |
|---|---|---|---|
| 语言 | C (C99) | C (C99) | C++11/17 |
| 代码体积 | 中等(完整库) | 极小(2 个文件) | 较大(完整框架) |
| 内存管理 | 传统手动管理 | 无动态分配 | RAII 现代 C++ |
| 支持语句数 | 中等(~15 种) | 较少(~10 种) | 丰富(80+ 种) |
| 是否支持生成语句 | 仅解析 | 仅解析 | 支持 |
| 特殊功能 | ESP32 官方组件 | 无浮点运算版本 | 航海计算、AIS |
| 许可证 | MIT | MIT | BSD 3-Clause |
| 最佳环境 | 嵌入式 Linux/ESP32 | 单片机/STM32/Arduino | 桌面/服务器 |
4.2 libnmea 详解
GitHub 仓库:https://github.com/janantes/libnmea(推荐)或相关衍生项目
核心特点:
- 纯 C 实现,跨平台支持好
- 模块化架构,可动态加载语句插件
- Espressif(ESP32)官方提供组件支持
- 语句结构体设计清晰,易于使用
使用示例:
#include <libnmea/nmea.h>
// 创建解析器
nmea_s *parser = nmea_new();
// 解析语句
char sentence[] = "$GPRMC,024813.640,A,3158.4608,N,11848.3737,E,10.05,324.27,150706,,,A*50";
nmea_info_s info;
if (nmea_parse(parser, sentence, &info)) {
printf("纬度: %f, 经度: %f\n", info.latitude, info.longitude);
printf("速度: %f 节\n", info.speed);
printf("航向: %f 度\n", info.course);
}
// 清理
nmea_free(parser);
适用场景:
- 嵌入式 Linux 设备
- ESP32/ESP8266 物联网项目
- 需要支持多种 NMEA 语句的 C 语言项目
4.3 minmea 详解
GitHub 仓库:https://github.com/kosma/minmea
核心特点:
- 极致轻量:仅两个文件(minmea.h / minmea.c)
- 零动态内存分配:所有内存栈上静态分配
- 可选无浮点模式:适合无 FPU 的低端 MCU
- 高可定制性:编译时宏配置
使用示例:
#include "minmea.h"
char sentence[] = "$GPRMC,024813.640,A,3158.4608,N,11848.3737,E,10.05,324.27,150706,,,A*50";
struct minmea_sentence_rmc rmc;
if (minmea_parse_rmc(&rmc, sentence)) {
printf("纬度: %d°%d'%d\" %c\n",
rmc.latitude.degrees,
rmc.latitude.minutes,
rmc.latitude.seconds,
rmc.latitude_direction == MINMEA_LAT_NORTH ? 'N' : 'S');
printf("速度: %d 节\n", rmc.speed.value);
}
适用场景:
- 低端单片机(STM32F1、AVR、PIC 等)
- 内存受限的 RTOS 环境
- 只需要核心 NMEA 语句的项目
4.4 marnav 详解
GitHub 仓库:https://github.com/mariokonrad/marnav
核心特点:
- 现代 C++ 接口:充分利用 C++11/17 特性
- 功能最全面:支持 80+ 种 NMEA 语句
- 双向支持:既解析也生成 NMEA 语句
- 内置航海计算:CPA、TCPA、恒向线计算等
- AIS 支持:可解析船舶自动识别系统数据
使用示例:
#include <marnav/nmea/io.hpp>
#include <marnav/nmea/rmc.hpp>
int main() {
std::string raw = "$GPRMC,024813.640,A,3158.4608,N,11848.3737,E,10.05,324.27,150706,,,A*50";
// 解析
auto sentence = marnav::nmea::make_sentence(raw);
auto rmc = marnav::nmea::sentence_cast<marnav::nmea::rmc>(sentence);
if (rmc) {
std::cout << "时间: " << rmc->get_time_utc() << std::endl;
std::cout << "纬度: " << rmc->get_latitude() << std::endl;
std::cout << "经度: " << rmc->get_longitude() << std::endl;
std::cout << "速度: " << rmc->get_sog_knots() << " kn" << std::endl;
}
return 0;
}
CMake 集成:
include(FetchContent)
FetchContent_Declare(
marnav
GIT_REPOSITORY https://github.com/mariokonrad/marnav.git
)
FetchContent_MakeAvailable(marnav)
target_link_libraries(your_project PRIVATE marnav::marnav)
适用场景:
- 桌面/服务器端应用程序(Windows/Linux/macOS)
- 船舶导航系统
- 电子海图显示与信息系统(ECDIS)模拟器
- 需要 AIS 解析的专业项目
五、快速选型建议
| 你的情况 | 推荐库 | 理由 |
|---|---|---|
| 使用 C++11/17 开发桌面/服务器应用 | marnav | 现代 C++ 接口,功能最全,开发效率高 |
| 使用 C 语言,运行在嵌入式 Linux | libnmea | 轻量、模块化、生态好 |
| 开发 ESP32 项目 | libnmea | 官方组件支持,安装方便 |
| 在低端单片机(STM32F1、Arduino)上开发 | minmea | 无动态内存分配,代码极小 |
| 需要生成 NMEA 语句 | marnav | 唯一原生支持双向的 C++ 库 |
| 需要 AIS 解析或航海专业计算 | marnav | 内置相关功能,无需额外开发 |
六、总结与建议
6.1 关于协议处理
NMEA 0183 协议本身并不复杂,核心要点包括:
- 正确配置串口参数(4800-8-N-1)
- 校验和验证确保数据完整性
- 度分坐标到十进制度的转换
- 根据语句类型字段分发给对应的解析器
6.2 关于开源库选择
对于实际项目,强烈建议使用成熟的开源库而非自己实现。原因如下:
- 节省大量开发与调试时间
- 避免隐藏的边界条件 bug
- 获得社区持续维护和更新
最终建议:
- 如果你正在开发 C++ 桌面/服务器 项目 → 选择
marnav - 如果你在 嵌入式 Linux 上开发(C 语言为主) → 选择
libnmea - 如果你在 低端单片机 上开发 → 选择
minmea
七、参考资料
| 资源 | 链接/说明 |
|---|---|
| NMEA 0183 官方规范 | NMEA 官网(需购买) |
| marnav 文档 | https://marnav.io |
| minmea GitHub | https://github.com/kosma/minmea |
| libnmea(ESP32 组件) | https://components.espressif.com/components/janantala/libnmea |
| 各大 GNSS 厂商文档 | u-blox、Trimble、NovAtel 等厂商协议手册 |