news 2026/9/28 1:27:06

NMEA-0183协议详解:从GPS模块串口数据到经纬度解析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NMEA-0183协议详解:从GPS模块串口数据到经纬度解析实战

做定位相关的项目,不管是两轮小车、四轴无人机,还是车载终端、宠物追踪器,绕不开的就是GPS模块。而只要一接上GPS模块,串口里就会刷刷刷往外冒字符串——$GPGGA开头、$GPRMC开头,各种字母和数字混在一起,看着头皮发麻。

这篇文章就把NMEA-0183协议彻底讲透。我会从GPS模块为什么输出这种格式讲起,逐条拆解最常见的$GPGGA和$GPRMC语句,再给出从串口读取到经纬度解析的完整流程和代码示例,最后整理我这些年调GPS模块踩过的坑。无论你是刚接触嵌入式的学生,还是已经在做物联网项目的开发者,看完都能直接上手,把经纬度从原始数据里干净地拎出来。

1. GPS模块为什么不直接输出经纬度:协议在中间扮演什么角色

1.1 GPS芯片的语言:NMEA-0183是怎么来的

先说一个很多人没想过的点:GPS模块里真正干活的是那颗GPS芯片,比如Ublox的NEO系列、中科微的AT6558、天工测控的SKG系列。芯片计算完位置后,会把结果通过串口发出去——但发什么格式,芯片厂家说了不算,得有一个统一的“通用语言”,这就是NMEA-0183。

NMEA-0183是美国国家海洋电子协会(National Marine Electronics Association)制定的标准,最早用于航海电子设备之间的通信,后来GPS接收机也广泛采用。它的核心思路特别朴素:所有数据用纯ASCII文本表达,按行发送,行与行之间用回车换行分隔,字段之间用逗号分隔。这样的好处非常明显——任何单片机、任何电脑串口工具、任何编程语言都能轻松读取和处理,甚至你用手机的蓝牙串口app都能直接看到内容。

我经常用一个类比:NMEA-0183就像餐馆的菜单模板。不同厨师(不同GPS芯片厂家)做法可以不一样,但端上来的菜都得按照“凉菜-热菜-主食-汤”这个顺序写,顾客(单片机)才知道先吃哪个。没有这个标准协议的话,换了Ublox的模块要改一套解析代码,换成中科微又要改一套,那开发效率就崩了。

1.2 NMEA-0183的核心特性与常见语句类型

NMEA-0183的句子有几百种,但日常用到GPS模块时,关注的就那么几条。我在项目里实际处理过的语句中,这几个见得最多:

  • $GPGGA:全球定位系统定位数据(Global Positioning System Fix Data),包含经纬度、定位质量、卫星数量、海拔高度。
  • $GPRMC:推荐最小定位信息(Recommended Minimum Specific GPS/Transit Data),包含经纬度、速度、航向、日期。
  • $GPGSV:可见卫星信息,包含天空中搜索到的卫星编号、仰角、方位角、信噪比。
  • $GPGSA:精度及卫星使用信息,包含正在使用的卫星编号和位置精度因子(PDOP、HDOP、VDOP)。
  • $GPVTG:地面速度信息(Course Over Ground and Ground Speed),包含对地航向和速度。

其中$GPGGA和$GPRMC是我们做定位解析必用的两条。GGA偏“静态质量”,适合判断定位是否解算成功;RMC偏“运动状态”,里面有速度、航向和日期,适合车载或运动轨迹记录。后面我会把这两条语句逐字段拆开讲,这是解析流程里最核心的环节。

这里还要强调一个容易忽略的参数:波特率。绝大多数GPS模块默认是9600波特率,也有模块出厂设成115200(比如部分4G+GPS模组)。如果串口工具波特率设置不对,看到的就是一堆乱码。这一点在后面的实操环节会再次提到,算是新手最常犯的第一个错误。

2. 一帧NMEA数据长什么样:GGA与RMC核心字段逐行拆解

2.1 从一条$GPGGA看起:每个数字背后是什么

我随便拿一条真实的GGA语句举例,这是我在窗边实测得到的:

$GPGGA,092204.999,4250.5589,S,14718.5084,E,1,04,24.4,19.7,M,--.-,M,,*6E

拆开看,逗号分隔的每一个字段都有明确含义。我按索引标出来:

字段索引示例值含义与说明
0$GPGGA语句标识符,GGA代表定位数据
1092204.999UTC时间,格式为时分秒毫秒:09时22分04.999秒
24250.5589纬度,格式为度分(ddmm.mmmm):42度50.5589分
3S南纬标志,S表示南纬,N表示北纬
414718.5084经度,格式为度分(dddmm.mmmm):147度18.5084分
5E东经标志,E表示东经,W表示西经
61定位质量:0=未定位或无效,1=GPS单点定位,2=差分定位
704参与定位的卫星数量
824.4HDOP(水平精度因子),值越小精度越高
919.7海拔高度,单位米
10M高度单位,固定为M(米)
11--.-大地水准面高度差,部分模块不输出则为空
12M高度差单位,固定为M
13空差分定位时间,非差分模式为空
14空差分站ID,非差分模式为空
15*6E校验和,星号后跟两位十六进制数

绝大多数解析代码只关心第0~9个字段。其中第6个字段“定位质量”是判断数据是否有效的关键:值等于1或2,说明当前定位有效,经纬度可以拿去用;值是0,说明卫星没锁定,经纬度还停留在上次的位置或者是空数据。

2.2$GPRMC怎么读:速度、航向和有效状态都在这

RMC语句在运动场景下非常有用,比GGA多出了速度和航向信息。看一条实测示例:

$GPRMC,092204.999,A,4250.5589,S,14718.5084,E,123.45,89.68,211005,,,A*7D

字段拆解如下:

字段索引示例值含义与说明
0$GPRMC语句标识符,RMC代表推荐最小定位信息
1092204.999UTC时间,同GGA,时分秒毫秒
2A定位状态:A=有效定位,V=无效警告
34250.5589纬度,度分格式
4S南纬/北纬
514718.5084经度,度分格式
6E东经/西经
7123.45对地速度,单位节(海里/小时),1节约等于1.852千米/小时
889.68对地航向,单位度,相对真北方向
9211005UTC日期,格式为日/月/年:21日10月05年
10空磁偏角,部分模块为空
11空磁偏角方向,E/W
12A定位模式:A=自主定位,D=差分定位,E=估算,N=无效
13*7D校验和

注意RMC第2个字段是单字符A/V,判断有效性时比GGA更直接。A就是定位有效,V代表当前数据无效。我在写解析程序时一般优先用RMC判断有效状态,因为它连日期都给了,在记录轨迹时间戳时很方便。

2.3 校验和算法:怎么防止一帧脏数据毁掉整个定位

NMEA-0183每一行语句在*后面都有一个两位十六进制校验和。算法不复杂,但很多人会忽略:

把$和*之间的所有字符(不包含$和*)逐字节做异或运算,得到一个8位整数,转成两位大写十六进制就是校验和。

拿GGA那行举例,需要参与校验的是从GPGGA开始到校验前最后一个逗号为止的所有字符。用Python写校验函数非常简单:

def calculate_checksum(sentence_without_prefix): checksum = 0 for char in sentence_without_prefix: checksum ^= ord(char) return f"{checksum:02X}"

实际解析时,先按*把语句拆成“数据区”和“校验区”,对数据区计算校验和,再和校验区比对。不一致就丢弃这一帧。我实测过,串口在长线传输、干扰环境下确实会出现个别字符跳变的情况,如果不做校验,解析出来的经纬度可能突然跳到几百公里外,轨迹上出现一个离谱的毛刺点。解析流程里加一道校验拦截,是性价比最高的稳定性优化。

3. 从接线到串口数据读取:完整实操过程

3.1 模块接线与上电注意事项

先说硬件接线。绝大多数GPS模块引出的脚位都差不多:VCC、GND、TX、RX,有的还有PPS(秒脉冲)和1PPS输出。只需要把模块的TX接到单片机的RX,模块的RX接到单片机的TX,交叉连接即可。我第一次焊模块时就踩过坑,把TX接TX、RX接RX,结果串口一点动静都没有,排查了半天才发现是同向对接,信号根本没通。

供电方面要特别小心。很多GPS模块(比如中科微的AT6558系列)工作电压是3.3V,但板载了LDO,可以直接用5V供电。如果模块没有任何稳压电路,你用5V直接怼上去,大概率直接烧掉。稳妥做法是:先看模块丝印或者卖家说明,默认按3.3V供电,TTL电平也选3.3V。连接单片机和USB转TTL时同样要确认电平,STM32的板子很多是3.3V逻辑,传数据没问题;但如果接的是5V单片机,注意电平转换,免得把模块串口引脚打坏。

GPS天线朝向也很重要。模块背面那一整块是陶瓷天线,必须朝向天空。有人把它贴在铁板下面、扣在金属外壳里,结果死活搜不到星,这其实是天线被屏蔽了。室内靠窗能搜到几颗星,但定位效果不稳定;空旷的窗边或者室外,冷启动通常在30秒到2分钟之间。

3.2 用串口工具直接看原始数据:第一手数据长什么样

拿到GPS模块后,我建议第一件事不是写代码,而是先用USB转TTL模块接电脑,打开串口工具直接看原始数据。这一步能帮你提前确认模块工作是否正常、波特率是多少、输出哪些语句,为后续写解析代码打下基础。

接线方法:GPS模块的TX接USB转TTL的RX,GPS模块的RX接USB转TTL的TX,共地,然后插电脑。打开串口工具(我用过sscom、MobaXterm的串口模式,还有Arduino IDE自带的串口监视器),波特率选9600,打开串口。

如果一切正常,你会看到一行行以$开头的ASCII字符串不断滚动。有的模块默认只输出GGA、RMC和GSV,有的输出全部语句。看到数据后,先确认RMC中第2个字段是不是A:

$GPRMC,055702.000,A,3115.5584,N,12126.2845,E,0.00,0.00,050423,,,A*7A

这个例子里A就代表定位有效。如果显示V,说明GPS还在搜星状态,把天线往窗户边挪一挪,等一会儿再看。

下面是一段我在室内窗边实测抓到的GGA完整输出,你可以拿来练手解析:

$GPGGA,055702.000,3115.5584,N,12126.2845,E,1,07,1.2,8.5,M,12.3,M,,*5B

注意经纬度字段第2位是3115.5584,代表的是北纬31度15.5584分,解析成十进制度数是31 + 15.5584/60 = 31.259306。关于度分转换的具体算法,下一节详细展开。

3.3 用Python写一个最小解析demo:从串口到十进制度数

串口工具确认数据正常后,就可以上代码了。我用Python的pyserial库写过一个最小的GPS解析程序,整个过程就三步:读串口一行、校验、按逗号拆字段并转换经纬度。

先把依赖装上:

pip install pyserial

然后直接写解析脚本:

import serial def calculate_checksum(payload): checksum = 0 for ch in payload: checksum ^= ord(ch) return f"{checksum:02X}" def convert_to_decimal(coord, direction): # coord 形如 ddmm.mmmm 或 dddmm.mmmm # 度分转换为十进制度:度 + 分/60 if not coord: return None dot_index = coord.index('.') degrees = int(coord[:dot_index - 2]) minutes = float(coord[dot_index - 2:]) decimal = degrees + minutes / 60.0 if direction in ('S', 'W'): decimal = -decimal return decimal def parse_gga(sentence): if not sentence.startswith('$GPGGA'): return None star_index = sentence.index('*') payload = sentence[1:star_index] received_checksum = sentence[star_index + 1:] if calculate_checksum(payload) != received_checksum: return None fields = payload.split(',') # 字段顺序见前面表格 if len(fields) < 6: return None # 判断定位质量:1或2才有效 fix_quality = fields[5] if fix_quality not in ('1', '2'): return None latitude = convert_to_decimal(fields[2], fields[3]) # 纬度 longitude = convert_to_decimal(fields[4], fields[5]) # 经度 satellites = fields[6] altitude = fields[8] if len(fields) > 8 else None return { 'lat': latitude, 'lon': longitude, 'satellites': satellites, 'altitude': altitude } ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1) while True: line = ser.readline().decode('ascii', errors='ignore').strip() if line.startswith('$GPGGA'): result = parse_gga(line) if result: print(f"经度: {result['lon']:.6f}, 纬度: {result['lat']:.6f}, 卫星数: {result['satellites']}, 海拔: {result['altitude']}m")

这段代码看起来不长,但已经把NMEA解析最关键的几个点都覆盖了:校验和判断、度分转换、定位质量判断。convert_to_decimal函数是核心,它先找到小数点位置,往前倒推两位是“分”的整数部分,之前是“度”,再把分除以60加进去。南纬和西经要转成负值,很多人在这一步栽跟头,坐标反了或者符号丢了,地图上定位点直接跑到另一个半球。

我建议你把这段代码保存下来,等GPS模块到手后跑一遍,看到串口滚动输出经纬度,你就已经完成了从“原始字符串”到“可用坐标”的完整链路。

4. GPS解析的通用流程与精度优化技巧

4.1 从原始数据到坐标的全流程:一个能复用的解析架构

在实际项目中,我不会把解析逻辑写死在while循环里,而是整理成一个小的解析模块。通用流程就五步:

  1. 串口读取原始字节流:设定波特率、超时时间,持续读取。
  2. 按行切分数据:NMEA语句以\r\n结尾,按行切分最简单,同时能避免粘包问题。
  3. 解析语句类型:通过每行开头的$GPGGA、$GPRMC等标识判断是哪条语句。
  4. 校验和验证:算异或,和*后面两位比对,不通过直接丢弃。
  5. 提取字段并转换:按逗号拆分,提取目标字段,做度分转换和符号处理。

这个架构的优点在于:每一层都可以单独测试。你可以先喂一段固定字符串进去验证校验函数对不对,再验证度分转换函数,最后再接串口跑真实数据。如果一上来就全部糊在一起写,出了问题很难定位是串口读取问题、数据格式问题还是转换逻辑问题。

我还习惯再加一个“数据新鲜度”判断:GPS模块在定位有效后会持续输出数据,但一旦卫星信号丢失,模块输出的还是携带旧经纬度的GGA语句,只是定位质量字段变成0。如果程序只看字段格式,很容易把旧数据当成新数据用。我一般会记录每帧数据的到达时间,超过3秒没收到有效定位,就认为当前位置是失效的,上传平台时打一个离线标签。

4.2 定位精度没法提升时,先检查这些因素

很多人换了贵价模块后,发现精度并没有比便宜的好多少,就开始怀疑模块有问题。实际情况是,GPS定位精度受环境影响极大,NMEA数据里能直观反映精度的是GGA第8个字段HDOP和卫星数量。

我整理了一个经验参考表:

HDOP值精度等级说明
< 1.0优秀多见于开阔地带,卫星数量多且几何分布好
1.0~2.0良好正常室外定位水平,误差约3~8米
2.0~4.0一般略有遮挡或者卫星数偏少,误差可能到15米
> 4.0较差室内、高楼峡谷、密集树冠下常见,不建议做精度敏感应用

我在项目里遇到过一种情况:HDOP稳定在1.0以内,经纬度输出却很飘。排查到最后发现是模块的PPS引脚没有做滤波,导致模块内部参考时钟抖动,影响了定位解算。这里想表达的是,模块标称精度只是理想值,实际效果依赖天线、电源纹波、周围环境这三大因素。电源纹波大的板子,GPS信号非常容易受干扰,建议模块供电处加一个10uF以上的去耦电容。

4.3 地图坐标与GPS坐标的差异:WGS-84、火星坐标与偏转

还有一个很多人会踩的坑:把GPS模块输出的经纬度直接标到国内地图平台上,结果发现位置偏了几十米甚至几百米。这不是GPS模块坏了,而是坐标系统的问题。

GPS模块默认输出的坐标基于WGS-84(World Geodetic System 1984)国际标准,这是一个全球统一的坐标系。但国内地图产品(比如高德、百度)使用的是经过偏移处理的坐标系统,其中最常见的是GCJ-02“火星坐标”,百度的BD-09又是在火星基础上做了第二次加密。你需要把GPS解析出的WGS-84经纬度换算成目标平台的坐标系后再展示。

坐标转换有现成库可以用,Python生态里有coord_convert等库,C/C++也有开源转换函数,核心算法就是把经纬度做一次基于椭圆体参数的平移变换。在写定位采集程序时,一定给经纬度单独留一个坐标转换模块,方便后面适配不同平台。

5. 常见问题与排查技巧实录

5.1 GPS模块搜不到星或长时间无输出

排查这类问题,我一般按下面的顺序来:

  1. 看串口有没有数据:先开串口工具看有没有$开头字符串滚出来。如果一行都没有,检查TX/RX是否接反、波特率是否正确、模块供电是否正常。
  2. 看数据里有没有GGA和RMC:如果有GSV一直在变,但没有GGA,说明模块还在搜星阶段;如果GGA有输出但定位质量是0,说明虽然看到卫星但没有定位成功。
  3. 看天线环境:把模块放到室外或窗边空旷处,避免金属遮挡和强干扰源。
  4. 看冷启动时间:首次通电或者模块断电很久后,需要重新下载星历,这个过程可能持续几十秒到几分钟,不要一开机没定位就以为坏了。

我遇到过最离奇的一次,是模块所有引脚都正常,串口也有数据,但就是没有任何卫星信息。排查到最后发现是模块的散热焊盘焊得不好,地线接触不良,GPS射频信号根本没有被正常处理。这类问题很难从外部看出端倪,只能用替换法排除。

5.2 解析出来的经纬度差了好几公里

这个问题的诊断方向很明确。经纬度偏差达到公里级,通常只有四种可能:

  • 度分转换算错:最常见。把4250.5589直接当成42.505589度用,差了将近0.3度,大概是30公里误差。记住:小数点前最后两位(或三位)是“分”,要除以60。
  • 南北纬/东西经符号丢失:南纬没转负值,位置跑到赤道另一侧,差得就很离谱。
  • 定位质量没有判断:把定位质量字段为0(无效)的数据也拿去用了,位置可能是上一个有效点的旧值,一两公里偏差很正常。
  • 坐标混用:拿WGS-84数据直接标到GCJ-02地图上,偏几十米到几百米,在小范围电子地图上特别明显。

我建议在代码里加一个断言式检查:纬度绝对值必须在0到90之间,经度绝对值必须在0到180之间,不符合直接丢弃。这一条能拦住绝大多数脏数据。

5.3 串口数据乱码、断帧和半包问题怎么处理

GPS数据是持续的ASCII流,单片机接收时不注意处理,很容易出现半包数据。比如你按固定长度去读串口,一条NMEA语句被拦腰截断,解析自然失败。我的习惯是按\r\n做行切分,维护一个环形缓冲区,等收到完整的\n再处理一行,彻底规避半包问题。

如果乱码是偶发的,通常是波特率不匹配或者线路干扰。波特率设置错误表现为整片乱码;线路干扰表现为随机几个字符变成?或乱码,伴随偶发的校验失败。USB转TTL模块质量差、杜邦线太长(超过20cm就要注意),都会引入干扰。

还有一个容易忽略的点:GPS模块串口输出的数据量不大,但GSV语句一多,低性能单片机如果不及时读取串口缓冲区,会有溢出丢数据的情况。处理办法是提高串口中断优先级,或者减少不需要的语句输出。部分模块支持用专用配置指令关闭GSV、GSA等不必要语句,只保留GGA和RMC,能显著降低数据量。

6. 经验总结:真正用时才发现的门道

做定位项目这几年,最大的体会是:NMEA-0183协议本身不难,难的是把它放到真实环境里用起来。

一个小小的GPS模块,要处理好串口电平匹配、天线布局、冷热启动、数据有效性判断、坐标系转换,才能在最终产品里稳定输出一个可信的位置。我建议新手不要急着上复杂的双频模块、RTK方案,先把最基础的UART转NMEA解析跑通,再用误差分析去判断上高精度方案的必要性,避免盲目堆硬件。

最后分享一个很实用的习惯:先把原始串口数据录下来存成日志文件,再离线调试解析逻辑。这样你手头永远有一份可复现的数据集,改代码不会受天气和天线位置影响。当初我就是靠这个习惯,快速定位过好几个“理论上绝对不可能”的Bug,比如RMC语句里日期字段少了一位数导致时间戳错乱、某个模块会把磁偏角输出成空字段导致字段错位。有了日志,所有解析问题都能在几分钟内复原和核查。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:27:04

葡萄图像数据集目标检测实战:YOLO格式转换与迁移学习要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:57

一键开关机芯片选型指南:从电压电流到低功耗逻辑的四个维度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:07

Keil5找不到ARM Compiler 5.06?从安装到配置一次性解决AC5报错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:25:29

ESP32S3+W5500实战:SPI以太网接线、驱动与TCP测速全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:25:02

CAN总线长帧传输解析:ISO-TP协议原理与CANoe高效实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:24:08

C# USB HID读卡器上位机开发:IC卡与CPU卡稳定通信实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华