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,那这帧数据基本可以扔了。
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年 |
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']}度")
3.5 核心流程:从串口到可用坐标
为了让你看得更清楚,我画了一张流程图,展示从原始数据到可用坐标的完整链路:
这个流程看起来简单,但实际项目中,你可能会遇到各种奇怪的问题。比如串口缓冲区溢出、数据帧错位、模块输出频率不稳定等等。我的建议是:先保证能稳定读到数据,再谈解析。
3.6 避坑指南
- 波特率不匹配:模块默认9600,但有些模块支持自动波特率。我遇到过模块输出115200,但代码里配的9600,结果读出来全是乱码。
- 时间戳问题:NMEA里的时间是UTC时间,不是本地时间。如果你在中国,记得加8小时。但要注意,模块本身可能没有做时区转换。
- 数据频率:大部分模块默认1Hz输出,也就是每秒一条GGA和一条RMC。如果你需要更高频率,得改模块配置。
- 多系统兼容:现在很多模块支持GPS+北斗+GLONASS,语句ID会变成
$BDGGA、$GLRMC。解析时别只认GP开头。
好了,这一章的内容就到这里。你学会了NMEA-0183的基本结构,知道了GGA和RMC各自包含什么信息,也拿到了一个能用的Python解析器。接下来,你就可以用这些原始数据去做漂移修复了——不过那是后面章节的事。
可可精品资料库