简介:GPS原始数据捕获与简单分析资料包,面向嵌入式开发者、测绘专业学生以及GNSS数据应用入门者,聚焦GPS接收器原始NMEA语句的抓取与解析,帮助读者打通从卫星信号到经纬度坐标、速度、方向、UTC时间等关键信息的处理链路。资源共32个文件,以C语言源码、头文件、工程配置文件(uvproj/uvopt)、hex可烧录程序为主,同时附带GPS数据捕获全程记录与处理后数据的txt文档,压缩包仅91KB,结构清晰,便于快速下载和按需查阅。当前已有665人学习使用。内容覆盖NMEA协议常见语句(如GPGGA、GPRMC)的读取、最少4颗卫星的三维定位原理、基于连续坐标的速度与方向计算、信号质量与GDOP精度因子评估等核心知识点,并给出了基于LCD1602/12864显示的完整单片机工程,可直接编译运行或移植到自己的项目。适合作为课程设计、毕业设计或自学GPS数据解析的有效参考。
1. GPS原始数据捕获:从一行NMEA到一条可用轨迹
GPS原始数据捕获听起来像必须上专业设备才能做的事,实际上一根串口线加一块接收模组就能开始。这里的捕获,不是用地图App看坐标,而是把GPS接收机输出的原始语句一行行读下来、存成文件,再做简单分析。它解决的是定位测试里最磨人的一类问题:模组在手却不知道数据长什么样,或者做轨迹采集时拿不到干净可用的数据。这个方向适合嵌入式开发者、做数据捕获和分析的分析工程师,也适合刚接触GPS想搞清数据链路的从业者。目标很简单——让原始数据从串口进到硬盘,再从硬盘变成看得懂的轨迹和指标。
2. 把原始数据接到电脑:GPS捕获的硬件链路与最小命令
2.1 串口读取是GPS捕获的主干道:先确认波特率再谈分析
GPS接收模组对外输出的通道基本是UART串口,数据格式是NMEA 0183协议文本,以$开头、一行一条语句。PC或嵌入式开发板一般通过USB转串口把模组接进来,Linux下设备节点常见的是/dev/ttyUSB0或/dev/ttyAMA0。常见默认波特率是9600,也有模组出厂设成4800或115200,接线通电前最好先确认这个值。判断捕获链路通没通,第一眼就是波特率对不对。
当读出来整行整行的乱码或只有半截字符,通常不是天线干扰,而是波特率设置和模组实际值不一致,跟玄学无关。我印象里某块开发板默认9600,通电就能看到$GPGGA、$GPRMC;换了一块模组后默认变成4800,同一段脚本读出来全是残缺行,把波特率改成4800立刻恢复。所以排查第一步永远是核对波特率。
最小捕获链路是VCC、GND、TX、RX四根线,其中共地这根最容易被忽略但最不该省:
- 模组TX接转接板RX
- 模组RX接转接板TX
- 转接板与模组必须共地,不共地时TTL电平参考点不一致,会出现偶发乱码
- 主动天线模组还需要给天线供电,具体看模组手册的PHANTOM脚说明
这个链路上不需要额外电平转换芯片,常见GPS模组的UART是3.3V TTL,USB转串口板通常可以直接兼容。要是用5V单片机直连3.3V模组,就要加电平转换或至少确认IO容限,否则长期工作容易烧坏模组引脚。捕获端确认无误后,下一步就是写一个能连续收数据的最小脚本。
2.2 用Python读一轮原始语句:最小可跑脚本
import serial import time # 打开串口,波特率按模组出厂默认设置修改 ser = serial.Serial( port="/dev/ttyUSB0", baudrate=9600, timeout=1, ) try: # 持续读到以$开头、以换行结束的原始语句并打印 while True: line = ser.readline() if not line: continue text = line.decode("ascii", errors="ignore").strip() if text.startswith("$"): print(f"{time.strftime('%H:%M:%S')} {text}") except KeyboardInterrupt: ser.close()逻辑说明:
- serial.Serial里的timeout=1表示每次readline最多等1秒,避免模组长时间未定位时空转卡死。
- line.decode用errors="ignore",GPS原始数据是ASCII文本,但偶发干扰字节会让日常的UTF-8解码直接抛异常,忽略错误能让采集进程更耐造。
- if text.startswith("$")过滤掉串口打开瞬间的残留字符。
- print前加了本机时间戳,方便把系统时间和GPS自带UTC时间做粗略对照。
任何一条NMEA语句都是$开头、*和两位校验和结尾,语句间以\r\n分隔。这个脚本把print换成写文件,就是最朴素的GPS数据采集Log。这里有一个容易翻车的点:不要把串口当普通文本文件直接open读取,也不要在Windows下用记事本打开抓包文件。NMEA用\r\n换行,记事本会把整段内容拼成一行,后续按行解析会直接失效。我习惯在Linux或Python统一处理换行,保存成\n结尾的文件,跨平台分析时少很多麻烦。
2.3 PPS与时间戳:GPS捕获里最容易被忽略的一条线
GPS原始数据每条语句都自带UTC时间字段,但那是接收机内部的秒级时间,每一句真正打印出来的时刻还叠加了串口发送延迟。如果只做轨迹回放,秒级精度够用;要做速度曲线或和传感器数据对齐,就必须引入PPS脉冲线。
PPS是GPS接收机每秒输出一个边沿脉冲的信号,它严格对齐GPS秒边界。我在采集高精度轨迹时,会把PPS接到开发板的GPIO中断脚,在中断里记录系统时间戳,再用队列把NMEA里的时间字段挂到最近一次PPS上。这样原始数据的定位时间误差能从几十毫秒压缩到几毫秒。
没有示波器时也有验证办法:用GPIO边沿中断配合Python的time.time(),连续记录100个PPS中断间隔。间隔平均值接近1.000秒,抖动在几毫秒内,说明捕获链路时间基准健康。这一步对后续做轨迹分析价值很大——原始GPS文件一旦有了准确时间基准,速度和丢星分析就都有可靠坐标系。
3. 原始语句拆开看:GPS分析前要把NMEA字段读明白
3.1 GGA、RMC、GSA三句话,覆盖GPS简单分析全部要素
原始数据里的语句种类不少,但简单分析常用到的就三种:GGA、RMC、GSA。搞懂这三句,经纬度、时间、速度、卫星数、精度因子就都能拿到。
| 语句 | 关键字段 | 简单分析用途 |
|---|---|---|
| GGA | UTC时间、纬度、经度、定位质量、卫星数、HDOP、海拔 | 判断定位是否有效,统计卫星数和HDOP变化 |
| RMC | UTC时间、状态A/V、经纬度、速度(节)、航向、日期 | 轨迹绘制、速度计算、按日期归档 |
| GSA | 定位模式、参与定位卫星编号、PDOP/HDOP/VDOP | 观测卫星几何强度,判断定位环境 |
RMC里的状态位只有A和V两种,A代表当前定位有效,V代表接收机已经启动但还没有锁住卫星。很多人在数据里看到大量V还继续画轨迹,画出来就是一条从零点开始乱飞的线。正确做法是解析时直接丢掉V。
GGA里的定位质量字段是一个数字,0表示无效,1表示单点定位,2表示差分定位,4表示RTK固定解。这个数字比卫星数更能说明问题,因为卫星数只代表“看到几颗”,定位质量代表“真的解出位置了没有”。GSA里的PDOP/HDOP/VDOP是几何精度因子,数值越小越好,后面单独开一节说。
3.2 经纬度换算:DDMM.MMMM转十进制度代码与边界坑
NMEA里经纬度不是日常用的小数度,而是度分格式。纬度和经度都长成这样:3103.4567表示31度03.4567分,前两位是度,后面是分。必须先换算成十进制度才能画图和算距离。
def parse_nmea_coord(raw: str, hemi: str) -> float | None: """NMEA经纬度字符串转十进制度。 raw: 形如 '3103.4567' 的 ddmm.mmmm 字段 hemi: N/S/E/W 方向字母 """ if not raw: return None try: value = float(raw) except ValueError: return None # 无效语句常见字段为全0,返回None防止画出(0,0)点 if value == 0: return None # 前两位是度,其余是分 deg = int(value // 100) minute = value - deg * 100 decimal = deg + minute / 60.0 # 南纬和西经取负 if hemi in ("S", "W"): decimal = -decimal return round(decimal, 7)参数说明:
- value // 100取整数度,3103.4567前两位31是度。
- minute = value - deg * 100取出03.4567分,除以60换算成小数度。
- 这个换算错位是新手最常踩的坑,有人直接把字符串第3位起当小数度,误差在纬向上是公里级。
- 南纬和西经为负,地球坐标约定东经北纬为正,符号处理必须在解析层完成,不能留给可视化环节。
- 返回round(...,7)是为了显示整洁,约等于1cm级别精度,对GPS原始数据分析足够。
注意:如果原始字段解出来是0或空字符串,直接返回None。不要保留0值经纬度,否则轨迹图上会出现一个坐标原点附近的假点,排查时很容易误导。
3.3 Fix Quality、卫星数与PDOP:判断这行数据能不能用
GGA里有一串典型数据: $GPGGA,092204.999,3103.4567,N,12134.5678,E,1,09,1.2,15.6,M,-2.3,M,,*7A
这条里定位质量是1,表示单点定位可用;卫星数09表示有9颗参与定位;1.2是HDOP。如果定位质量是0,后面这些字段都不可信,滤波时应该整行剔除。
PDOP的判断标准在不同环境差别很大。开阔地带PDOP通常小于2,城市峡谷里会到3到5,超过5说明参与定位的卫星几何分布很差,位置解的水平误差可能比正常时放大一倍以上。我一般把HDOP大于5的点标记为低置信度,简单分析直接排除,比后续做平滑处理更干净。
卫星数也不是越多越好。卫星数多但集中在一个方向,PDOP照样难看;卫星数少但分布均匀,定位质量反而可能不错。所以判断数据能不能用,顺序是定位质量优先,然后是HDOP,最后才是卫星数。这个顺序在很多现成工具里是反的,容易让人误判数据质量。
4. 从GPS原始数据到可读结论:速度、轨迹、丢星与稳定性
4.1 轨迹基础:把RMC按行解析成CSV
简单分析的第一步通常是做轨迹,最可靠的输入是RMC语句,因为它自带日期、时间、经纬度、速度和航向,一句搞定全部要素。
import csv import sys def parse_nmea_coord(raw: str, hemi: str) -> float | None: """NMEA度分格式转十进制度,无效值返回None。""" if not raw: return None try: value = float(raw) except ValueError: return None if value == 0: return None deg = int(value // 100) minute = value - deg * 100 decimal = deg + minute / 60.0 if hemi in ("S", "W"): decimal = -decimal return round(decimal, 7) def parse_rmc(line: str) -> dict | None: fields = line.split(",") if not line.startswith("$GPRMC"): return None # fields[2]是状态位,A有效、V无效,V直接丢弃 if fields[2] != "A": return None lat = parse_nmea_coord(fields[3], fields[4]) lon = parse_nmea_coord(fields[5], fields[6]) if lat is None or lon is None: return None return { "utc_time": fields[1], "date": fields[9], "lat": lat, "lon": lon, "speed_knots": fields[7], "course": fields[8], } # 输入原始GPS Log文件,输出track.csv with open(sys.argv[1], newline="", encoding="ascii") as src, \ open("track.csv", "w", newline="", encoding="utf-8") as dst: writer = csv.writer(dst) writer.writerow(["utc_time", "date", "lat", "lon", "speed_knots", "course"]) for raw in src: rec = parse_rmc(raw.strip()) if rec: writer.writerow(rec.values())逻辑说明:
- fields[2]是RMC状态位,A为有效、V为无效。无效记录必须过滤,否则轨迹会多出大量从零点出发的噪声线。
- fields[1]是UTC时间,fields[9]是日期。文件里保持UTC不动,本地时区转换放到展示阶段做,避免日期跨日错位。
- 速度单位是节,转公里每小时乘以1.852。采集文件里保留原始节值,单位转换放到分析阶段,防止源数据混入多种单位。
- csv标准库输出,中文环境里Excel打开不乱码,省掉后续沟通成本。
CSV生成后,可以用任意绘图工具按lat/lon画散点图或连线图。注意按时间排序后再连线,GPS原始数据偶尔会乱序,乱序连线会产生锯齿状回线。排序前先把utc_time解析成秒数或直接用字符串排序,因为hhmmss.ss格式按字典序排也是对的,但要保证同一秒内的多条记录顺序稳定。
4.2 速度与丢星:从GSV看GPS捕获信号质量
速度曲线可以直接从RMC字段7换算后绘制,比速度更有判断价值的是卫星数变化。GGA里有参与定位卫星数,GSV里有可见卫星信噪比,两者一起看能识别丢星事件。
丢星的现象很典型:参与定位卫星数从9掉到3,HDOP同步上涨,速度曲线出现尖刺。采集日志里如果卫星数连续几条低于4,大概率是进入了高架下、地库出口或者建筑物密集区。做数据捕获和分析时,这类事件应该手动标记时间点,否则轨迹会在目标消失段画出一条并不存在的直线。
我习惯写一个简单统计:把日志按分钟切段,统计每段GGA的卫星数最小值和中位数。最小值连续两分钟低于4,标记为一次丢星时段;卫星数中位数低于6但定位质量仍为1,说明信号边缘化,轨迹可信度要下调。这个统计可以输出成一张文本表:开始时间、结束时间、卫星数最小值、HDOP最大值、丢星结论。
GSV里的信噪比数据更细,能看到单颗卫星的SNR在20dBHz以下。SNR长期低于20的卫星参与定位时,往往就是跳点源头。简单分析不必做到逐星追踪,但可以把SNR低于20的卫星数量按分钟统计出来,作为环境质量的参考指标。
4.3 定位稳定性量化:HDOP散点与固定率是比坐标更稳的指标
坐标跳动看单点不明显,但HDOP整个序列能说明接收环境的几何强度。做法是解析GGA里的HDOP字段,输出最大值、最小值、均值,以及HDOP超过2和超过5的占比。
这个指标的判读经验大致是:HDOP均值在2以内,属于开阔环境;2到5是弱信号区域;大于5时后续轨迹处理应直接降权甚至剔除。在真实道路测试里,开阔路段HDOP稳定在1左右,两侧有高楼的路段会抬到2到3,高架下会短时冲上5以上。
我把HDOP大于5的采样点标记为低置信度,在动态分析里排除,比单看坐标做平滑稳定得多。配合定位质量字段一起用:定位质量不是1且HDOP大于5的点,直接不参与统计。这样产出的固定率指标更贴合实际可用性。
注意:HDOP是水平精度因子,只能反映水平方向的几何强度。高架场景下垂直方向误差很大,需要同时看VDOP,简单分析至少要把GSA里的PDOP记录保留下来,避免后续想查垂直精度时发现原始字段没存。
5. GPS捕获常见问题排查:现象到原因再到解决
5.1 串口读出来全是乱码:先查波特率再查地线
现象:串口数据部分可读,夹着大量乱码,甚至完全不可读;偶尔还会出现数据能读但每行长度明显偏短。
原因:最常见是波特率不匹配,其次是转接板和模组没有共地。TTL电平的参考点不一致时,数据位会在采样点上翻转错误,表现就是乱码。TX/RX接反一般是无数据而不是乱码,这可以作为区分线索。
解决:把波特率轮流设为4800、9600、115200三个常用值,观察首条语句是否变成$开头的完整文本。共地问题补一根GND线,从头到尾走一遍链路,确保模组和转接板的地是同一参考点。仍乱码的话用示波器看TX波形,确认电平在0V与3.3V之间完整摆动,电平只有一半多是硬件损坏或供电不足。
5.2 只有GPGSV刷屏,看不到GGA和RMC
现象:日志里全是$GPGSV和$GPGSA,GGA和RMC比例很低甚至完全没有,打开日志感觉像被搜星语句刷屏。
原因:接收机处于搜星状态但尚未定位成功。GSV代表接收机正在扫描天空,GGA的状态位长期为0。另一种情况是天线开路或天线供电缺失,接收机始终无法锁定卫星。
解决:把天线移到窗边或室外空旷处,等待1到2分钟再看。GGA状态位仍为0时,检查天线供电电压,主动天线如果缺供电,搜星能力会大幅下降。区分方法很简单:搜星但未定位时GSV里会持续出现卫星编号,天线故障时GSV几乎全是空字段。
5.3 轨迹出现几十米到几百米的瞬时跳点
现象:轨迹整体正常,个别点突然跑出离主线很远的距离,连成一条长直线后又跳回主线。
原因:多径或弱信号条件下,某条语句使用了一颗信噪比很差的卫星解算位置,单点解发生偏移。跳点通常发生在高架桥两侧、玻璃幕墙附近和隧道出口。
解决:不要修改原始采集文件,把原始数据完整保留下来做回溯。分析阶段把定位质量为0、HDOP大于5、速度较前一帧跳动超过3倍以上的点标记为异常点,按时间段剔除。这比直接上平滑滤波器安全,因为保留原始数据等于保留后悔药,平滑会丢失跳点的真实成因信息。
5.4 时间字段老是差8小时
现象:原始语句的UTC时间和本地时间对不上,按日期归档时数据切到了错误的一天,夜间采集的数据全部归到前一天。
原因:NMEA里所有时间字段都是UTC,没有本地时区概念。部分解析工具或脚本会把UTC当本地时间处理,于是出现整小时偏移。
解决:采集文件里保持UTC不变,只在输出展示阶段转换时区。需要转换为本地时间时,使用固定偏移而不是读取系统时区设置。某些地区实行夏令时,用系统时区会自动跳变,固定偏移才能保证历史文件的时间轴一致。这条在跨地区采集时尤其重要,日志里的时区标注最好直接写进文件名或字段名,避免后续交接时二次误判。
6. 进阶验证:用PPS把捕获数据回放到毫秒级对齐
拿到一段采集好的GPS原始Log,怎么知道时间轴可信?把PPS中断时间戳间隔统计出来就能验证。采集时在GPIO中断里记录time.time(),连续记录1000次,然后计算间隔均值与标准差:
import statistics def pps_interval_stats(pps_timestamps: list[float]) -> tuple[float, float]: """输入PPS中断时间戳数组,返回间隔均值与标准差。""" intervals = [] for i in range(1, len(pps_timestamps)): intervals.append(pps_timestamps[i] - pps_timestamps[i - 1]) if not intervals: return 0.0, 0.0 return statistics.mean(intervals), statistics.stdev(intervals) # 使用示例 stats = pps_interval_stats(pps_timestamps) print("mean_interval", round(stats[0], 4)) print("stddev_interval", round(stats[1], 4))均值应贴近1.000秒,标准差小于5毫秒说明采集端时间戳稳定。如果标准差到了几十毫秒量级,说明中断响应被系统调度抖动干扰,后续用它做轨迹重放或与传感器时间对齐时要谨慎。
更细的验证方法是把每个PPS中断的本地时间与随后第一条GGA语句里的UTC秒字段做差。差值稳定在同一个值附近,说明串口解析和PPS中断之间的延迟基本固定;差值忽大忽小,说明接收端任务调度不稳定,数据的时间归属需要重新标记。
这个验证步骤是我做GPS数据分析前必做的一项。曾经有一次外场采集跑了好几个小时,回来看时间戳标准差大到无法接受,整段数据的时间轴没办法用来做速度对比,等于白跑一趟。多花两分钟做PPS标定,能避免几次白干。希望帮到你。
本文还有配套的精品资源,点击获取