接手这个项目时,手头平台是 Jetson Orin NX16GB,要接的定位模块是联适 R70M-GNSS,目标是让机器人在室外场景稳定拿到串口定位数据。乍一看这事简单:串口嘛,波特率对上就能读。但真正连线调起来才发现,从电平匹配、设备权限、NMEA语句前缀,到长时间运行的丢帧问题,坑一个接一个,光“收不到数据”这一个现象背后就能排出一长串原因。
这篇文章把我这次从硬件接线到数据落地的完整过程写出来,包含可直接抄的配置命令、Python解析代码和踩坑记录。无论你是要给无人车、机器人底盘接GNSS,还是想在Jetson系列板子上做串口传感器接入,这套流程都适用。
1. 项目整体设计:为什么用这块板子配这个模块
1.1 R70M-GNSS模块在项目里的角色
联适R70M-GNSS属于多系统GNSS接收模块,支持GPS、北斗、GLONASS、Galileo等多频段信号接收,对外输出NMEA 0183协议数据,部分版本还支持RTCM原始观测量输出,可以用来做RTK差分定位。
在无人车、农机导航这类项目里,它的角色很明确:提供高精度的WGS84经纬度坐标,配合IMU做组合导航,或者直接给建图定位系统提供绝对位置约束。我这次的核心需求就是先把它的串口输出接进Jetson Orin NX,拿到稳定的经纬度、速度和定位状态,为后续的导航算法铺路。
1.2 为什么优先选串口而不是USB或网口
GNSS模块和主机之间的接口选择,其实取决于你的应用场景。R70M这类模块基本都会引出UART串口,因为NMEA协议本身就是为串口设计的,简单、实时、延迟低。
- 网口/RJ45方案:能跑NTRIP、RTCM等更复杂的双向数据流,但对板子和系统的要求高,需要配IP和端口,调试成本明显上升。
- USB方案:一般是通过模块内置或外接USB转串口芯片实现,优点即插即用,但小厂模块的USB转接稳定性参差不齐,长时间运行偶发掉线的概率比原生UART高。
- 串口方案:一根杜邦线就能通,系统层面的依赖少,中断延迟可控,适合做实时定位。
所以这次选串口作为主链路,USB转串口作为调试辅助,两种线都接了,方便对照排查。
1.3 两种接线方式的取舍:原生UART与USB转串口
Jetson Orin NX16GB的40-pin排针上提供原生UART,但同时它也支持USB HOST接口。实际用下来,这两种方式各有优劣,我列了个表供参考:
| 对比项 | 原生UART(40-pin) | USB转串口(USB-TTL) |
|---|---|---|
| 设备节点 | /dev/ttyTHS1等 | /dev/ttyUSB0 |
| 电平 | 3.3V TTL,注意电平匹配 | 取决于转接芯片,一般也是3.3V |
| 驱动 | 内核自带,加载即用 | 需要芯片驱动,Ubuntu一般内置 |
| 稳定性 | 高,不依赖USB控制器 | 较高,但劣质线材会出问题 |
| 调试便利性 | 一般,节点固定 | 灵活,可以随时拔插换电脑 |
| 长线传输 | 不擅长,抗干扰弱 | USB线抗干扰相对好 |
我个人的习惯是:前期调代码、验证模块输出时用USB转串口,方便拔下来插到电脑上配合串口调试助手看原始数据;等整套逻辑稳定后,再切到40-pin原生UART做正式运行。两条路都走通,后面不管换哪种方式心里都有底。
2. 硬件接线与上电前确认
2.1 在Jetson 40-pin排针上找UART引脚
Jetson Orin NX16GB的40-pin排针定义和树莓派不完全一样,UART位置最好以官方引脚图表为准。我这次用的是URT1对应的引脚:TX在Pin 8,RX在Pin 10,GND在Pin 9附近。连接时遵循交叉规则:模块的TX接板子的RX,模块的RX接板子的TX,GND必须共地。
这里面有个关键点:Jetson的GPIO电平是3.3V,不能直接接RS232电平的设备。R70M的串口输出是TTL电平,可以直接对接,但如果你用的是老式RS232接口的GNSS模块,一定要先过电平转换芯片,否则轻则收不到数据,重则烧坏接口。上电前用万用表量一下引脚电压,这个步骤不能省。
2.2 USB转串口接线时的选材和连线
如果走USB转串口这条路,第一个坑就是线材芯片。CH340能用,但市面上劣质CH340线在长跑时丢数据的情况不少。我在Jetson上测试过,FT232、CP2102这类芯片的表现明显更稳。
接线顺序和原生UART一样:USB转串口的TX接模块RX,RX接模块TX,GND三端共地。注意很多USB转串口线引出的杜邦头颜色不统一,别默认红色就是VCC,黑色就是GND,最好量一下再断言。
还有一点,R70M模组的工作电压通常按厂家手册来,我手上这台是5V供电比较稳定,串口电平虽然还是3.3V,但VCC引脚不能为了省事直接接到Jetson的3.3V上。供电不足会直接导致模块搜星慢、定位不稳定,甚至压根不工作。
2.3 上电前检查清单
接好线后别急着敲命令,先按下面清单过一遍:
- 模块供电是否独立且稳定,Jetson的3.3V/5V输出能力是否满足模块峰值电流。
- TX/RX是否交叉连接,GND是否共地。
- 电平是否匹配,3.3V TTL不能直连RS232。
- GNSS天线是否接好,且是有源天线的话是否已通过模块给天线馈电。
- 波特率参数是否已知,不同批次模块默认波特率可能不同,常见的是115200或9600。
我当时就吃过“天线没拧紧”的亏,看着串口有数据输出,但GGA语句里的卫星数永远是0,折腾半天才发现是天线接头松了。
3. 系统配置与串口调试
3.1 确认设备节点与驱动加载
插上USB转串口后,先在终端里确认系统有没有识别到设备:
lsusb dmesg | grep -i tty ls -l /dev/ttyUSB* /dev/ttyTHS*正常情况能看到类似下面的输出:
/dev/ttyUSB0如果是原生UART,节点名是/dev/ttyTHS1。这里容易踩的坑是Jetson的调试串口默认占用了ttyTHS0,你如果对着ttyTHS0去读,读出来的全是系统开机日志,压根不是GNSS数据。第一次用40-pin UART时不认识的,就先查一下Pin 8/Pin 10对应的设备节点再动手。
3.2 权限配置与udev固定设备节点
Jetson默认的串口文件属于dialout组,如果你的用户不在这个组里,打开串口会报Permission denied。先把用户加进去,然后注销重新登录:
sudo usermod -aG dialout $USER设备节点漂移也是个烦人的问题,USB转串口每次插拔,节点顺序可能从ttyUSB0变成ttyUSB1。写程序时如果硬编码ttyUSB0,下次开机就可能串到别的设备上。用udev规则固定节点名:
sudo nano /etc/udev/rules.d/99-gnss.rules内容按实际芯片参数写,CH340和FT232的ID不一样:
KERNEL=="ttyUSB*", SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="gnss"保存后重载规则:
sudo udevadm control --reload sudo udevadm trigger之后插入设备,无论它被分到ttyUSB几,都会生成一个/dev/gnss的软链接。程序里固定用/dev/gnss,排查问题时也能一眼认出是哪条线。
3.3 用串口调试工具验证原始数据流
写任何解析代码之前,先确认原始数据完整。用minicom是最直接的:
sudo minicom -D /dev/ttyUSB0 -b 115200如果波特率对、接线对、模块已定位,屏幕上会滚动输出类似这样的NMEA语句:
$GNGGA,092734.00,2234.56789,N,11356.12345,E,1,12,0.8,45.2,M,-2.1,M,,*5B $GNRMC,092734.00,A,2234.56789,N,11356.12345,E,0.2,128.3,051124,,,A*60如果屏幕上全是乱码,多半是波特率不对,试试9600再观察。如果屏幕干干净净什么输出都没有,优先检查TX/RX是否交叉、GND是否共地。这一步不要直接上Python脚本,用串口调试助手看清楚数据长什么样,后面写解析逻辑才有方向。
如果手头没有真实模块,可以用socat虚拟出一对串口,配合串口调试助手模拟数据源,先把解析代码跑通,再拿到实机上验证,调试效率会高很多。
4. Python读取并解析定位数据
4.1 GGA与RMC语句字段速查
NMEA 0183里我们最关心的是GGA和RMC两种句子。GGA包含经纬度、定位状态、卫星数和海拔,RMC包含速度、航向和有效状态。以GGA为例:
| 字段序号 | 示例值 | 含义 |
|---|---|---|
| 0 | $GNGGA | 语句头 |
| 1 | 092734.00 | UTC时间 时分秒 |
| 2 | 2234.56789 | 纬度,度分格式 |
| 3 | N | 北纬 |
| 4 | 11356.12345 | 经度,度分格式 |
| 5 | E | 东经 |
| 6 | 1 | 定位质量,1=单点定位,2=差分,4=RTK固定 |
| 7 | 12 | 参与定位的卫星数 |
| 8 | 0.8 | HDOP水平精度因子 |
| 9 | 45.2 | 海拔高度 |
| 10 | M | 高度单位 |
注意NMEA里的经纬度是“度分”格式:2234.56789表示22度34.56789分,换算成十进制度需要除以100取整得到度,余数除以60加到度上:
dd = int(raw / 100) mm = raw - dd * 100 decimal_degrees = dd + mm / 60.0这个换算功能是解析代码里最容易写错的地方,很多人直接把整个数字当成度,位置偏出去几十公里。
4.2 用pyserial实现一个简洁的GGA解析器
先安装依赖:
pip install pyserial下面是一个可以直接跑的读取循环,串口设备名、波特率、解析逻辑都集中在代码里:
import serial def convert_deg_min(raw): if not raw or raw == '': return None val = float(raw) deg = int(val / 100) minutes = val - deg * 100 return round(deg + minutes / 60.0, 8) def parse_gga(line): parts = line.split(',') if len(parts) < 10: return None lat = convert_deg_min(parts[2]) lon = convert_deg_min(parts[4]) quality = int(parts[6]) if parts[6] else -1 sats = int(parts[7]) if parts[7] else 0 hdop = float(parts[8]) if parts[8] else 99.0 altitude = float(parts[9]) if parts[9] else 0.0 return { 'lat': lat if parts[3] == 'N' else -lat, 'lon': lon if parts[5] == 'E' else -lon, 'quality': quality, 'sats': sats, 'hdop': hdop, 'altitude': altitude } ser = serial.Serial('/dev/gnss', 115200, timeout=0.2) while True: raw_line = ser.readline() try: text = raw_line.decode('ascii', errors='ignore').strip() except Exception: continue if text.startswith('$GNGGA'): data = parse_gga(text) if data: print(data)解释几个关键点:
- serial.Serial的timeout设为0.2秒,避免readline长时间阻塞。
- decode时用errors='ignore',防止偶发的半个字节导致整个解析崩溃。
- 判断用的是startswith('$GNGGA')而不是startswith('$GPGGA'),这一点非常关键,下面展开说。
4.3 多系统语句前缀的坑:GP还是GN
很多教程里的NMEA解析都是拿GPS单系统设备写的,过滤的是$GPGGA。但联适R70M这种多系统模块,默认输出的往往是$GNGGA,因为“GN”代表GPS+北斗等多系统联合定位。如果你按老教程只认$GPGGA,程序会一直在那空转,一帧数据都进不来。
我调试的时候亲眼见过两种前缀切换:模块设置为单独GPS模式时输出GPGGA,设置为多系统融合时输出GNGGA。所以解析代码里最好同时兼容:
if text.startswith(('$GNGGA', '$GPGGA')):同理,RMC前缀可能是GNRMC或GPRMC。兼容处理,能省掉一晚上的排查时间。
5. 踩坑记录与排查清单
5.1 常见问题速查表
把这次调试遇到过的典型问题和对应解法整理成了一张表,方便现场对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 串口无任何输出 | 接线错误/供电不足/波特率错 | 量电压,确认TX/RX交叉、检共地,换波特率 |
| 输出乱码 | 波特率不匹配 | 115200与9600互换测试 |
| 有输出但卫星数永远为0 | 天线没接好/天线馈电缺失 | 检查天线接头、确认模块是否给天线供电 |
| GGA能读到但quality字段为0 | 室内或遮挡严重 | 把天线放室外开阔地,等待搜星 |
| 数据突然中断 | USB转串口不稳定/线材劣质 | 换FT232线材,或切回原生UART |
| 设备节点漂移 | udev规则未配置 | 按上文配置固定软链接 |
| 定位精度很差 | HDOP高/处于RTK浮点解 | 看卫星数和HDOP,检查天线位置 |
这张表看起来简单,但每一项背后都是实打实的花时间换来的教训。
5.2 Linux串口接收数据丢失的实测经验
长时间运行后发现另一个隐蔽问题:Linux下串口读取偶发丢数据。现象是程序跑一两个小时,GPS坐标会跳一下,或者某几帧输出缺失,但模块本身输出是正常的。
排查下来有两个重点:
- USB转串口芯片的latency timer默认值偏大,数据会在芯片里攒一会儿再批量交上来,实时性受影响。FT232线可以在系统层面把latency调到1:
echo 1 | sudo tee /sys/bus/usb-serial/devices/ttyUSB0/latency_timer- Python侧用readline时,如果系统缓冲区积压太多,老数据会覆盖新数据。这时候应该用in_waiting配合read读取缓冲区内所有可用字节,再做按行处理:
if ser.in_waiting > 0: data = ser.read(ser.in_waiting)这个做法适合数据量稳定、语句节奏固定的场景。不要把读取逻辑搞得太复杂,先确保每一条GGA都能进入解析流程,再考虑加缓冲队列。
5.3 天线与定位质量的细节
GNSS定位的质量,很大程度取决于天线端。R70M这类模块通常接有源天线,天线需要模块的引脚馈电才能工作。如果天线没供电,串口数据依然会输出,但卫星数一直是0,这就是真正的“假在线”。
天线位置也直接影响定位精度。窗台上能收到卫星,但信号多路径效应明显,HDOP能飙到3以上。我实际测试的体会是:天线放在室外空旷处,头顶无遮挡时,HDOP基本能控制在1以内;放在室内靠窗位置,虽然也能定位,但漂移明显变大。
RTK模式下,GGA语句的quality字段有更细的区分:4表示RTK固定解,5表示RTK浮点解。固定解的定位精度能达到厘米级,浮点解则在分米级到米级之间浮动。程序里要记录这个状态,因为它决定了当前定位数据是否满足导航算法要求。
6. 进阶用法:把定位数据接进ROS2 Humble
6.1 用pyserial写一个NavSatFix发布节点
项目要在机器人上跑导航的话,定位数据最终要接入ROS,Jetson Orin NX16GB上装的是ROS2 Humble。用pyserial把串口数据读出来后,转换成sensor_msgs/NavSatFix消息发布,是最快的桥接方式:
import rclpy from rclpy.node import Node from sensor_msgs.msg import NavSatFix import serial class GnssPublisher(Node): def __init__(self): super().__init__('gnss_publisher') self.pub = self.create_publisher(NavSatFix, 'fix', 10) self.ser = serial.Serial('/dev/gnss', 115200, timeout=0.2) self.timer = self.create_timer(0.1, self.timer_callback) def timer_callback(self): while self.ser.in_waiting: raw_line = self.ser.readline() try: text = raw_line.decode('ascii', errors='ignore').strip() except Exception: continue if text.startswith('$GNGGA'): data = parse_gga(text) if data: msg = NavSatFix() msg.latitude = data['lat'] msg.longitude = data['lon'] msg.altitude = data['altitude'] msg.status.status = 2 if data['quality'] >= 1 else 0 msg.position_covariance_type = 0 self.pub.publish(msg)这段代码保持了单纯的“串口桥接”角色,不引入复杂的驱动框架。对于快速原型验证、数据记录、后续算法输入已经足够。运行前记得给设备加dialout权限。
6.2 后续可以扩展的方向
拿到稳定经纬度后,项目的路就宽了。比如把GNSS数据和IMU做融合,解决树下、桥下、高楼旁短暂丢星的场景。更常见的是接NTRIP服务做实时RTK差分,让定位精度从米级直接压到厘米级,R70M这类模块本身支持外接差分数据,串口和网络口都可以喂RTCM。再往下做,就是把这些数据落盘记录轨迹,配合地图做路径回放。
我目前的进度是数据稳定接入ROS2,下一步计划把RTK差分链路建立起来,然后融合IMU做建图定位。每个新功能的推进,底层还是同一个串口数据流,所以把最底层的链路调稳,整个系统后面都受益。
实际调下来的整体感受是:GNSS模块本身不复杂,真正花时间的是硬件确认、设备权限和格式差异处理。建议所有拿到新模块的人,第一步永远是插上串口调试助手看原始输出,把能打的数据一帧一帧看清楚,再写解析代码。跑通之后,务必用udev固定设备名,不然重启一次节点名漂移,等到现场再找ttyUSB几就真的麻烦了。