行空板Python实战:GNSS模块连接与NMEA协议解析全攻略
2026/7/28 9:24:33 网站建设 项目流程

1. 项目缘起:为什么要在行空板上折腾卫星导航?

最近在捣鼓一个户外数据采集的小项目,核心需求是设备能自主记录精确的地理位置和时间戳。市面上现成的GPS模块和开发板方案很多,但要么体积功耗感人,要么二次开发不够灵活。直到我看到了行空板,这块集成了丰富传感器和Python环境的国产开源硬件,让我眼前一亮——它本身没有GPS,但预留了标准的UART串口,这不正好是连接外置GNSS模块的绝佳平台吗?

用Python在行空板上实现卫星导航,听起来像是个“缝合怪”项目,但实际意义不小。行空板基于高性能微控制器,运行着阉割版的MicroPython,其交互性和易用性远超传统的嵌入式C开发。这意味着,你可以用写脚本的方式,快速解析NMEA协议、处理经纬度数据、甚至结合板载的Wi-Fi/蓝牙做数据回传或地理围栏,开发效率是指数级提升。对于物联网原型验证、教育实验、或者需要快速集成定位功能的创客项目来说,这无疑是一条捷径。

不过,这条路也并非一片坦途。MicroPython的生态和性能与桌面Python有差距,GNSS模块的选型、供电、天线摆放都直接影响定位效果,NMEA数据的实时解析与处理也需要一些技巧。接下来,我就结合自己的踩坑经历,把从硬件连接到软件解析,再到数据应用的完整链条拆解清楚。

2. 硬件选型与连接:不只是“接上线”那么简单

实现卫星导航功能,硬件是地基。行空板作为主机,我们需要为其挑选一个合适的“眼睛”——GNSS模块。

2.1 GNSS模块怎么选?

市面上常见的模块主要分两类:纯GPS模块和支持多模(如GPS+北斗+GLONASS)的GNSS模块。对于国内项目,强烈建议选择支持北斗的模块,因为在国内上空北斗卫星的可见性和信号强度通常更有优势,定位速度和精度会更好。我这次选用的是ATGM336H,这是一款性价比很高的国产模块,支持GPS、北斗、GLONASS、Galileo、QZSS五大系统。

除了定位系统,还要关注几个关键参数:

  • 刷新率:常见有1Hz、5Hz、10Hz。对于车辆导航、无人机等高速移动物体,高刷新率很重要。但对于大多数数据记录应用,1Hz完全足够,功耗也更低。
  • 定位精度:模块标称的定位精度(如2.5米CEP)只是一个理想值。实际精度受天线、环境、天空开阔度影响巨大。
  • 通信协议:绝大多数模块都通过UART串口输出标准的NMEA-0183协议数据,这是我们的软件能够解析的前提。需要确认模块的默认波特率,常见的是9600或115200。
  • 供电电压:行空板的GPIO口输出电压通常是3.3V,务必选择支持3.3V逻辑电平的模块,否则需要电平转换。

注意:购买模块时,最好选择已经焊接好邮票孔或排针的版本,并顺便买一条配套的“GPS有源天线”。天线至关重要,其增益和性能直接决定了能否在室内或城市峡谷中收到信号。

2.2 与行空板的物理连接

行空板提供了多个UART接口,我们通常使用UART1UART2,因为它们对应的引脚已引出到边缘的Grove接口或排针上,方便连接。以使用UART1为例,它的引脚对应关系是:

  • TX(行空板发送端) -> 连接 GNSS模块的RX(接收端)
  • RX(行空板接收端) -> 连接 GNSS模块的TX(发送端)
  • GND-> 连接 GNSS模块的GND
  • 3V3-> 连接 GNSS模块的VCC

这里有一个极易出错的点:交叉连接。行空板的TX应该接模块的RX,行空板的RX接模块的TX,这样数据才能正确收发。接反了会导致通信完全失败。我的习惯是,先用杜邦线连接好,再用万用表通断档确认TX-RX是交叉相连的。

供电方面,务必确认模块的电流需求。大部分GNSS模块在工作时峰值电流可能达到50mA,行空板的3.3V输出引脚需要能提供足够的电流。将天线连接到模块的“天线接口”上,并尽可能将天线放置在金属遮挡少、天空视野开阔的位置。车窗边、屋顶、户外是理想环境。

3. 软件环境搭建与串口通信

硬件连通后,下一步就是在行空板的MicroPython环境中,编写代码来读取数据。

3.1 行空板Python环境初探

行空板开机后,可以通过USB连接电脑,使用Mu Editor、Thonny等支持MicroPython的IDE进行编程。首先,我们需要导入必要的机器(machine)模块来操作硬件。

from machine import UART, Pin import time

UART类用于串口通信,Pin类用于控制引脚(虽然本项目不一定用到,但常备无患),time用于延时和计时。

3.2 初始化UART并读取原始数据

初始化UART1,参数需要与你的GNSS模块匹配。假设模块波特率为9600。

# 初始化UART1,波特率9600,8位数据位,无校验,1位停止位 # TX引脚是行空板上的Pin.8, RX引脚是Pin.9(根据行空板具体型号确认) uart1 = UART(1, baudrate=9600, tx=Pin(8), rx=Pin(9)) uart1.init(9600, bits=8, parity=None, stop=1, timeout=1000)

timeout参数设置为1000毫秒很重要,它指定了读取数据时的超时时间。设置一个合理的超时可以避免read()函数一直阻塞。

接下来,我们可以尝试循环读取原始数据:

while True: if uart1.any(): # 检查是否有数据在缓冲区 raw_data = uart1.read() # 读取所有可用数据 if raw_data: # 将字节数据转换为字符串 try: data_string = raw_data.decode('utf-8') print(data_string, end='') except UnicodeDecodeError: # 偶尔会有乱码,直接跳过 pass time.sleep(0.1) # 短暂休眠,避免CPU占用率100%

运行这段代码,如果一切正常,你应该在串口终端里看到源源不断的、以$符号开头的文本数据流,例如:

$GNGGA,114153.00,3110.76123,N,12123.45678,E,1,08,1.01,45.6,M,,M,,*6A $GNRMC,114153.00,A,3110.76123,N,12123.45678,E,0.05,120.5,270324,,,A*7D

这就是NMEA-0183协议数据。每一行称为一个“语句”,以逗号分隔不同字段,以*后跟两位校验和结束。不同的语句前缀(如GGA,RMC)包含了不同类型的信息。

3.3 处理串口读取中的坑

在实际操作中,直接read()可能会遇到问题。因为NMEA数据是持续不断发送的,read()函数可能会在一条语句的中间被调用,导致读到的数据是半条语句,解析时就会出错。

更稳健的做法是使用readline()方法,它会一直读取,直到遇到换行符\n,从而保证每次获取的都是一条完整的NMEA语句。修改上面的读取部分:

while True: # 使用 readline 确保读取完整的一行 raw_line = uart1.readline() if raw_line: try: line = raw_line.decode('utf-8').strip() if line.startswith('$'): # 简单过滤有效数据 print(line) # 这里可以调用解析函数 # parse_nmea_sentence(line) except Exception as e: print("Decode error:", e) time.sleep(0.05)

使用readline()并检查起始字符$,能极大地提高数据处理的可靠性。

4. NMEA协议解析:从字符串到有意义的信息

拿到了原始的NMEA字符串,下一步就是从中提取出我们关心的经纬度、时间、速度、卫星数等信息。我们需要根据不同的语句类型来编写解析函数。

4.1 认识核心NMEA语句

对于基础定位应用,最常用的两条语句是:

  1. GxGGA(Global Positioning System Fix Data):全球定位系统定位信息。它提供了最核心的定位结果,包括时间、纬度、经度、定位状态、使用的卫星数量、水平精度因子(HDOP)、海拔高度等。x可以是N(GNSS)、P(GPS)等,取决于模块输出配置。
  2. GxRMC(Recommended Minimum Specific GNSS Data):推荐最小定位信息。它包含了时间、定位状态、纬度、经度、地面速率(速度)、地面航向(对地真北方向角)、日期、磁偏角等。RMC语句的特点是包含了日期和速度,对于轨迹记录应用非常有用。

4.2 编写GGA语句解析函数

我们来详细解析一条GNGGA语句:$GNGGA,114153.00,3110.76123,N,12123.45678,E,1,08,1.01,45.6,M,,M,,*6A

字段索引示例值含义
0$GNGGA语句头
1114153.00UTC时间,格式为hhmmss.ss
23110.76123纬度,格式为ddmm.mmmmm
3N纬度半球,N(北纬)或S(南纬)
412123.45678经度,格式为dddmm.mmmmm
5E经度半球,E(东经)或W(西经)
61定位状态,0=无效,1=单点定位,2=差分定位等
708正在使用的卫星数量
81.01水平精度因子(HDOP),值越小精度越高
945.6海拔高度
10M海拔高度单位,米
......后续字段(大地水准面起伏等)常为空

注意,经纬度的格式是“度分”格式。需要将其转换为通用的十进制度数。转换公式为:十进制度数 = 度 + 分 / 60例如:3110.76123-> 度 =31,分 =10.76123-> 十进制纬度 =31 + 10.76123/60 = 31.17935383

基于此,我们可以编写解析函数:

def parse_gga(nmea_line): """解析GGA语句""" if not nmea_line.startswith('$GNGGA') and not nmea_line.startswith('$GPGGA'): return None # 不是GGA语句 parts = nmea_line.split(',') if len(parts) < 10: return None try: # 解析时间 utc_time = parts[1] if utc_time: hour = int(utc_time[0:2]) minute = int(utc_time[2:4]) second = float(utc_time[4:]) time_str = f"{hour:02d}:{minute:02d}:{second:05.2f}" else: time_str = None # 解析纬度 lat_str = parts[2] lat_dir = parts[3] if lat_str and lat_dir: lat_deg = float(lat_str[0:2]) lat_min = float(lat_str[2:]) latitude = lat_deg + lat_min / 60.0 if lat_dir == 'S': latitude = -latitude else: latitude = None # 解析经度 lon_str = parts[4] lon_dir = parts[5] if lon_str and lon_dir: lon_deg = float(lon_str[0:3]) # 经度度数是3位 lon_min = float(lon_str[3:]) longitude = lon_deg + lon_min / 60.0 if lon_dir == 'W': longitude = -longitude else: longitude = None # 定位状态 fix_status = int(parts[6]) if parts[6] else 0 # 卫星数量 satellites = int(parts[7]) if parts[7] else 0 # HDOP hdop = float(parts[8]) if parts[8] else 99.9 # 海拔 altitude = float(parts[9]) if parts[9] else 0.0 return { 'sentence': 'GGA', 'time': time_str, 'latitude': latitude, 'longitude': longitude, 'fix_status': fix_status, # 0=无效,1=有效 'satellites_used': satellites, 'hdop': hdop, 'altitude': altitude, 'raw': nmea_line } except (ValueError, IndexError) as e: print(f"解析GGA语句出错: {e}, 原始数据: {nmea_line}") return None

4.3 编写RMC语句解析函数

同样地,我们解析GNRMC语句:$GNRMC,114153.00,A,3110.76123,N,12123.45678,E,0.05,120.5,270324,,,A*7D

字段索引示例值含义
0$GNRMC语句头
1114153.00UTC时间
2A状态,A=有效定位,V=无效定位
33110.76123纬度
4N纬度半球
512123.45678经度
6E经度半球
70.05地面速率(节)
8120.5地面航向(度)
9270324UTC日期,格式为ddmmyy
10``磁偏角(通常为空)
11``磁偏角方向
12A模式指示,A=自主定位,D=差分等

RMC解析函数与GGA类似,但增加了速度、航向和日期的解析。速度单位是“节”(knots),1节=1.852公里/小时,通常我们需要转换一下。

def parse_rmc(nmea_line): """解析RMC语句""" if not nmea_line.startswith('$GNRMC') and not nmea_line.startswith('$GPRMC'): return None parts = nmea_line.split(',') if len(parts) < 13: return None try: # 状态检查 status = parts[2] if status != 'A': return {'sentence': 'RMC', 'status': 'Invalid', 'raw': nmea_line} # 解析时间 (同GGA) # 解析经纬度 (同GGA,代码复用略) # ... # 地面速率(节 -> 米/秒) speed_knots = float(parts[7]) if parts[7] else 0.0 speed_ms = speed_knots * 0.514444 # 转换系数 # 地面航向 course = float(parts[8]) if parts[8] else 0.0 # 解析日期 date_str = parts[9] if date_str: day = int(date_str[0:2]) month = int(date_str[2:4]) year = 2000 + int(date_str[4:6]) # 假设是20xx年 date_formatted = f"{year:04d}-{month:02d}-{day:02d}" else: date_formatted = None # 模式指示 mode = parts[12][0] if parts[12] else 'N' # N=无数据 return { 'sentence': 'RMC', 'status': 'Valid', 'time': time_str, # 从前面解析得来 'latitude': latitude, 'longitude': longitude, 'speed_knots': speed_knots, 'speed_ms': speed_ms, 'course': course, 'date': date_formatted, 'mode': mode, 'raw': nmea_line } except (ValueError, IndexError) as e: print(f"解析RMC语句出错: {e}") return None

4.4 整合解析与主循环

现在,我们将读取和解析整合到一个主循环中,并设计一个简单的数据管理器,可以选择性地输出或存储我们需要的信息。

class SimpleGPS: def __init__(self, uart_port=1, baudrate=9600): self.uart = UART(uart_port, baudrate=baudrate, tx=Pin(8), rx=Pin(9)) self.uart.init(baudrate, timeout=1000) self.last_fix = {'latitude': None, 'longitude': None, 'time': None, 'altitude': None} self.current_data = {} def update(self): """读取并解析最新的一行数据""" raw_line = self.uart.readline() if not raw_line: return False try: line = raw_line.decode('utf-8').strip() except UnicodeDecodeError: return False # 根据语句类型调用不同的解析器 if line.startswith('$GNGGA') or line.startswith('$GPGGA'): data = parse_gga(line) if data and data['fix_status'] > 0: # 定位有效 self.last_fix.update({ 'latitude': data['latitude'], 'longitude': data['longitude'], 'time': data['time'], 'altitude': data['altitude'] }) self.current_data['gga'] = data return True elif line.startswith('$GNRMC') or line.startswith('$GPRMC'): data = parse_rmc(line) if data and data['status'] == 'Valid': self.current_data['rmc'] = data # 可以用RMC的数据更新位置(如果GGA没收到) if self.last_fix['latitude'] is None: self.last_fix.update({ 'latitude': data['latitude'], 'longitude': data['longitude'], 'time': data['time'] }) return True return False def get_last_fix(self): """获取最后一次有效定位信息""" return self.last_fix.copy() # 主程序 gps = SimpleGPS() print("开始监听GPS数据...") try: while True: if gps.update(): fix = gps.get_last_fix() if fix['latitude']: print(f"位置: {fix['latitude']:.6f}, {fix['longitude']:.6f}, 时间: {fix['time']}, 海拔: {fix['altitude']}m") time.sleep(0.1) # 控制循环频率 except KeyboardInterrupt: print("\n程序退出")

这个简单的类封装了串口通信和基础解析,并维护了最后一次有效定位的信息。在主循环中,我们不断调用update()方法,一旦解析到有效数据,就打印出来。

5. 进阶应用与性能优化

基础解析跑通后,我们可以基于此构建更实用的应用,并解决一些实际遇到性能问题。

5.1 应用场景一:轨迹记录器

一个典型的应用是将定位数据连同传感器数据(如行空板上的加速度计、温湿度传感器)一起,以固定时间间隔记录到SD卡或文件中。

import json import os def log_track_point(gps_data, sensor_data=None, filename='/sd/track.log'): """记录一个轨迹点到文件""" log_entry = { 'timestamp': time.time(), # 系统时间戳 'utc_time': gps_data.get('time'), 'lat': gps_data.get('latitude'), 'lon': gps_data.get('longitude'), 'alt': gps_data.get('altitude'), 'speed': gps_data.get('speed_ms'), 'course': gps_data.get('course'), 'hdop': gps_data.get('hdop'), 'sats': gps_data.get('satellites_used') } if sensor_data: log_entry.update(sensor_data) # 以JSON格式追加写入,方便后续处理 try: with open(filename, 'a') as f: f.write(json.dumps(log_entry) + '\n') except OSError: print("写入文件失败,请检查SD卡")

在主循环中,可以设置一个定时器,例如每5秒记录一次有效定位点,避免数据过于密集。同时,要注意文件系统的写入操作比较耗时,频繁写入可能会阻塞GPS数据的读取,导致数据丢失。可以考虑使用缓冲区,积累一定数量的点后再批量写入。

5.2 应用场景二:简单的导航与地理围栏

利用解析出的经纬度,可以计算两点之间的距离(如使用Haversine公式),或者判断当前点是否进入/离开某个预设的圆形或矩形区域(地理围栏)。这对于资产追踪、电子打卡等应用非常有用。

import math def haversine_distance(lat1, lon1, lat2, lon2): """计算两个经纬度坐标之间的球面距离(米)""" R = 6371000 # 地球平均半径,单位米 phi1 = math.radians(lat1) phi2 = math.radians(lat2) delta_phi = math.radians(lat2 - lat1) delta_lambda = math.radians(lon2 - lon1) a = math.sin(delta_phi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(delta_lambda/2)**2 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) return R * c def in_geofence(current_lat, current_lon, fence_center_lat, fence_center_lon, radius_m): """判断当前点是否在圆形地理围栏内""" distance = haversine_distance(current_lat, current_lon, fence_center_lat, fence_center_lon) return distance <= radius_m

5.3 性能优化与常见问题排查

在MicroPython环境下,性能是需要考虑的问题。解析NMEA字符串、进行浮点数计算(如Haversine公式)都是CPU密集型操作。

  • 优化解析逻辑:在主循环中,避免在每次update时都进行完整的字符串分割和浮点转换。可以先进行简单的头部检查(startswith),只有确定是需要处理的语句类型,才进行完整解析。
  • 使用_thread模块(谨慎):对于更复杂的应用,可以考虑使用_thread模块创建一个单独的线程来负责读取串口数据并放入队列,主线程从队列中取出数据进行处理和记录。但这会显著增加代码复杂度,且MicroPython的线程支持有限,容易引入同步问题。对于大多数应用,单线程配合非阻塞读取(readlinewith timeout)已经足够。
  • 内存管理:持续运行的程序要注意内存泄漏。避免在循环中不断创建大的对象(如长列表)。使用gc.collect()可以手动触发垃圾回收,但不宜过于频繁。
  • 冷启动与热启动:GNSS模块在完全断电后首次定位(冷启动)可能需要几十秒到几分钟。如果只是短暂断电(热启动),因为星历数据部分保存,定位会快很多。在程序初始化后,需要有一个等待有效定位的状态判断,而不是立即认为定位失败。
  • “漂移”与精度问题:静态时坐标在小范围内跳动是正常现象,这是由卫星信号的多路径效应、大气延迟等造成的。可以通过软件滤波来平滑数据,例如取一段时间内的坐标平均值。HDOP值是一个很好的参考,HDOP>2.0时,定位精度会显著下降,此时的数据可信度较低。

6. 天线摆放、供电与信号质量提升实战

硬件部署是决定项目成败的最后一步,也是新手最容易忽略的一环。

6.1 天线:定位系统的“耳朵”

GNSS天线分为有源和无源两种。有源天线内部集成了低噪声放大器(LNA),能显著增强微弱的卫星信号,是绝大多数模块的标配。使用时请注意:

  1. 朝向天空:天线接收面(通常是陶瓷片一面)必须朝向天空,尽可能无遮挡。金属物体会严重屏蔽信号,所以不要放在金属盒内或紧贴金属表面。
  2. 远离干扰源:尽量远离行空板、电源模块、电机驱动器等可能产生电磁噪声的部件。可以将天线通过延长线引到设备外部。
  3. 固定牢固:在移动应用中,天线的晃动会影响信号接收。使用磁吸底座或3M胶将其牢固粘贴在车顶、外壳顶部等位置。

6.2 供电稳定性的影响

GNSS模块对电源噪声非常敏感。不干净的电源会导致模块工作不稳定,甚至无法定位。

  • 独立LDO供电:如果条件允许,不要直接从行空板的3.3V引脚取电,而是使用一个独立的低压差线性稳压器(LDO)为GNSS模块供电。这能有效隔离数字电路的噪声。
  • 电源去耦:在模块的VCC和GND引脚之间,尽可能靠近引脚的地方,焊接一个10uF的钽电容和一个0.1uF的陶瓷电容,用于滤除高低频噪声。很多模块板上已经集成,如果没有,自己加上会大大提升稳定性。
  • 检查电压:用万用表测量模块供电引脚的实际电压,确保在额定范围内(如3.3V±0.2V)。电压过低会导致模块无法启动或频繁重启。

6.3 实战信号调试技巧

当你发现定位慢、卫星数少、HDOP值高时,可以按以下步骤排查:

  1. 查看原始NMEA数据:除了GGARMC,让模块输出GxGSV语句(查看可见卫星信息)。这条语句会列出天空中可以“看到”的每颗卫星的编号、仰角、方位角和信噪比(SNR)。信噪比是衡量信号质量的关键,一般高于40dB-Hz算好信号,低于30dB-Hz可能无法用于定位。通过GSV语句,你可以直观地看到哪些方向的卫星信号弱。
  2. 改变天线位置:这是提升效果最直接的方法。哪怕只是将天线从桌面移到窗台,卫星数和信噪比都可能大幅改善。
  3. 耐心等待:冷启动后,模块需要时间下载星历数据。首次定位请给予至少1-2分钟时间。可以在程序中加入一个“等待有效定位”的循环,直到fix_status变为1或GGA语句中的卫星数大于等于4再开始记录数据。
  4. 检查波特率:确保行空板UART的波特率与模块输出的波特率完全一致。如果不确定,可以尝试常见的9600, 19200, 38400, 57600, 115200。有些模块支持通过特定指令修改波特率,但需要查阅其数据手册。

通过以上从硬件到软件,从原理到实战的完整梳理,你应该已经掌握了在行空板上用Python玩转卫星导航的核心技能。这个过程本质上是一个典型的嵌入式传感器集成案例,其思路——硬件连接、协议解析、数据处理、应用开发——可以迁移到很多其他传感器上。最关键的是动手去试,把模块接上,看着终端里滚动的数据变成屏幕上的坐标点时,那种成就感才是创客项目最大的乐趣。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询