ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Jetson Orin NX接入GNSS接收机:串口读取NMEA与RTK定位实践

Jetson Orin NX接入GNSS接收机:串口读取NMEA与RTK定位实践 把户外移动机器人的定位方案定到“Jetson Orin NX 16GB 联适R70M-GNSS 串口获取定位数据”这条路线是最近一个月我做的最顺手的一次传感器接入。之前用过树莓派、也用过工控机接GNSS模块但这次换成Jetson Orin NX之后发现整个流程里有不少细节和之前x86平台完全不同比如串口设备节点的稳定性、权限管理、ROS2环境下的数据对接方式。这篇文章就把我从接线、驱动、读取到解析NMEA数据的完整过程记录下来给同样想在Orin平台上接GNSS接收机的朋友做个参考。联适R70M-GNSS本身是一台支持RTK差分定位的GNSS接收机输出协议常见的是NMEA 0183也支持部分二进制指令。而Jetson Orin NX 16GB是英伟达的嵌入式AI计算平台JetPack系统底层是Ubuntu所以读取串口数据这件事本质上还是Linux串口编程那一套但嵌入式平台上的设备节点、权限、电平转换这些坑比台式机要多。这篇文章适合正在做机器人导航、无人车定位、移动测绘的开发者也适合刚拿到Orin NX想快速验证传感器通信的朋友。1. 为什么要把GNSS接到Orin NX上从定位需求说起1.1 轮式里程计和IMU在室外场景的致命弱点做移动机器人的朋友应该都有这种体会室内用激光雷达里程计跑SLAM效果可以做到很不错但一旦拉到室外比如园区道路、矿山、农田这些场景轮式里程计打滑、IMU积分漂移、激光特征稀疏位姿很容易就跑飞了。我之前的方案是激光雷达建图加IMU融合短时间精度还行但跑个几百米之后误差就开始累积地图越来越歪最后只能手动把人找回来重新初始化。室外定位必须引入绝对坐标信息而GNSS就是最直接的绝对定位源。普通手机级别的GNSS单点定位精度大概是3到5米做导航勉强能看但做不了车道级或者厘米级。这时候就需要RTKReal Time Kinematic差分定位。联适R70M-GNSS这类接收机拿到RTK差分数据后水平精度可以到厘米级正好满足室外机器人作业的需求。Jetson Orin NX 16GB在这里的角色是边缘计算主机。它既要读GNSS数据还要跑路径规划、视觉感知、和上位机通信16GB内存版本还能带得动一些轻量的深度学习模型。所以整套架构就是GNSS天线接收卫星信号R70M接收机解算位置通过串口把定位结果发给Orin NXOrin NX再把这些数据用于导航或者与IMU、激光雷达做融合定位。1.2 一套完整的串口GNSS定位链路包含哪些环节从信号到可用坐标整条链路其实比想象中长卫星信号从太空到达地面经过电离层、对流层延迟到达GNSS天线。天线把信号传给R70M接收机接收机内部完成捕获、跟踪、解算输出定位结果。R70M通过串口RS232或TTL把NMEA格式的语句发出来。串口线经过电平转换到USB或者直接接到Orin NX的40pin UART。系统层通过/dev/ttyUSB0或/dev/ttyTHS1设备节点读取字节流。应用层解析NMEA语句提取经纬度、高程、定位精度、卫星数等字段。这里面任何一个环节出错最后都拿不到干净的数据。比如天线没放到开阔地、串口波特率不匹配、设备节点没权限都会导致问题表现诡异有时是完全无数据有时是乱码有时是数据断断续续。下面我按实际操作的顺序从硬件接线开始把整个过程拆开讲。2. 硬件接线R70M-GNSS与Orin NX的串口连通方案2.1 R70M的对外输出接口与电平标准联适R70M-GNSS接收机在典型配置下通过DB9或者航插接口输出串口信号标称电平有两种情况需要区分RS232电平和TTL电平。RS232电平是正负电压摆动比如-12V表示112V表示0TTL电平是0到3.3V或者0到5V用高电平表示1低电平表示0。这两者不能直接互连否则轻则收不到数据重则烧坏芯片。Jetson Orin NX上的40pin扩展接口UART引脚是TTL电平1.8V或3.3V取决于具体引脚和配置。所以接线时优先考虑用USB转TTL的方式而不是直接拿R70M的RS232输出接到Orin NX的UART引脚。如果用RS232输出中间还需要一块RS232转TTL的电平转换板比如MAX3232方案多一层转换就多一层出问题的概率我第一次实验时就因为偷懒没接转换直接导致数据全乱码。下表是这几种电平的对比方便大家对照手上的设备电平类型信号幅度常见接口是否可直接接Jetson UARTRS232正负3V~15VDB9串口不可以需要电平转换TTL 3.3V0~3.3V4pin插针、蓝牙模块可以TTL 5V0~5V常见USB转TTL模块不推荐5V可能损坏引脚USB差分信号USB-A / Micro-USB可以通过USB转串口芯片2.2 USB转串口方案 vs 直接使用40pin UART在Orin NX上接串口设备有两套主流方案。第一套是USB转串口买一个FT232或者CH340的USB转TTL模块R70M的TTL输出接到模块上模块插进Orin NX的USB口。第二套是用40pin引脚里的UART需要自己飞线还要在设备树里配置引脚复用。我强烈建议第一套方案原因有三点USB转串口成熟稳定驱动在Ubuntu内核里基本是现成的插上就能看到/dev/ttyUSB0。万一个模块坏了直接换一个就行不需要改系统配置。JetPack系统对40pin的UART默认命名是/dev/ttyTHS1但不同版本的内核和BOOT配置可能导致引脚复用不对排查起来非常痛苦。当然USB转TTL也要注意线序。TTL串口标准是三根线TX发送、RX接收、GND地线。R70M的发送端要接到USB转TTL模块的接收端R70M的接收端要接到模块的发送端也就是所谓TX-RX交叉连接。GND必须共地很多人第一次接串口没共地导致数据时有时无或者完全没反应。我在配线上用三根杜邦线加一个CH340模块花了不到十分钟就搞定了后面所有测试都是在这套配置上完成的。3. 系统侧准备驱动、设备节点与权限问题3.1 JetPack系统下识别USB转串口芯片Orin NX刷好JetPack之后系统里默认包含大量USB串口芯片驱动FTDI、CH340、CP2102这些基本都覆盖到了。插上USB转TTL模块之后可以在终端执行dmesg查看内核日志看有没有识别到设备。我插上CH340模块后dmesg输出大致如下usb 1-2: new full-speed USB device number 4 using xhci-hcd usb 1-2: New USB device found: idVendor1a86, idProduct7523 usb 1-2: New USB device strings: Mfr0, Product2, SerialNumber0 usb 1-2: Product: USB-Serial CH340 ch341-uart ttyUSB0: ch341-uart converter now attached to ttyUSB0看到ttyUSB0就代表系统已经识别到串口芯片了。如果用的是FT232设备名同样是ttyUSB0如果是CP2102也是ttyUSB0。可以再用lsusb确认芯片厂商信息lsusb看输出里有没有CH340对应的idVendor 1a86。如果插上完全没有反应先换一根USB线试试这个坑我踩过有些USB线只供电不传数据折腾了半天才发现是线的问题。3.2 dialout权限组和常用串口工具安装识别到设备不等于能直接读数据。Linux下访问串口设备需要权限Ubuntu系统的用户默认不在dialout组里直接打开/dev/ttyUSB0会报Permission denied。解决方法是把当前用户加入dialout组sudo usermod -aG dialout $USER然后注销重新登录或者重启一次。如果不方便重启也可以直接sudo chmod 666 /dev/ttyUSB0临时给权限但这样每次插拔设备后权限会重置不是长久之计。接下来安装串口测试工具。我习惯先用minicom做纯文本调试最小安装方式sudo apt update sudo apt install minicom其他备选工具还有tio和screen个人觉得tio对新手更友好彩色输出比较简单。但minicom功能全排查问题时够用。另外建议装一个python3-pip后面用pyserial写解析脚本离不了它sudo apt install python3-pip pip3 install pyserial这些都准备好之后就可以开始读串口了。4. 串口读取实操从原始字节流到干净定位语句4.1 先用minicom确认波特率和数据流方向联适R70M-GNSS的串口默认波特率在不同版本设备上可能不同常见有9600、115200两种甚至有些支持460800用于输出原始观测量。我开始安装时没注意直接按115200去读结果屏幕上全是乱码和无处不在的替代字符。用minicom配置串口参数先确认设备节点然后进入配置界面sudo minicom -s在Serial port setup菜单里把Serial Device改成/dev/ttyUSB0把Bps/Par/Bits改成115200 8N18数据位、无校验、1停止位。保存退出后如果一切正常应该能看到类似这样的NMEA语句$GNGGA,023456.00,3101.2345678,N,12123.4567890,E,4,24,0.8,10.5,M,0.0,M,,*5A $GNRMC,023456.00,A,3101.2345678,N,12123.4567890,E,0.45,137.20,070423,,,D,V*16 $GNZDA,023456.00,07,04,2023,00,00*47如果看到的是$GPGGA、$GNRMC这种说明波特率对了。如果还带一些看不懂的字节首先怀疑波特率不对切换到9600试试。4.2 用pyserial写一个最小读取脚本确认串口数据和波特率没问题之后就可以用Python来做正式的读取和解析了。pyserial是串口编程的基础库读取NMEA数据不需要高深技巧核心就是打开串口设置波特率然后循环readline。这里给一个最小脚本import serial ser serial.Serial( port/dev/ttyUSB0, baudrate115200, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1.0 ) while True: line ser.readline() if line: text line.decode(utf-8, errorsignore).strip() if text.startswith($): print(text)这段代码里有个必须注意的点timeout参数。如果不设置timeoutreadline会一直阻塞等待数据当串口异常断流时脚本就卡死了。设成1.0表示每秒超时一次循环还能继续跑后面做异常检测和自动重连都方便。另外decode的时候要加errorsignore因为串口数据偶尔会被USB传输干扰出现个别非UTF-8字节直接decode会抛异常导致程序退出。做嵌入式串口开发代码的健壮性比优雅性重要得多这种小细节往往就是半夜被电话叫醒的根源。4.3 串口数据不稳定的第一轮排查如果你执行上面的脚本发现数据时断时续不要急着怀疑GNSS接收机坏了。先排查两个地方第一USB转TTL模块和R70M之间的杜邦线是否稳固很多时断时续都是接触不良引起的轻轻碰一下线就有反应。第二USB供电是否稳定有些USB转TTL模块工作电流较大插在扩展坞上电压不够会导致芯片复位表现也是数据中断。我把模块从扩展坞换到Orin NX原生USB口之后问题立刻消失了。如果数据完全没有任何输出但系统能识别到ttyUSB0那还可能是R70M的NMEA输出功能默认没有开启。这个在后面踩坑部分详细说。5. NMEA 0183解析实操拿到经纬度、高程和定位质量5.1 GGA语句的结构和各字段含义NMEA 0183是GNSS接收机最通用的输出协议每条语句以$开头以\r\n结尾字段之间用逗号分隔。对定位来说最重要的两条语句是GGA和RMC。GGA输出的是当前定位结果包含经纬度、高程、定位质量、卫星数等。以一条实际抓到的GGA为例$GNGGA,023456.00,3101.2345678,N,12123.4567890,E,4,24,0.8,10.5,M,0.0,M,,*5A字段拆解如下字段序号内容示例值说明1UTC时间023456.00时:分:秒.毫秒UTC时间不是北京时间2纬度3101.2345678度分格式31度01.2345678分3南北半球NN北纬S南纬4经度12123.4567890度分格式121度23.4567890分5东西半球EE东经W西经6定位质量40无定位1单点定位2差分定位4RTK固定解5RTK浮点解7卫星数24参与解算的卫星数量8水平精度因子HDOP0.8越小越好9海拔高度10.5单位米10高度单位M固定为M11大地水准面差距0.0单位米可不关心12差分时间空差分数据龄期差分断开会变大13差分站ID空基站标识看到定位质量字段是4的时候说明RTK固定解这是最好的状态水平精度可以达到厘米级。如果这个字段长时间停在1说明只拿到了单点定位RTK差分链路可能没建立起来。5.2 经纬度度分格式转换的经典坑GNSS输出的经纬度是度分格式DMS但是这个格式和日常使用的十进制度数不一样。比如纬度值3101.2345678它表示的是31度01.2345678分而不是31.012345678度。转换成十进制度数的公式是十进制度 度 分/60也就是31 01.2345678 / 60 31.02057613度。我第一次写解析脚本的时候想当然地直接把数字除以100然后拿去在Google Maps里定位结果差了大概几千公里报错报得莫名其妙。后来冷静下来仔细拆了字段才意识到问题。这部分是NMEA解析最常见的坑做这块开发一定要在纸上把公式推一遍再写代码。还有一个细节经纬度字段的整数部分位数不一样纬度范围是0到90所以是两位整数31度就是31经度范围是0到180所以是三位整数121度就是121。解析时必须保留整数部分不能统一用浮点数截断。5.3 一个完整的GGA/RMC解析类下面给一个我实际在用的解析类只截取GGA和RMC的关键字段对外提供经纬度、高度、航向、速度、定位状态这些常用信息import serial import math class GNSSParser: def __init__(self): self.lat 0.0 # 十进制度纬度 self.lon 0.0 # 十进制度经度 self.alt 0.0 # 海拔高度米 self.fix_quality 0 # 定位质量 self.satellites 0 # 卫星数 self.speed_kmh 0.0 # 速度公里/小时 self.track_deg 0.0 # 航向度 self.utc_time # UTC时间字符串 self.valid False # 是否有效定位 staticmethod def ddm_to_decimal(ddm_value): 将度分格式转换为十进制度格式 if not ddm_value: return 0.0 value float(ddm_value) degrees int(value / 100) minutes value - degrees * 100 return degrees minutes / 60.0 def parse_line(self, line): if line.startswith($GNGGA) or line.startswith($GPGGA): parts line.split(,) if len(parts) 10: return try: self.utc_time parts[1] lat_raw parts[2] lat_dir parts[3] lon_raw parts[4] lon_dir parts[5] self.fix_quality int(parts[6]) if parts[6] else 0 self.satellites int(parts[7]) if parts[7] else 0 self.alt float(parts[9]) if parts[9] else 0.0 self.lat self.ddm_to_decimal(lat_raw) self.lon self.ddm_to_decimal(lon_raw) if lat_dir S: self.lat -self.lat if lon_dir W: self.lon -self.lon except (ValueError, IndexError): return if line.startswith($GNRMC) or line.startswith($GPRMC): parts line.split(,) if len(parts) 8: return try: self.valid parts[2] A self.speed_kmh float(parts[7]) * 1.852 if parts[7] else 0.0 self.track_deg float(parts[8]) if parts[8] else 0.0 except (ValueError, IndexError): return def is_rtk_fixed(self): return self.fix_quality 4 def get_position(self): return self.lat, self.lon, self.alt if __name__ __main__: parser GNSSParser() ser serial.Serial(/dev/ttyUSB0, 115200, timeout1.0) while True: line ser.readline().decode(utf-8, errorsignore).strip() if line.startswith($): parser.parse_line(line) if parser.valid and parser.lat ! 0 and parser.lon ! 0: print(fLat: {parser.lat:.7f}, Lon: {parser.lon:.7f}, fAlt: {parser.alt:.2f}m, Quality: {parser.fix_quality}, fSats: {parser.satellites})解析类的核心是把度分转十进制封装成独立函数这样多个语句类型都能复用转换逻辑。此外用is_rtk_fixed方法专门判断是否RTK固定解这个在自动导航中非常关键RtK没有固定解的时候系统不应该进入自动作业模式否则位置误差大可能直接导致撞到障碍物或者执行错误的路径。6. 踩坑记录乱码、静默、断流三类典型故障的完整排查链路这一节我直接分享这段时间遇到最多的三类问题每类都给出从现象到底层原因的完整排查路径按步骤走能解决九成以上的串口GNSS接入问题。6.1 乱码从怀疑硬件到锁定波特率现象屏幕上全是奇怪的字符偶尔夹杂可读的字母和数字。第一次遇到乱码我第一反应是线接错了或者USB转TTL模块坏了。后来把模块换到同款新模块现象依旧才意识到不是硬件问题。用示波器量R70M的TX引脚发现波形是好的进一步确认是波特率不匹配。嵌入式串口通信里发送端和接收端必须使用相同的波特率差一个数字都不行。9600和115200这两个波特率差距很大信号完全解析不出来。解决方法是逐个试候选波特率先用minicom在9600下看数据如果还是乱码再试19200、38400、57600直到出现干净的$GN开头语句。最终我的设备是在115200下正常通信的后续所有脚本都按这个波特率写死。6.2 静默无输出天线遮挡、差分链路和NMEA开关三层排查现象串口配置看起来完全正确但readline始终没有返回任何数据。这种问题最磨人。我的排查顺序是第一步检查天线。GNSS接收机对信号非常敏感在室内或者靠近窗户的位置搜星数量会急剧下降甚至完全无法定位。把天线挪到室外空旷地带等待1到2分钟冷启动看接收机面板或上位机软件有没有定位信息。如果室外依然无输出那就是后面的问题。第二步检查差分链路。R70M如果工作模式是RTK需要同时有卫星信号和差分数据通过4G网络、电台或者Ntrip服务。如果差分链路断掉接收机可能长时间处于搜星状态NMEA语句可能照常输出但定位质量字段不会变成4。这个和完全静默有一定区别但某些固件版本在无定位状态下确实会减少甚至暂停NMEA输出。第三步检查NMEA语句是否被关闭。部分GNSS接收机可以通过配置指令关闭某几条NMEA语句比如厂商上位机软件里只勾选了二进制协议没勾选NMEA输出那么串口上就什么都没有。这时候要么通过厂商工具重新打开要么查询产品手册通过串口发配置指令。我手上这台R70M支持通过串口发特定指令来恢复默认配置这个功能建议提前确认好关键时刻能救命。6.3 断流USB供电、串口缓冲和读数方法三个角度现象数据能读到但每隔几十秒就中断几秒或者数据流出现大段丢失。第三个假设是USB转TTL模块所在的USB口供电不足。Orin NX的USB口供电能力有限如果又接了USB硬盘又接了多个传感器可能导致串口芯片工作不稳定。我的模块插在扩展坞上时断流频率明显更高插到原生USB口后基本消失。另一个容易被忽略的点是串口缓冲区溢出。GNSS接收机的输出频率可以设置常见的从1Hz到10Hz不等。如果输出频率很高而你的读取脚本处理速度跟不上内核缓冲区会被填满数据被丢弃。解决办法有两个方向一是调低接收机输出频率到5Hz以下对大多数机器人应用完全够用二是在读取脚本中加大读取缓冲或者用独立的读线程及时把内核缓冲区的数据消费掉。我实测下来1Hz到5Hz的频率在pyserial默认缓冲下没有问题超过10Hz就会开始丢数据。6.4 故障排查速查表现象优先排查项处理建议完全没有数据线序、GND共地、USB线是否只供电三根线重新对一遍换USB线乱码波特率、电平标准不匹配依次切换波特率检查是否需要RS232转TTL数据断流USB供电、串口缓冲溢出换原生USB口降低输出频率有GGA但无RTK固定解差分链路、天线遮挡、差分断开检查基站信号确认R70M差分模式权限报错dialout组或设备节点权限usermod -aG dialout或chmod 666解析到错误坐标度分转换、南北纬东西半球符号确认ddm_to_decimal实现符号逻辑在做完这些排查之后我的读取脚本稳定跑了两周定位数据基本没出现过断档这算是一个比较健康的状态了。7. 进阶把定位数据接入ROS2驱动自动导航和融合定位7.1 用pyserial读取并发布NavSatFix消息Orin NX上的工作不会止步于在终端打印经纬度很多时候你需要把数据喂给ROS2让导航栈能用。ROS2里表示GNSS定位的标准消息是sensor_msgs/NavSatFix里面包含经纬度、海拔高度和定位协方差。我的做法是写一个简单的ROS2 Python节点在rclpy.init之后创建一个发布者然后在循环中读取串口数据解析后放入NavSatFix消息。核心代码片段如下#!/usr/bin/env python3 import rclpy import serial from rclpy.node import Node from sensor_msgs.msg import NavSatFix class GNSSPublisher(Node): def __init__(self): super().__init__(gnss_publisher) self.publisher self.create_publisher(NavSatFix, /gnss/fix, 10) self.parser GNSSParser() self.ser serial.Serial(/dev/ttyUSB0, 115200, timeout1.0) self.timer self.create_timer(0.1, self.read_and_publish) def read_and_publish(self): for _ in range(5): line self.ser.readline().decode(utf-8, errorsignore).strip() if line.startswith($): self.parser.parse_line(line) if self.parser.valid and self.parser.lat ! 0: msg NavSatFix() msg.latitude self.parser.lat msg.longitude self.parser.lon msg.altitude self.parser.alt msg.status.service 1 self.publisher.publish(msg) self.get_logger().info( fPublish: {msg.latitude:.7f}, {msg.longitude:.7f} ) return def main(argsNone): rclpy.init(argsargs) node GNSSPublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这里用create_timer(0.1)来控制发布频率是10Hz但因为串口数据到达的速度取决于接收机的输出频率我设成每0.1秒读取一次一次最多读5行找到有效数据就发布一条。这样即使数据延迟也不会占满CPU。如果想用现成的方案ROS2里还有nmea_navsat_driver包可以直接把NMEA语句转换成ROS标准的NavSatFix但它的依赖配置过程比这个自写节点要重一些我图省事是直接自己写的。7.2 时间同步和差分状态对上层算法的影响接入ROS2之后还会有两个隐藏问题浮现出来。第一个是时间戳。串口读到的GNSS数据自带UTC时间但发布到ROS2里面时消息头部的时间戳却是系统当前时间。这两者之间可能有几百毫秒到几秒的差距如果后续要做传感器融合比如GNSS和IMU融合时间戳不一致会导致融合结果误差扩大。解决办法是把GNSS自带的UTC时间解析出来后换算成Unix时间戳赋给NavSatFix消息的header.stamp这样下游算法拿到的时间才和卫星时间对齐。第二个是RTK固定解状态。自动导航系统中定位质量必须达到RTK固定解quality4才能进入自动作业状态。如果系统只是在单点定位或者浮点解下就开始导航位置漂移可能达到几十厘米甚至几米。所以我在节点里加入了状态发布逻辑把当前的定位质量状态作为一条std_msgs/Int8发布出去导航主控在收到quality低于4时直接进入等待状态不让机器人乱跑。这个功能建议每个做GNSS导航的人都加进去非常关键。7.3 后续扩展GNSSIMU融合、差分基站和地图定位把串口GNSS数据接入ROS2只是第一步后续可以做的事还很多。比如用robot_localization包把GNSS和IMU做卡尔曼滤波融合获得更高频率、更平滑的位姿输出比如通过Ntrip接入网络RTK服务拿到固定解之后做厘米级精度的自动作业再比如把GNSS位置和激光雷达点云地图对齐用于室外环境下的三维地图维护。Jetson Orin NX 16GB这个平台的算力冗余比较大跑完GNSS解析、ros2核心之后还能同时跑视觉SLAM或者轻量化AI推理。我在同一个板子上还部署了视觉识别任务整体CPU占用大概在30%左右16GB内存也足够同时跑多个容器所以这条路对于资源需求量大的室外机器人方案来说是一个比较舒服的硬件组合。最后分享一点个人体会串口GNSS接入这件事硬件上需要注意的无非是电平匹配、线序交叉、共地、供电稳定这四件事软件上需要注意的是权限、波特率、缓冲区、时间戳。把这四项都理顺剩下的都是水到渠成的事。遇到乱码别怀疑模块坏了先换波特率遇到静默别急着找厂商先看天线位置和差分链路状态遇到断流别盲目降低频率先排查供电和缓冲。这套排查思路不仅适用于联适R70M换其他品牌的GNSS接收机同样有效希望能帮各位少走一些弯路。
返回列表