尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Jetson Orin NX串口读取GNSS模块:NMEA解析与RTK定位实战

发布时间:2026/9/29 18:10:28

资讯中心
01
ARTICLE

Jetson Orin NX串口读取GNSS模块:NMEA解析与RTK定位实战

Jetson Orin NX串口读取GNSS模块:NMEA解析与RTK定位实战
做室外机器人定位这件事绕不开GNSS。我最近在Jetson Orin NX 16GB上接了一个联适R70M-GNSS模块目标很明确用串口把高精度定位数据读出来并解析成程序能直接用的经纬度和状态信息。这套组合不是什么高深技术但正因为基础踩坑的人反而特别多——电平接反、串口被占用、RTK一直不固定、数据丢一半……这篇文章把你可能遇到的问题都梳理一遍并给出可直接复制的代码和命令。适合正在做机器人、自动驾驶、测绘设备集成的朋友尤其是刚把Jetson和GNSS模块凑到一起、却不知道从哪下手的阶段。1. 项目整体设计与思路拆解1.1 为什么是 Jetson Orin NX 与 R70M-GNSS 的组合Jetson Orin NX 16GB 是边缘计算里很常见的一块板卡16GB的统一内存意味着可以同时跑检测、分割这类视觉模型和定位导航逻辑。而联适R70M-GNSS不是普通的GPS模块它支持RTK/PPP这类高精度解算既能输出GPS/北斗等系统的卫星定位数据也能输出原始观测数据和差分数据。把两者接在一起本质上是给Jetson加了一个“厘米级”的室外定位源。对做无人配送车、巡检机器人、农业机械导航的朋友来说这个搭配属于非常标准的“计算平台 高精度定位模块”组合。串口在这个项目里承担了唯一的桥梁角色。为什么不用USB或网口因为R70M这种专业GNSS接收机通常都会提供UART接口在嵌入式系统里UART最少只需要三根线而且只要波特率和数据格式设置正确就能稳定接收数据。对Jetson来说Linux把串口设备抽象成了 /dev/ttyUSB0 这类文件open/read就行不用处理复杂的协议栈。这个选择最省事也最容易排查问题先看线再看权限最后看软件解析每一环都很直观。1.2 串口是GNSS数据流的“最后一公里”GNSS数据从卫星到应用要经历好几跳天线先接收射频信号接收机内部完成捕获、跟踪、解调、解算最后把结果按照约定协议推出来。协议层面最常见的就是NMEA 0183它是一行行的ASCII文本以“$”开头语句之间用回车换行隔开。R70M在默认配置下就是通过串口输出这些语句所以只要把串口数据流接到Jetson就已经走通了数据链路的最后一公里。这一公里虽然短却最值得花时间打磨。因为后面的建图、导航、时间同步全都依赖这一行数据是否完整、及时。如果程序读串口时处理不过来或者系统把GPS信息当成普通串口日志混在一起那么后面所有轨迹都会飘。我的习惯是先把串口数据流当成一个独立的“数据源”专门用一个线程或进程去读解析结果通过队列交给上层而不是在主流程里顺手读两行。这也是后面第3章、第6章代码模板的核心思想。补充一个选型心得为什么选“16GB”而不是“8GB”对我来说16GB版本在跑视觉模型时基本不需要担心内存溢出剩下的显存还能给RTK解算辅助或SLAM留余量。如果你只做定位数据采集8GB也够但做自动驾驶或完整机器人系统16GB明显更从容。2. 硬件接线与系统环境准备2.1 硬件清单与接线要点在开始敲命令之前先确认手里的硬件。我这边清单如下Jetson Orin NX 16GB开发套件载板是第三方定制40Pin引脚和供电方式略有差异联适R70M-GNSS接收模块我这里用的是TTL电平UART版本配套有源天线必须用GPS/北斗双频天线插上去拧紧USB转TTL适配器CH340或CP2102都行用来看串口数据杜邦线RTK差分源可选通过内置4G网络模块或外接电台接线顺序我是这样做的先把GNSS模块断电然后接三根线模块TXD → Jetson载板或者USB转TTL的RXD模块RXD → Jetson的TXD最后GND共地。这个顺序看起来简单但最容易错的恰恰是TX/RX交叉。如果你发现数据完全收不到第一步就该怀疑是不是两条数据线接反了。还有一个容易忽略的问题是电平Jetson 40Pin上的UART一般是1.8V或3.3V逻辑而R70M如果默认是TTL 3.3V可以直接对接如果是RS232电平就不能直接接必须加一个RS232转TTL模块。我一开始差点踩了这个坑因为联适有的板卡型号会选配RS232口。大家接线前务必看一眼模块铭牌或手册确认是“UART TTL”还是“RS232”。USB转TTL适配器则基本都带电平转换相对安全。天线也要提前规划。GNSS天线必须放在开阔位置最好车顶或窗边不要被金属遮挡。R70M模块的供电一般是3.3V-5V我直接从载板取5V注意电流别超过手册限值。最后插上USB转TTL到Jetson USB口。你可能会有疑问Jetson不是有板载UART吗为什么还要额外买USB转TTL因为Jetson的40Pin串口默认可能被控制台占用需要改设备树或通过config去启用新手容易卡住。用USB转TTL适配器则即插即用系统自带CH340/CP2102驱动所以我推荐先用USB转TTL跑通流程之后再考虑板载UART以节省USB口。2.2 Jetson串口驱动与设备识别Jetson默认系统是Ubuntu大多数USB转串口芯片的驱动已经编译进内核。插上USB转TTL后先执行 lsusb 和 dmesg | tail看内核是否识别到了设备。如果芯片是CH340lsusb里会出现 QinHeng Electronics HL-340 serial converter 这类字样CP2102则显示 Silicon Labs CP210x。如果没识别最直接的办法是换一根USB线或者重插一次有时候供电不足也会导致设备反复掉线。识别成功后设备节点通常叫 /dev/ttyUSB0。如果同时挂了多个USB串口会按顺序编号 ttyUSB0、ttyUSB1。为了不认错设备建议用 udev 固定一个符号链接。我在 /etc/udev/rules.d/99-gnss.rules 里写了这样一行SUBSYSTEMtty, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, MODE0666, SYMLINKgnss这里的 idVendor 和 idProduct 要根据 lsusb 查到的实际值改。CH340一般是 1a86:7523CP2102是 10c4:ea60。写好后执行 sudo udevadm control --reload-rules重新拔插USB之后 /dev/gnss 就会指向同一个设备号权限也变成了0666。如果你非要用板载UART那需要确认Jetson的40Pin串口是否被系统占用。默认情况下有的镜像会把某个UART配置成调试串口直接用它会导致乱码或冲突。我建议第一版先用USB转串口跑通板载UART作为优化项再处理。另外无论用哪种当前用户都要加入 dialout 组否则读写 /dev/ttyUSB0 可能报 Permission deniedsudo usermod -aG dialout $USER改完组之后要重新登录一次才生效。这一步虽然简单但很多新手卡在“明明有设备却打不开”上其实就是没加组。3. 串口读取定位数据的四种实操方式3.1 先用 minicom 粗看数据拿到定位数据之前不要直接写代码。先用现成工具验证链路通不通省得后面调试时分不清是硬件还是软件问题。minicom 是Linux下很常用的串口终端安装就一行命令sudo apt install minicom然后启动minicom -D /dev/ttyUSB0 -b 115200打开后如果一切正常屏幕上会不断滚动类似这些文本$GPGGA,082756.00,3117.30845,N,12128.41482,E,1,08,1.0,7.8,M,-3.2,M,,*5B $GPRMC,082756.00,A,3117.30845,N,12128.41482,E,0.5,123.4,230225,3.2,W*6F看到 $GPGGA、$GPRMC 这行就说明串口通信已经通了。没看到就检查接线和波特率。minicom退出快捷键是 CtrlA 然后 Q它会直接问你是否退出选 Yes 就行。这里我要说一个习惯看到NMEA语句后先不要急着关掉观察几秒钟看行与行之间是否连续、有没有半行乱码。如果连续滚动但偶发乱码往往是波特率或电平问题如果完全静止则是TX/RX接反或者模块没在输出。这一步虽然简单却能帮你把问题范围缩小一大半。我在调试时还会顺便看下卫星数如果一直是0那就说明天线没搜到星先别折腾串口了。3.2 用 Python pyserial 按行读取确认链路正常后就可以写读取脚本了。Python 的 pyserial 是处理串口的标准库安装pip install pyserial一个最小读取脚本如下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(ascii, errorsignore).strip() if text.startswith($): print(text)这段代码的作用是打开串口、按行读取、解码成ASCII字符串然后打印。timeout1.0 让 readline 最多等一秒避免程序卡死。实际使用中还要处理异常——串口拔掉、进程被杀、系统休眠回来之后串口对象会失效。我习惯把打开和读写封装成类重连时重新调用 serial.Serial这点在第6章会给出更完整的模板。串口读数据看似简单但有一个细节必须注意串口读操作默认是阻塞的如果上层处理速度跟不上GNSS数据帧速率数据就会积压在系统缓冲区里读出来时可能已经丢帧。GNSS输出频率通常不高5Hz或10Hz每条NMEA语句也就几十个字节所以Python按行读一般不会丢。但如果你同时还在做图像推理、把读串口放到主线程里就有风险。最好单独开一个线程专门读。3.3 解析经纬度并保存到文件打印NMEA仅是第一步真正有用的是把经纬度、高度、定位状态解析成结构化数据。最简单的方式是用 pynmea2 库pip install pynmea2然后写个脚本只提取 GGA 消息中的关键字段import serial import pynmea2 ser serial.Serial(/dev/ttyUSB0, 115200, timeout1.0) while True: line ser.readline() try: text line.decode(ascii, errorsignore).strip() if text.startswith($GPGGA): msg pynmea2.parse(text) print(f时间 {msg.timestamp} f纬度 {msg.latitude} {msg.lat_dir} f经度 {msg.longitude} {msg.lon_dir} f质量 {msg.gps_qual} f卫星数 {msg.num_sats} f海拔 {msg.altitude} m) except Exception as e: print(解析失败, e)pynmea2 自动把 “3117.30845,N” 这样的度分格式转成十进制度数非常省事。想保存历史轨迹可以直接把解析结果追加到CSVimport csv from datetime import datetime with open(gnss_track.csv, a, newline) as f: writer csv.writer(f) writer.writerow([datetime.now().isoformat(), msg.latitude, msg.longitude, msg.altitude])不过要注意日志里最好同时记录原始NMEA语句。因为CSV一旦字段定义错了后面换一种消息类型再补就麻烦了。我实际做采集时都是原始报文和解析结果各存一份原始报文用于校验解析结果用于实时展示。这样一旦最终轨迹出现问题还能回到原始数据重新查。3.4 用 socat 做数据分发解决多进程抢串口很多场景下你不可能只用一个进程读串口。比如我想一边在终端看NMEA报文、一边让ROS2节点解析定位两边同时读同一个 /dev/ttyUSB0 会冲突因为Linux串口默认只允许一个进程独占。这时可以借 socat 把物理串口“映射”到TCP端口让多个进程分别作为客户端连接。socat 是一个数据转发工具安装 apt install socat然后socat -d -d /dev/ttyUSB0,raw,echo0,b115200 TCP-LISTEN:54321,reuseaddr这条命令会把 /dev/ttyUSB0 收到的数据转发到 TCP 54321 端口其他程序可以分别用 nc 连接看数据也可以在自己的代码里用 socket 读取。也可以反过来把TCP端口收到的数据写进串口这就成了双向桥接。socat 方案虽然好用但初次配置容易把链路搞混。TCP端口同时允许多个连接所以比伪终端的“多进程读”更直观。我建议先用第3.2节单进程脚本验证核心功能再考虑分发。如果只是临时想开两个终端看同一份数据socat 是最省事的。4. 数据解析与 RTK 定位细节4.1 NMEA 0183 常用语句拆解NMEA 0183 是GNSS接收机最通用的输出协议每一行都是一个句子以 $ 开头以回车换行结束。我们最常用的两句话是 $GPGGA 和 $GPRMC。专门做高精度定位时还会看 $GPGSV、$GNGGA 等。R70M支持多系统联合定位所以很多报文以 “GN” 开头比如 $GNGGA含义和 GPGGA 基本一致只是兼容了GPS北斗GLONASS。GGA语句字段举例$GNGGA,082756.00,3117.30845,N,12128.41482,E,1,08,1.0,7.8,M,-3.2,M,,*5B字段含义示例082756.00UTC时间时分秒08:27:56.003117.30845纬度度分格式31度17.30845分N北纬/南纬N12128.41482经度度分格式121度28.41482分E东经/西经E1定位质量0无效1单点2差分4RTK固定5RTK浮点108参与解算的卫星数81.0HDOP水平精度因子1.07.8,M海拔高度和单位7.8米-3.2,M大地水准面差距和单位-3.2米记住一个关键点度分格式不是十进制度。3117.30845 表示 31度17.30845分转十进制度就是 31 17.30845/60 31.28847417。pynmea2 已经做了转换但如果你自己手写解析器千万别忘了除以60。这是新手最容易算错的地方也是第6章我建议单独写一个转换函数做单元测试的原因。4.2 RTK初始化与固定状态判断R70M的价值是能达到RTK固定解的厘米级精度。普通单点定位精度在1.5到2米左右RTK固定后可以到2厘米左右差异非常大。但RTK需要差分源要么通过内置4G模块连接CORS或网络RTK服务要么外接电台接收基准站差分数据。在串口上我们需要判断当前是不是真的固定解。判断方法有两个第一看GGA语句中的定位质量字段。质量4表示RTK固定解5表示浮点解2表示差分但未固定。第二看卫星数和HDOP。一般RTK固定时卫星数不少、HDOP很小如果HDOP大于3说明天空可见条件很差即使有差分也很难固定。我实际操作时先看一眼屏幕上的 $GNGGA如果质量字段是4就放心用如果是5或2得检查天线遮挡和差分网络。R70M的RTK模式配置一般通过专用配置工具或串口指令具体指令要按官方手册来。我这边用的是默认配置上电后自动通过4G网络接入CORS服务所以只要天线位置开阔过一分钟左右就能看到质量字段跳到4。如果你的模块没有内置网络模块就需要自己接电台或者通过串口把RTCM数据灌给R70M这样接收机会把RTCM当作差分输入。无论哪种方式串口上看到的最终效果都是质量字段的变化所以先把串口数据读好永远是第一步。4.3 波特率、校验位等参数怎么定串口通信参数无非就是波特率、数据位、校验位、停止位。GNSS模块最常见的默认配置是“115200 8N1”波特率1152008个数据位无校验1个停止位。这符合串口调试助手里最常见的“115200 8N1”选项也适配绝大多数USB转TTL适配器。为什么NMEA输出不需要很高的波特率因为一条典型GGA语句大约70个字符即使10Hz输出1秒钟也就700字节115200bps远够用。但如果要输出RTCM差分数据、原始观测数据或者双天线航向数据数据量会暴涨这时波特率可能要设到460800甚至921600。我实际上在采集原始观测量时就把R70M的串口波特率调到了460800否则会明显丢数据。另一个值得提醒的点是奇偶校验。有些人习惯把串口调试助手的“无校验”改成“偶校验”但GNSS模块默认通常是None两边必须完全一致。如果解析结果是乱码或直接没有数据先对照两边的波特率、数据位、停止位、校验位这四个参数一字不差才能通。串口调试时不要乱猜先用stty或minicom把参数固化下来再动代码。5. 常见问题、坑与排查实录5.1 设备节点时有时无权限拒绝这类问题多出现在USB转串口上。现象是插上设备后 ls /dev/ttyUSB0 存在但过一会又消失或者程序打开时报 Permission denied。排查步骤我总结成三个第一步看 dmesg | tail确认USB设备有没有发生“disconnect”或“cannot enumerate”之类的报告。如果出现大概率是供电不稳或USB线质量差。Jetson的USB口供电能力有限外接有源天线不要用USB口直接供电要给天线独立供电。第二步看lsusb里芯片还在不在如果在就说明内核驱动OK问题只是设备节点如果不在排除线材和接口。第三步权限问题确认当前用户是否在 dialout 组或者直接用代码里面的串口路径权限。如果只是临时调试也可以 sudo chmod 666 /dev/ttyUSB0但重启后会失效所以最好用udev规则固化。我遇到过一次比较隐蔽的问题Jetson Orin NX的载板上有两个USB口靠近电源的那个口给GNSS模块供电时电流不够导致设备每隔几分钟就掉线一次。换成扩展坞上的另一个USB口供电更强的之后连续跑了一周没掉线。所以“设备节点时有时无”时优先怀疑供电而不是驱动版本。5.2 读到乱码或只有一半数据乱码和半行数据90%是三项原因波特率不对、TX/RX接反、电平不匹配。波特率不对时你会看到类似 “GPGGA…” 或不连续的字符TX/RX接反则完全没有输出电平不匹配可能导致要么没数据要么偶发错位。把这三个逐一排除后通常就好了。另外一个容易被忽略的是“多个程序同时占用了串口”。串口设备同一时刻只能由一个进程打开除非用了socat/TCP转发。如果之前开着 minicom再运行Python脚本就会打开失败或读取到半个缓存。我个人的经验每次调试前先 sudo lsof /dev/ttyUSB0 看看是谁占着串口然后在minicom里正常退出不要直接关终端。否则下一次打开时内核缓冲区里残留的半行数据也会被读出来。Linux下串口数据丢失还有一个常见原因程序读取不够快。如果主线程同时做图像处理串口线程被调度延迟内核tty缓冲区一旦堆满就会直接丢掉新数据。解决办法是提高读线程优先级或者把串口数据转发到一个内存队列中让算法线程去消费队列而不是直接读串口。至于“串口DMA”这个概念在嵌入式MCU上很常用但Linux主机侧我们依赖内核驱动和 tty 层缓冲不需要自己管理DMA这也是为什么Jetson上和单片机写串口驱动的思路完全不一样。5.3 天线架设与定位精度的影响天线是所有GNSS系统里最容易压制性能的环节。很多朋友买了高精度模块第一次上电放在室内窗口然后抱怨“为什么一直单点解”。GNSS信号来自卫星必须直接接收到来自天空的电磁波任何遮挡、反射都会造成多径效应让定位精度急剧下降。R70M配套的有源天线更加明显它需要供电一般通过同轴馈线内的偏置电压供电如果馈线接触不良天线放大器不工作卫星数会直接跌到0。我通常用GGA语句中的卫星数和HDOP作为信号质量指标。如果室内能搜到6颗星已经算不错但RTK固定几乎不可能。到了室外开阔地搜星数量一般在20颗以上HDOP小于1这时候RTK固定才稳定。天线尽量远离金属栏杆、屋顶边缘、大树安装高度越高越好。如果设备装在移动机器人上天线固定在车顶中央并确保周围不被相机支架或传感器遮挡。实际测试时我把天线从桌面移到车顶固定解的时间从十几分钟缩短到几十秒差距就是这么明显。5.4 如何接入ROS2 humble / 与IMU融合建图定位很多机器人项目最终要把定位数据接进ROS2。最常见的方式是使用 nmea_navsat_driver 包它本质上是订阅串口或NMEA话题然后发布 NavSatFix 消息。在ROS2 humble下可以这样启动ros2 run nmea_navsat_driver nmea_serial_driver --ros-args \ -p port:/dev/ttyUSB0 \ -p baud:115200 \ -p frame_id:gnss_link发布出来的 /fix 话题就是标准 GPS 消息可以给 robot_localization 做传感器融合。如果嫌这个包老也可以自己写一个ROS2节点在Python里调用pyserial然后把解析结果包装成 sensor_msgs/msg/NavSatFix 发出去。串口部分和单机脚本完全一样只是加了一个 rclpy 初始化。和IMU融合是很自然的下一步。GNSS输出频率一般只有10HzIMU却能到几百Hz两者卡尔曼融合后才能支撑高速运动的车辆姿态和位置估算。Jetson上跑 robot_localization 的 ekf_node输入两个话题/fixGNSS和 /imu/dataIMU输出 /odometry/filtered。在做室外建图时这个融合结果可以和激光雷达里程计互相校正避免长时间漂移。所以现在费心把串口数据接干净后面融合才有好原料。如果你之前用esp32或者单片机做过串口桥接会明显感觉到Jetson这边更像一台Linux服务器串口只是众多传感器接口中的一个前面加一层网络转发也不是什么难事。6. 我的实操心得与后续扩展6.1 一个低成本稳定的串口读取脚本模板最后分享一个可以直接拿去用的Python模块。它比前面demo多了重连和线程能应对USB临时掉线、Jetson休眠等情况。代码结构很简单import serial import threading import time import pynmea2 class GNSSReader: def __init__(self, port/dev/ttyUSB0, baud115200): self.port port self.baud baud self.ser None self.running False self.lock threading.Lock() self.last_fix None def _open(self): self.ser serial.Serial(self.port, self.baud, timeout1.0) def read_loop(self): while self.running: if self.ser is None: try: self._open() except serial.SerialException: time.sleep(1) continue try: line self.ser.readline() if not line: continue text line.decode(ascii, errorsignore).strip() if text.startswith($GNGGA): msg pynmea2.parse(text) with self.lock: self.last_fix msg except serial.SerialException: print(串口断开正在重连...) self.ser.close() self.ser None except Exception as e: print(解析失败:, e) def start(self): self.running True threading.Thread(targetself.read_loop, daemonTrue).start() def stop(self): self.running False if self.ser: self.ser.close() if __name__ __main__: gnss GNSSReader() gnss.start() while True: time.sleep(1) with gnss.lock: if gnss.last_fix: print(gnss.last_fix.latitude, gnss.last_fix.longitude, gnss.last_fix.altitude, gnss.last_fix.gps_qual)这个脚本我在Jetson上实际跑过配合R70M 115200波特率输出连续一整天没有丢包。核心思想是“串口随时可能坏”所有读操作都在后台线程里上层任何时候去读 last_fix 都能拿到最新的有效定位不会因为瞬时串口错误影响整个程序。如果你要把它接进ROS2只需要在这个线程里额外发布NavSatFix消息就行。6.2 后续可以怎么扩展读完定位数据只是第一步后续可以扩展的方向很多。如果你的机器人需要在室外自主导航可以把解析出来的经纬度换算到UTM坐标系或本地笛卡尔坐标再和IMU输出一起喂给扩展卡尔曼滤波器。如果做测绘可以定时保存原始NMEA和RTCM数据用于事后差分。如果想获得更平滑的轨迹可以把双天线航向、轮式里程计都加进来。我个人最推荐的一个方向是先花一天时间把串口数据的记录回放框架搭好。原始报文落盘、解析字段落盘、再写一个回放脚本这样后续调导航算法时不用每次都把设备搬出去直接回放数据就能复现环境。很多团队把精力全花在算法模型上反而忽略了定位数据样本的采集与回放结果现场出了问题没法离线复现。把GNSS串口数据当作一种“信号”去认真记录才会发现里面藏着很多调试线索。最后分享一个小技巧不要直接拿十进制度和角度去调坐标转换先在脚本里打印原始语句把度分转换为十进制的逻辑单独写成一个纯函数并准备几组“已知经纬度”做单元测试。因为GNSS数据链路上协议解析阶段的错误是最难查的一旦经纬度算错后面所有定位、建图、控制都会跟着错。串口拿到数据只是开始把数据变成可靠、可追溯、可回放的定位结果才是这套硬件组合真正发挥价值的地方。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。