做定位相关的项目,不管是两轮小车、四轴无人机,还是车载终端、宠物追踪器,绕不开的就是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代表定位数据 |
| 1 | 092204.999 | UTC时间,格式为时分秒毫秒:09时22分04.999秒 |
| 2 | 4250.5589 | 纬度,格式为度分(ddmm.mmmm):42度50.5589分 |
| 3 | S | 南纬标志,S表示南纬,N表示北纬 |
| 4 | 14718.5084 | 经度,格式为度分(dddmm.mmmm):147度18.5084分 |
| 5 | E | 东经标志,E表示东经,W表示西经 |
| 6 | 1 | 定位质量:0=未定位或无效,1=GPS单点定位,2=差分定位 |
| 7 | 04 | 参与定位的卫星数量 |
| 8 | 24.4 | HDOP(水平精度因子),值越小精度越高 |
| 9 | 19.7 | 海拔高度,单位米 |
| 10 | M | 高度单位,固定为M(米) |
| 11 | --.- | 大地水准面高度差,部分模块不输出则为空 |
| 12 | M | 高度差单位,固定为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代表推荐最小定位信息 |
| 1 | 092204.999 | UTC时间,同GGA,时分秒毫秒 |
| 2 | A | 定位状态:A=有效定位,V=无效警告 |
| 3 | 4250.5589 | 纬度,度分格式 |
| 4 | S | 南纬/北纬 |
| 5 | 14718.5084 | 经度,度分格式 |
| 6 | E | 东经/西经 |
| 7 | 123.45 | 对地速度,单位节(海里/小时),1节约等于1.852千米/小时 |
| 8 | 89.68 | 对地航向,单位度,相对真北方向 |
| 9 | 211005 | UTC日期,格式为日/月/年:21日10月05年 |
| 10 | 空 | 磁偏角,部分模块为空 |
| 11 | 空 | 磁偏角方向,E/W |
| 12 | A | 定位模式: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循环里,而是整理成一个小的解析模块。通用流程就五步:
- 串口读取原始字节流:设定波特率、超时时间,持续读取。
- 按行切分数据:NMEA语句以
\r\n结尾,按行切分最简单,同时能避免粘包问题。 - 解析语句类型:通过每行开头的
$GPGGA、$GPRMC等标识判断是哪条语句。 - 校验和验证:算异或,和
*后面两位比对,不通过直接丢弃。 - 提取字段并转换:按逗号拆分,提取目标字段,做度分转换和符号处理。
这个架构的优点在于:每一层都可以单独测试。你可以先喂一段固定字符串进去验证校验函数对不对,再验证度分转换函数,最后再接串口跑真实数据。如果一上来就全部糊在一起写,出了问题很难定位是串口读取问题、数据格式问题还是转换逻辑问题。
我还习惯再加一个“数据新鲜度”判断: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模块搜不到星或长时间无输出
排查这类问题,我一般按下面的顺序来:
- 看串口有没有数据:先开串口工具看有没有$开头字符串滚出来。如果一行都没有,检查TX/RX是否接反、波特率是否正确、模块供电是否正常。
- 看数据里有没有GGA和RMC:如果有GSV一直在变,但没有GGA,说明模块还在搜星阶段;如果GGA有输出但定位质量是0,说明虽然看到卫星但没有定位成功。
- 看天线环境:把模块放到室外或窗边空旷处,避免金属遮挡和强干扰源。
- 看冷启动时间:首次通电或者模块断电很久后,需要重新下载星历,这个过程可能持续几十秒到几分钟,不要一开机没定位就以为坏了。
我遇到过最离奇的一次,是模块所有引脚都正常,串口也有数据,但就是没有任何卫星信息。排查到最后发现是模块的散热焊盘焊得不好,地线接触不良,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语句里日期字段少了一位数导致时间戳错乱、某个模块会把磁偏角输出成空字段导致字段错位。有了日志,所有解析问题都能在几分钟内复原和核查。