3、原始数据采集:NMEA-0183协议详解,解析$GPGGA、$GPRMC语句,Python串口读取实战

做定位模块开发,第一步就是跟原始数据打交道。很多新手一上来就想着怎么滤波、怎么融合,结果连串口数据都读不对——嗯,我当年也犯过这个错。

说白了,GPS模块吐出来的数据,遵循的是一个叫NMEA-0183的协议。这是航海电子协会定的标准,几乎所有定位模块都支持。你拿串口助手看一眼,满屏都是$开头的字符串,那就是了。

3.1 NMEA-0183协议长什么样?

先看一个最典型的例子。我随便从模块里抓了一帧数据:

$GPGGA,092725.00,2232.1234,N,11403.4567,E,1,08,1.2,50.0,M,-5.0,M,,*47
$GPRMC,092725.00,A,2232.1234,N,11403.4567,E,0.5,120.3,120324,,*3A

是不是看着有点晕?别急,我拆开给你看。

每条语句都以$开头,后面跟着两个字母的「谈话者ID」和三个字母的「语句ID」。比如GPGGA,GP代表GPS系统,GGA代表全球定位系统固定数据。后面就是逗号分隔的字段,最后以*加两位校验和结尾。

这里有个坑——校验和。我曾经在项目里遇到过,模块输出的数据偶尔会丢字节,导致整条语句解析出错。如果不做校验和检查,你拿到的坐标可能就是错的。所以,永远不要跳过校验。

3.2 $GPGGA语句详解

我个人习惯,先讲GGA。因为它包含了最核心的定位信息:时间、经纬度、海拔、卫星数。

字段序号 名称 示例值 说明
1 UTC时间 092725.00 09时27分25.00秒
2 纬度 2232.1234 22度32.1234分(注意单位)
3 纬度方向 N 北纬/南纬
4 经度 11403.4567 114度03.4567分
5 经度方向 E 东经/西经
6 定位质量 1 0=无效, 1=单点, 2=差分
7 卫星数 08 参与定位的卫星数量
8 水平精度因子 1.2 越小越好,一般<2算不错
9 海拔高度 50.0 单位:米
10 大地水准面高度 -5.0 单位:米

你想想看,有了这些数据,你就能知道模块当前定位质量怎么样。如果卫星数少于4颗,或者定位质量是0,那这帧数据基本可以扔了。

重点:纬度/经度的格式是「度分」格式,不是纯小数。比如2232.1234,要转成22 + 32.1234/60 = 22.53539度。这个转换我建议单独写个函数,因为后面RMC语句也要用。

3.3 $GPRMC语句详解

RMC是「推荐最小定位信息」。它比GGA多了地面速度和航向角,但没有海拔。我个人做移动轨迹分析时,更偏爱RMC,因为它的速度数据很关键。

字段序号 名称 示例值 说明
1 UTC时间 092725.00 同GGA
2 定位状态 A A=有效, V=无效
3 纬度 2232.1234 同GGA
4 纬度方向 N 同GGA
5 经度 11403.4567 同GGA
6 经度方向 E 同GGA
7 地面速度 0.5 单位:节(1节≈0.514m/s)
8 航向角 120.3 相对真北,单位:度
9 UTC日期 120324 12日03月24年
我的经验:判断定位是否有效,先看RMC的状态位是不是'A'。如果是'V',那后面的经纬度、速度全是垃圾数据。我曾经在调试时没注意这个,结果画出来的轨迹满天飞——后来才发现是模块还没锁定卫星。

3.4 Python串口读取实战

理论说完了,上代码。我用Python的pyserial库来读串口,然后解析GGA和RMC。

先安装库:

pip install pyserial

然后写一个简单的读取程序:

import serial
import re

# 打开串口,波特率通常为9600或115200
ser = serial.Serial(
    port='COM3',      # Windows下是COM口,Linux下是/dev/ttyUSB0
    baudrate=9600,
    timeout=1
)

def parse_gga(sentence):
    """解析$GPGGA语句"""
    fields = sentence.split(',')
    if len(fields) < 10:
        return None
    
    try:
        data = {
            'type': 'GGA',
            'time': fields[1],
            'lat_raw': fields[2],
            'lat_dir': fields[3],
            'lon_raw': fields[4],
            'lon_dir': fields[5],
            'quality': int(fields[6]),
            'satellites': int(fields[7]),
            'hdop': float(fields[8]),
            'altitude': float(fields[9])
        }
        return data
    except (ValueError, IndexError):
        return None

def parse_rmc(sentence):
    """解析$GPRMC语句"""
    fields = sentence.split(',')
    if len(fields) < 9:
        return None
    
    try:
        data = {
            'type': 'RMC',
            'time': fields[1],
            'status': fields[2],
            'lat_raw': fields[3],
            'lat_dir': fields[4],
            'lon_raw': fields[5],
            'lon_dir': fields[6],
            'speed_knots': float(fields[7]),
            'course': float(fields[8]),
            'date': fields[9]
        }
        return data
    except (ValueError, IndexError):
        return None

def checksum_valid(sentence):
    """校验和检查"""
    if '*' not in sentence:
        return False
    data, checksum = sentence.split('*')
    # 去掉开头的$
    data = data[1:]
    calc = 0
    for c in data:
        calc ^= ord(c)
    return calc == int(checksum.strip(), 16)

# 主循环
while True:
    line = ser.readline().decode('ascii', errors='ignore').strip()
    if not line:
        continue
    
    # 先做校验和
    if not checksum_valid(line):
        print(f"校验失败: {line}")
        continue
    
    if line.startswith('$GPGGA'):
        data = parse_gga(line)
        if data and data['quality'] > 0:
            print(f"GGA: 卫星{data['satellites']}颗, "
                  f"海拔{data['altitude']}米")
    
    elif line.startswith('$GPRMC'):
        data = parse_rmc(line)
        if data and data['status'] == 'A':
            print(f"RMC: 速度{data['speed_knots']}节, "
                  f"航向{data['course']}度")
注意:串口读取时,一定要处理异常。模块刚上电时,输出的数据可能不完整,或者包含乱码。我建议加个try-except,别让程序崩了。

3.5 核心流程:从串口到可用坐标

为了让你看得更清楚,我画了一张流程图,展示从原始数据到可用坐标的完整链路:

NMEA数据采集与解析流程 1. 打开串口 2. 读取一行数据 3. 校验和检查 4. 判断语句类型 解析 $GPGGA 解析 $GPRMC 纬度/经度/海拔/卫星数 速度/航向/日期/状态 校验失败 → 丢弃

这个流程看起来简单,但实际项目中,你可能会遇到各种奇怪的问题。比如串口缓冲区溢出、数据帧错位、模块输出频率不稳定等等。我的建议是:先保证能稳定读到数据,再谈解析。

3.6 避坑指南

  • 波特率不匹配:模块默认9600,但有些模块支持自动波特率。我遇到过模块输出115200,但代码里配的9600,结果读出来全是乱码。
  • 时间戳问题:NMEA里的时间是UTC时间,不是本地时间。如果你在中国,记得加8小时。但要注意,模块本身可能没有做时区转换。
  • 数据频率:大部分模块默认1Hz输出,也就是每秒一条GGA和一条RMC。如果你需要更高频率,得改模块配置。
  • 多系统兼容:现在很多模块支持GPS+北斗+GLONASS,语句ID会变成$BDGGA、$GLRMC。解析时别只认GP开头。
一个小技巧:调试阶段,先把原始数据保存到文件里。这样你可以离线反复解析,不用每次都跑到室外去收星。我一般会写个脚本,把串口数据全部dump下来,然后慢慢分析。

好了,这一章的内容就到这里。你学会了NMEA-0183的基本结构,知道了GGA和RMC各自包含什么信息,也拿到了一个能用的Python解析器。接下来,你就可以用这些原始数据去做漂移修复了——不过那是后面章节的事。


可可精品资料库