RTK模块接入ROS Noetic:从串口到NavSatFix高精度话题
发布时间:2026/10/19 13:45:08 锦皓数字建站

1. 为什么需要把RTK模块接入ROS Noetic做移动机器人、无人车或者高精度测绘的朋友应该都有过这样的经历手里拿着一块RTK模块串口一插终端里哗哗往外蹦$GPGGA,、$GPRMC这种字符串一眼看去全是字符但真正要用的时候却不知道该拿它怎么办——尤其是在ROS Noetic环境下。RTK模块的定位结果是厘米级的但ROS里的机器人导航、建图、路径规划消费的是navsatfix、sensor_msgs/NavSatFix这样的结构化话题而不是一串裸文本。所以你要解决的问题其实很明确把串口上不断输出的NMEA协议数据解析成ROS能识别、能使用的话题消息并保证数据可用、时间戳合理、固定解状态可判断。这篇文章就是围绕这件事展开的。我假设你已经装好了Ubuntu和ROS Noetic手里有一块支持NMEA输出的RTK模块不管它是内置板卡还是外接接收机底层逻辑都一样。我会从串口怎么认、NMEA每句话讲了什么讲到自写发布节点的完整代码再到现成驱动怎么选、怎么避坑。适合正在做移动机器人高精度定位、准备把RTK接入建图导航系统或者单纯想把模块数据读进ROS的开发者参考。先说一句总结性的话这件事本身不算难但坑特别多主要集中在串口权限、波特率、NMEA语句解析、固定解状态判断和时间戳处理这几个点上。下面我把每个环节拆开讲清楚。2. RTK模块和NMEA协议的基础认知2.1 RTK定位原理对数据解析的影响RTKReal-Time Kinematic定位的原理简单说就是用基准站的改正数去消除卫星信号传播过程中的电离层、对流层误差让流动站拿到厘米级定位结果。但你要注意RTK模块输出的NMEA语句里不是每一句都包含高精度坐标。比如最常用的GGA语句它有quality indicator这个字段第6个字段用来标记当前定位状态。数值是4或5时表示RTK固定解或浮点解这时候经纬度才是可用的厘米/分米级结果数值是1时只是单点定位只能用单GPS精度如果是0说明当前根本没有定位。所以解析数据时不能拿到经纬度就发出去必须先判断定位质量。我做过一个项目把RTK模块的GGA数据直接当成高精度坐标灌给建图算法结果发现地图一米多误差排查了一天最后才发现是模块在楼宇间经常掉回单点解但代码里根本没做质量判断把单点解也当成好数据用。这个教训后面会详细展开。2.2 NMEA语句的常见类型NMEA 0183协议是串口通信中的明文协议目前绝大多数RTK模块都支持输出以下几条常用语句语句全称包含的核心信息GGAGlobal Positioning System Fix Data定位时间、经纬度、定位质量、卫星数、海拔高度RMCRecommended Minimum Specific GNSS Data推荐最小定位信息含日期、时间、经纬度、速度GSAGNSS DOP and Active Satellites参与定位的卫星编号、PDOP/HDOP/VDOPGSVSatellites in View可见卫星详细信息GSTGNSS Pseudorange Errors伪距误差标准差RTK精度评估常用ZDATime and DateUTC时间和日期常用于时间同步我实际用得最多的就是GGA和RMC。GGA负责拿经纬度和海拔高度RMC负责拿地面速度和对地航向角。如果你要做时间同步ZDA也很重要因为它直接给UTC日期时间可以和系统时间做比对。2.3 串口参数怎么定RTK模块的串口输出参数一般通过上位机软件或AT指令配置常见默认波特率有9600、115200、230400、460800等。不同的模块默认值不一样所以不要想当然地按模块说明书上的值去配而是先用串口助手或者命令行扫一遍。NMEA 0183的串口约定通常是8位数据位、无校验、1位停止位即8N1。这个几乎没有例外。但波特率是最大的坑。有的模块默认115200有的模块默认9600你配错波特率时串口上会看到一堆乱码或者完全没数据而不是报错——这个特征很像硬件没接好所以特别容易误导人。3. 准备工作Ubuntu下串口识别与权限处理3.1 查看RTK模块对应的串口设备把RTK模块通过USB转串口线或者直接是USB接口模块接到Ubuntu主机上以后先确认系统识别到了哪个设备节点。ls /dev/ttyUSB* /dev/ttyACM* 2/dev/null dmesg | tail -30如果模块走的是USB转TTL芯片通常会出现/dev/ttyUSB0如果模块本身就带USB接口则可能是/dev/ttyACM0。dmesg可以看到类似USB Serial Converter detected这样的信息同时会显示设备对应的USB口描述比如cp210x或者ch341-uart这些可以帮你确认是哪块硬件。如果你同时接了多个串口设备可能会看到ttyUSB0、ttyUSB1两个设备。这时候不要靠猜可以用udevadm info查询设备信息udevadm info -a -n /dev/ttyUSB0 | grep -E serial|manufacturer通过序列号或者制造商信息就能区分哪个节点对应你的RTK模块。我习惯把这一步做扎实因为后面如果接多个传感器激光雷达、惯导、电台设备节点错乱是家常便饭。3.2 串口权限最常见的第一个坑Ubuntu下读串口默认需要dialout组权限。如果当前用户不在这个组里open()串口时会报Permission deniedROS节点直接启动失败但有时候你以为代码写错了其实只是权限问题。一次性解法是把当前用户加入dialout组sudo usermod -a -G dialout $USER改完以后必须重新登录或者重启组权限才会生效。如果你不想重启也可以临时用sudo跑节点但我不推荐ROS节点经常要配合launch文件启动用sudo会带来一堆环境变量问题比如找不到ROS_MASTER_URI或者rosrun都起不来。更推荐的方案是用一条udev规则把串口设备固定命名并放开权限。比如在/etc/udev/rules.d/99-rtk-usb.rules里写KERNELttyUSB*, ATTRS{idVendor}10c4, ATTRS{idProduct}ea60, MODE0666, SYMLINKttyRTK这里的idVendor和idProduct用lsusb查你的模块实际值替换。写完执行sudo udevadm control --reload sudo udevadm trigger以后插上模块就会固定出现/dev/ttyRTK这个稳定路径权限也直接放开。这个做法在机器人上特别实用因为每次插拔串口设备后节点启动顺序不同设备号会漂移固定名能彻底解决这个问题。4. 先不急着写代码命令行验证NMEA数据4.1 用screen或cat直接看串口写ROS节点之前我强烈建议先在终端里确认串口能出数据数据格式符合预期。这一步能帮你筛掉一半的问题。sudo screen /dev/ttyUSB0 115200或者如果你已经给了权限直接用catcat /dev/ttyUSB0屏幕上有数据翻滚说明链路通了。如果看到的是乱码大概率是波特率不对如果完全没反应需要检查RTK模块供电、天线状态以及串口线是否接对。有的模块TX/RX线序和USB转串口板不一致也可能导致完全没数据。看到正常NMEA输出后CtrlA退出screen。如果用的是cat按CtrlC即可。4.2 命令行阶段就该注意的信息我习惯在命令行阶段就观察几个关键点语句类型是否包含GGA、RMC。只输出GGA也可以用但不包含RMC时想要地面速度就得另想办法。GGA第6字段的数值。稳定输出4或5说明RTK固定解没问题如果长期是1说明RTK并未固定可能是基准站信号没接好或者差分数据没进来。更新率。RTK模块通常支持1Hz、5Hz、10Hz、20Hz配置命令行下可以看到数据翻页速度。后期在ROS里话题频率也要和这个保持一致。这里我提一个很容易忽略的点NMEA数据使用UTC时间不携带时区信息。GGA里的hhmmss.sss是UTC时间和你系统的本地时间默认差8小时东八区。如果你把NMEA里的时间拿来打时间戳后面做数据同步时会发现ROS里的时间戳和现实时间差一大截。这个我放在第7节详细说。5. 自写一个ROS Noetic话题发布节点5.1 节点设计思路命令行确认模块能出数据后就可以考虑把它接进ROS了。如果你用的是现成的RTK模块配套驱动或者网络上有现成的NMEA驱动那可以直接跳到第6节。但我还是建议自写一个——因为自写节点的过程能让你彻底搞懂NMEA里每个字段的语义后面遇到奇怪的定位数据时你才知道问题出在哪一环。节点设计其实很简单一个线程循环读串口按行分割NMEA语句解析出经纬度、海拔、定位质量、卫星数、速度、航向等信息然后以std_msgs/String或者raw字节的原始数据话题发出来再另起解析模块转成sensor_msgs/NavSatFix。我个人的经验是先把原始NMEA字符串作为一个话题原样发出来好处有两个一是方便调试任何时候都能用rostopic echo看模块到底输出了什么二是后续无论改解析逻辑还是换协议版本原始数据都在不需要重新从串口拉线。5.2 代码实现串口读取与发布下面是我常用的一个简化实现基于pyserial和rospy。它先把串口数据按行读出来发布到/nmea_sentence话题同时调用解析函数发布NavSatFix。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import rospy import serial import serial.tools.list_ports from sensor_msgs.msg import NavSatFix, NavSatStatus from std_msgs.msg import String def parse_gga(parts): 解析 $GPGGA 语句返回 (lat, lon, alt, quality, num_satellites) try: lat_raw float(parts[2]) lat_deg int(lat_raw / 100) lat_min lat_raw - lat_deg * 100 lat lat_deg lat_min / 60.0 if parts[3] S: lat -lat lon_raw float(parts[4]) lon_deg int(lon_raw / 100) lon_min lon_raw - lon_deg * 100 lon lon_deg lon_min / 60.0 if parts[5] W: lon -lon quality int(parts[6]) num_satellites int(parts[7]) alt float(parts[9]) if parts[9] else 0.0 return lat, lon, alt, quality, num_satellites except (ValueError, IndexError): return None def handle_line(line, pub_fix, pub_raw): line line.strip() if not line.startswith($): return pub_raw.publish(String(dataline)) if line.startswith($GPGGA) or line.startswith($GNGGA): fields line.split(,) result parse_gga(fields) if result is None: return lat, lon, alt, quality, num_satellites result if quality 0: return fix NavSatFix() fix.header.stamp rospy.Time.now() fix.header.frame_id gps_link fix.status.status NavSatStatus.STATUS_FIX fix.status.service NavSatStatus.SERVICE_GPS | NavSatStatus.SERVICE_RTK fix.latitude lat fix.longitude lon fix.altitude alt fix.position_covariance_type NavSatFix.COVARIANCE_TYPE_UNKNOWN pub_fix.publish(fix) def main(): rospy.init_node(rtk_nmea_driver, anonymousFalse) port rospy.get_param(~port, /dev/ttyUSB0) baudrate rospy.get_param(~baudrate, 115200) pub_fix rospy.Publisher(fix, NavSatFix, queue_size1) pub_raw rospy.Publisher(nmea_sentence, String, queue_size100) try: ser serial.Serial(port, baudrate, timeout0.1) except serial.SerialException as e: rospy.logerr(Failed to open serial port %s: %s, port, e) return rate rospy.Rate(50) line_buffer b while not rospy.is_shutdown(): data ser.read(ser.in_waiting or 1) if data: line_buffer data while b\n in line_buffer: raw_line, line_buffer line_buffer.split(b\n, 1) try: line raw_line.decode(ascii) handle_line(line, pub_fix, pub_raw) except UnicodeDecodeError: pass rate.sleep() if __name__ __main__: main()这里有一个很关键的细节ser.read(ser.in_waiting or 1)。in_waiting返回当前串口缓冲区里有多少个字节可读如果为0则read(1)会阻塞最多0.1秒由timeout决定。这样既能保证CPU占用低又能及时拿到新数据。如果你直接写ser.read(1)在高波特率下读取效率很低容易丢数据如果你写ser.read(1000)则会一直等缓冲区凑满1000个字节导致数据延迟。5.3 坐标转换NMEA和ROS的经纬度格式差异NMEA里的经纬度格式和ROS里NavSatFix用的十进制经纬度格式不同这个非常容易搞错。NMEA的GGA语句里纬度是一个数字比如3104.68201它的含义是31度04.68201分。要转成十进制度数需要先取出整数部分的前两位作为度数31剩下部分4.68201除以60得到小数度数约0.078然后加回31度最终纬度为31.078度左右。同理经度12123.4567表示121度23.4567分转出来是121.3909...度。如果你不管这个格式直接把3104.68201当作31.0468度来用那么误差会大到离谱——大约每差0.1度就会偏出10公里以上。我第一次写这个解析函数时就犯过错用Google地图核对半天发现位置飘到海里去了。所以这一步建议单独写成一个纯函数并写单元测试验证。5.4 运行节点并用rostopic验证把代码保存为rtk_nmea_driver.py放到功能包的scripts/目录下然后给可执行权限chmod x rtk_nmea_driver.py在CMakeLists.txt里加上catkin_install_python(PROGRAMS scripts/rtk_nmea_driver.py DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION})然后运行rosrun your_package rtk_nmea_driver.py _port:/dev/ttyUSB0 _baudrate:115200另外开一个终端rostopic echo /fix rostopic hz /fix rostopic echo /nmea_sentence如果/fix以你配置的频率持续输出经纬度、海拔并且/nmea_sentence能看到原始语句节点就基本通了。6. 现成驱动选用nmea_navsat_driver能做什么6.1 安装与配置如果你不想自己维护节点ROS社区里现成的方案是nmea_navsat_driver包。它帮你把串口读数据、解析NMEA、发布NavSatFix、发布Imu部分语句支持航向角这些工作全部做了。在Noetic下可以直接装sudo apt install ros-noetic-nmea-navsat-driver然后用launch启动launch node namenmea_navsat_driver pkgnmea_navsat_driver typenmea_serial_driver outputscreen param nameport value/dev/ttyUSB0 / param namebaud value115200 / param nameframe_id valuegps_link / rosparam paramtime_ref_sourcegps/rosparam /node /launch6.2 驱动发布的话题驱动起来之后你会看到它发布/fix类型sensor_msgs/NavSatFix包含经纬度和海拔。/fix_velocity包含东北天坐标系下的速度分量。/time_reference用于时间同步的参考时间戳。/imu部分NMEA语句如GPRMC里的航向角可以转成IMU数据。这个包的核心优势是它把NMEA解析做得很完整包括GGA、RMC、GST、ZDA等语句都支持而且已经在大量项目里验证过稳定性没问题。6.3 局限性与扩展思路但现成驱动也有几个问题我用下来最头疼的是第一它默认不判断RTK固定解状态。至少到目前这个版本它把GGA里的定位质量字段没有严格区分比如质量2差分定位和质量4RTK固定解都直接发布成STATUS_FIX。如果你的应用对精度要求高就得吞掉它输出的/fix自己读/nmea_sentence或者在驱动源码里加过滤器。第二时间戳处理方式不一定符合你的需求。驱动默认会用接收到的NMEA时间或者系统当前时间给/fix打时间戳。在我的项目里因为RTK和激光雷达需要做时间同步我更希望话题时间戳是接收到串口数据那一刻的系统时间这样才能和ROS的其他传感器时间站在同一个基准面上。此时我会把time_ref_source参数做相应处理或者干脆在输出的回调里重新定义header.stamp。第三它对单点解和浮点解的置信度没有输出。如果你要拿RTK数据做车道级定位或者扫描建图不仅需要知道经纬度还需要知道当前解的稀释度、伪距误差标准差GST语句里的数据。现成驱动不会把这些原始字段透传出来你需要自己扩展。所以我的建议是如果你只是想把RTK数据拿来看一眼或者精度要求不高直接用nmea_navsat_driver最省事如果你需要做高精度建图、车道级导航或者需要严格区分固定解状态那最好自写节点或者至少在现成节点外面再加一层质量过滤。7. 实际使用中的几个坑7.1 串口权限导致的启动失败这个话题在文档里讲得不多但实际遇到概率极高。特别是你用systemd服务或者roslaunch开机自启动RTK节点时环境里没有交互式终端usermod加的组权限可能不会在会话里生效。表现为手动rosrun没问题一加上launch文件启动就报Permission denied。解决方式一是用前面提到的udev规则把MODE设置为0666二是在launch文件里给节点显式指定用户或者用一个udev规则配合SYMLINK固定设备名。我实际项目里就是用固定名加0666权限彻底绕开了权限问题系统重启后节点自动连接不用人工干预。7.2 波特率配置不一致有个场景我印象很深模块说明书上写着默认波特率115200但是这块模块之前被测试人员用上位机改成过460800我直接用115200去读串口完全不出数据。排查老半天最后用上位机软件才发现波特率被改了。所以如果你发现模块上电后命令行完全没输出先不要怀疑硬件坏了试着用几个常见波特率轮询一遍for baud in 9600 115200 230400 460800 921600; do echo $baud timeout 1 cat /dev/ttyUSB0 sleep 0.5 done另一个场景是两个模块串口对接时一个模块的波特率配置和另一个不一致也会导致数据收发异常。注意RTK模块的串口波特率往往和差分数据链路电台、网络不是一回事需要单独确认。7.3 时间戳与UTC时区NMEA里的时间字段全是UTC时间不带时区。如果你在做传感器融合ROS中的时间戳最好统一用rospy.Time.now()即系统时间因为激光雷达、相机的时间戳都是以系统时间为基准的。千万别直接用NMEA里解析出来的UTC时间戳覆盖header.stamp否则在时区不是UTC的机器上你的RTK数据和其他传感器数据会整体偏8小时或对应时差融合时要么轨迹飘要么地图错位。我之前做过一个室外建图项目RTK时间戳没处理导致每帧点云和GPS坐标差了8小时程序直接以为传感器静止了地图糊成一团。后来把header.stamp改成系统接收时间问题立刻消失。7.4 固定解状态判断一定要做这个坑我在2.1节提过这里给一个可落地的判断逻辑。GGA语句的第6个字段数值状态建议处理0未定位丢弃1单点定位只能当普通GPS用2差分定位有SBAS或局域差分精度亚米级4RTK固定解厘米级放心用5RTK浮点解分米级可继续观测在自写节点里可以直接在handle_line里判断quality只有等于4或5时才往/fix里发数据。如果你用现成驱动可以加一层订阅者节点订阅/nmea_sentence解析出质量字段做过滤或者直接修改驱动的源码。我个人的经验是固定解状态比经纬度数值更容易暴露问题。有一次在树荫下测试RTK模块半天不固定但是单点解的数据依然在正常发布从/fix话题看起来定位没问题但实际上精度已经差了好几米。所以在系统里最好有可视化手段显示当前解类型比如在RVIZ里用不同颜色的图标标识固定解、浮点解、单点解这样室外测试时一眼就能发现问题。7.5 串口数据丢失与缓冲区溢出如果你发现rostopic hz /fix的频率不稳定或者/nmea_sentence有丢行大概率不是模块的问题而是读取逻辑不够及时。NMEA语句在115200波特率下每秒大约11520字节但模块的更新率可能是10Hz每条语句长度80个字节左右每秒输出800字节其实压力不算大。真正的瓶颈在于串口数据是流式的如果你在回调里做耗时操作比如写文件、网络发送下一个字节到了却没读内核缓冲区满后就会丢数据。解决办法是在串口读取线程里只做轻量解析和发布绝对不做阻塞操作。如果需要保存数据最好在另一个线程里订阅/nmea_sentence然后写文件避开串口读取路径上的瓶颈。7.6 天线的限制与固定率最后说一个和代码无关、但直接影响数据质量的点RTK模块能不能固定解很多时候取决于天线视野。在树荫、高楼旁、隧道口附近卫星信号被遮挡或反射固定解率会大幅下降。这时候不管你代码写得再好数据质量也无法保证。所以测试时尽量选开阔场地我一般会先在一个稳定的空地上确认模块能固定解再去做长时间跑测避免把环境问题误判成软件问题。8. 从串口字节到高精度话题整个流程的复盘到这里完整链路已经走通了Ubuntu识别串口设备处理权限和稳定命名命令行验证NMEA原始数据自写节点完成读取、解析、发布NavSatFix和原始NMEA话题或者直接用nmea_navsat_driver快速接进ROS最后根据实际场景处理固定解状态、时间戳、波特率等细节。在实际项目中我通常会把自写节点和现成驱动结合使用自写节点负责读取串口和解析关键字段同时把原始NMEA数据完整发出来然后单独起一个质量过滤节点订阅原始NMEA实时统计固定解率并决定往后续算法里喂多少数据。这样既保证链路简单可控又保留了原始数据可追溯性。如果你刚入门可以直接照着第5节的代码跑通第一个版本然后再逐步加功能。这个流程我帮好几个同事梳理过他们普遍反馈卡住的地方不是ROS本身而是串口通信和NMEA格式这两个老派的“基本功”。所以如果你在调的时候遇到奇怪的问题先回到命令行检查串口数据是不是正常的再回到ROS层面找问题比在代码里反复调试要快得多。分享一下我现在的固定习惯每次接一块新的RTK模块第一步永远是cat /dev/ttyUSB0看看原始数据第二步看GGA里的quality字段第三步才写代码或者接驱动。这套流程帮我省下来的调试时间远比最开始那个半小时多得多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。