
1. 项目概述为机器人平台装上“眼睛”在机器人、无人机或者任何需要自主导航的移动平台上定位系统就是它的“眼睛”。最近我在为一个基于NVIDIA Jetson Orin Nano的reComputer Robotics J3011机器人平台集成高精度定位功能时选择了移远通信的EM12-G GNSS模块。这个组合听起来很酷但实际配置过程远不是插上就能用那么简单涉及到硬件连接、系统驱动、服务配置和精度优化等一系列环节。如果你也正在为你的J3011寻找稳定可靠的定位方案或者手头有类似的GNSS模块需要调试那么这篇从零开始的踩坑实录应该能帮你省下不少时间。EM12-G是一款支持多星系GPS, GLONASS, BeiDou, Galileo的GNSS模块通过串口输出标准的NMEA-0183协议数据。而reComputer Robotics J3011作为一款强大的边缘AI计算设备其接口和系统环境有其特殊性。本指南的核心就是打通从EM12-G的物理引脚到J3011系统内部能够被上层应用如ROS2的Nav2、自动驾驶栈稳定调用的定位数据流。整个过程不仅关乎连接是否正确更涉及到如何在资源有限的边缘端确保定位数据的低延迟、高可用性。接下来我会从硬件连线开始一步步拆解设置过程中的每个技术细节和可能遇到的坑。2. 硬件连接与接口确认硬件连接是第一步也是最容易出错的一步。reComputer Robotics J3011提供了丰富的扩展接口而EM12-G模块通常以封装好的板卡或直接以芯片形式存在我们需要准确找到通信引脚。2.1 接口定义与引脚匹配首先明确双方的关键接口。EM12-G模块的核心通信接口是一个UART通用异步收发传输器用于传输NMEA数据。通常我们需要关注以下四个引脚VCC电源输入EM12-G的典型工作电压是3.3V。绝对禁止接入5V否则会瞬间损坏模块。GND电源地必须与J3011的GND可靠连接。TXD模块的发送引脚对应J3011的接收引脚RXD。RXD模块的接收引脚对应J3011的发送引脚TXD。注意在仅需读取定位数据的场景下RXD可以不接但为了后续发送配置命令如切换波特率、启用GLONASS建议一并连接。reComputer Robotics J3011的40针GPIO扩展接头兼容树莓派引脚排列。对于串口通信我们通常使用其中的UART0对应物理引脚第8脚/TXD0第10脚/RXD0。这是J3011上最常用的调试和外部模块通信串口。连接方案准备杜邦线母对母和万用表。供电将J3011 GPIO的1号引脚3.3V连接到EM12-G的VCC。接地将J3011 GPIO的任意GND引脚如6, 9, 14, 20等连接到EM12-G的GND。数据线将J3011 GPIO的第8脚TXD0连接到EM12-G的RXD将J3011 GPIO的第10脚RXD0连接到EM12-G的TXD。注意连接前务必用万用表确认J3011 GPIO引脚3.3V输出正常并且确保没有短路。线序接反是新手最常犯的错误TXD对TXD会导致通信完全失败。2.2 电源与天线注意事项电源稳定性直接影响GNSS模块的搜星速度和定位精度。J3011的3.3V引脚输出能力有限如果EM12-G模块板载了额外的LED或电平转换芯片可能引起电压跌落。一个实用的技巧是在VCC和GND之间并联一个100μF的电解电容和一个0.1μF的陶瓷电容分别用于缓冲低频和高频噪声这能显著提升模块在机器人电机启停等干扰环境下的稳定性。天线是GNSS的“耳朵”其重要性不言而喻。天线选型必须使用主动式GNSS天线即内置LNA低噪声放大器的有源天线。EM12-G需要为天线提供3V或5V的偏置电压以驱动LNA。天线接口确保天线接头与模块的RF接口匹配通常是SMA或MMCX。天线摆放这是影响性能的关键。尽可能将天线放置在金属接地平面之上并远离J3011主板、电机驱动器、Wi-Fi天线等强干扰源。在机器人上最好的位置通常是车体顶部中央且天空视野开阔无遮挡。我曾尝试将天线放在亚克力板下搜星数直接减半定位漂移严重。3. 系统配置与驱动层调试硬件连接无误后我们需要在J3011的Ubuntu系统上配置串口并验证数据流。3.1 串口设备识别与权限设置连接好硬件并给J3011上电。打开终端首先检查系统是否识别到了串口设备。ls -l /dev/ttyTHS*正常情况下你会看到/dev/ttyTHS0、/dev/ttyTHS1等设备文件。Jetson平台的串口控制器通常命名为ttyTHSx。我们使用的GPIO上的UART0对应的是/dev/ttyTHS1。这一点与树莓派的ttyAMA0或ttyS0不同务必确认。默认情况下普通用户无权访问串口设备。我们需要将当前用户加入dialout组并修改设备权限。sudo usermod -a -G dialout $USER sudo chmod 666 /dev/ttyTHS1第一条命令将用户加入组需要注销并重新登录才能生效。第二条命令直接修改设备权限但这是临时性的重启后可能恢复。为了永久生效更推荐使用udev规则echo KERNELttyTHS1, MODE0666 | sudo tee /etc/udev/rules.d/99-ttyTHS1.rules sudo udevadm control --reload-rules sudo udevadm trigger这样每次启动后/dev/ttyTHS1都对所有用户可读可写。3.2 串口通信测试与参数配置接下来我们使用最常用的串口调试工具screen或minicom来查看原始NMEA数据。首先需要知道EM12-G的默认通信参数。查阅移远通信的文档可知EM12-G默认波特率通常是9600数据位8停止位1无校验位8N1。使用screen进行测试sudo screen /dev/ttyTHS1 9600如果连接和配置正确你应该会看到屏幕上开始滚动出现以$GP、$GL、$GA、$BD等开头的文本行例如$GNGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 $GNRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A按CtrlA然后按K再按Y可以退出screen。如果屏幕一片空白请按顺序排查硬件连接再次确认VCC、GND、TXD、RXD交叉连接无误。设备节点确认使用的是/dev/ttyTHS1而不是其他。波特率尝试其他常见波特率如115200、57600。EM12-G也支持这些速率但需要后续通过AT命令配置。天线状态确保天线已连接且放置在窗外或开阔地。首次定位冷启动可能需要几分钟。配置固定波特率为了获得更高的数据刷新率例如10Hz我们通常会将波特率设置为115200。这需要通过串口向EM12-G发送AT命令。由于我们只有一路串口连接可以借助echo命令# 先以默认9600波特率配置模块 echo -e ATQGPSAT115200\r\n | sudo tee /dev/ttyTHS1然后立即以115200波特率重新连接screensudo screen /dev/ttyTHS1 115200如果看到OK响应并且NMEA数据流正常说明波特率修改成功。注意这个修改是掉电非易失的模块会记住这个设置。4. 软件层集成与数据解析获得稳定的原始数据流后我们需要在应用层使用这些数据。通常有两种方式直接读取串口解析或者使用GNSSDGNSS Daemon这类服务。4.1 方案一直接串口读取与解析对于Python应用使用pyserial库是最直接的方式。下面是一个简单的示例脚本它读取、解析GGA语句包含时间、经纬度、海拔、卫星数等信息并打印。import serial import pynmea2 import time def parse_gnss_data(port/dev/ttyTHS1, baudrate115200): ser serial.Serial(port, baudrate, timeout1) print(fConnected to {port} at {baudrate} baud.) try: while True: line ser.readline().decode(ascii, errorsignore).strip() if line.startswith($GNGGA) or line.startswith($GPGGA): # 兼容单系统和多系统 try: msg pynmea2.parse(line) print(fTime: {msg.timestamp} | fLat: {msg.latitude:.6f} {msg.lat_dir} | fLon: {msg.longitude:.6f} {msg.lon_dir} | fAlt: {msg.altitude} {msg.altitude_units} | fSatellites: {msg.num_sats} | fFix Quality: {msg.gps_qual}) except pynmea2.ParseError as e: print(fParse error: {e}) except KeyboardInterrupt: print(\nExiting.) finally: ser.close() if __name__ __main__: parse_gnss_data()这个脚本的核心是pynmea2库它能自动处理NMEA语句的校验和并解析出结构化数据。注意事项串口读取是阻塞操作在机器人主循环中需要放在单独的线程里避免卡住其他任务。4.2 方案二使用GNSSD服务推荐对于复杂的机器人系统尤其是使用ROS2的场合更推荐使用gnssd或gpsd这样的守护进程。它们管理串口连接提供统一的TCP/IP或共享内存接口并可以聚合多星系数据提高精度和可用性。以gpsd为例进行安装和配置sudo apt update sudo apt install gpsd gpsd-clients安装后需要停止并禁用系统自动启动的gpsd服务因为我们希望用自定义参数启动它。sudo systemctl stop gpsd sudo systemctl disable gpsd sudo systemctl stop gpsd.socket sudo systemctl disable gpsd.socket然后手动启动gpsd并指定我们的设备和协议sudo gpsd /dev/ttyTHS1 -F /var/run/gpsd.sock -n -S 2947参数解释/dev/ttyTHS1指定GNSS模块设备。-F /var/run/gpsd.sock指定控制socket位置。-n不等待客户端连接就轮询GPS。这对于无人值守的机器人系统很重要。-S 2947在2947端口监听TCP连接允许其他程序如cgps,gpsmon连接。测试gpsd是否工作cgps -s或者使用更详细的监视器gpsmon如果一切正常你将看到一个动态更新的界面显示卫星星空图、定位信息、各星系卫星信噪比等丰富数据。将gpsd设为自启动编辑systemd服务文件或创建一个启动脚本。更简单的方法是编辑/etc/default/gpsdSTART_DAEMONtrue USBAUTOfalse DEVICES/dev/ttyTHS1 GPSD_OPTIONS-n -S 2947然后启用服务sudo systemctl enable gpsd sudo systemctl start gpsd4.3 在ROS2中集成GNSS数据对于ROS2 Humble或Foxy我们可以使用nmea_navsat_driver包来将gpsd或直接串口的数据转换为ROS2标准消息sensor_msgs/msg/NavSatFix和sensor_msgs/msg/NavSatStatus。首先安装相关包sudo apt install ros-${ROS_DISTRO}-nmea-msgs ros-${ROS_DISTRO}-nmea-navsat-driver创建一个启动文件例如gnss_bringup.launch.pyfrom launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ # 方案A通过gpsd桥接推荐更稳定 Node( packagenmea_navsat_driver, executablegpsd_client_node, namegpsd_client, outputscreen, parameters[{use_polling: True}] # 主动轮询更及时 ), # 方案B直接读取串口备选 # Node( # packagenmea_navsat_driver, # executablenmea_serial_driver_node, # namenmea_serial_driver, # outputscreen, # parameters[{ # port: /dev/ttyTHS1, # baud: 115200, # frame_id: gps_link # }] # ) ])启动后你可以通过ros2 topic echo /fix来查看高精度的经纬度、海拔和协方差信息。这个NavSatFix消息可以直接被ROS2导航栈如robot_localization包融合到机器人的定位中。5. 精度优化与故障排查实录即使一切连通定位数据也可能存在跳变、漂移或延迟大的问题。以下是我在实践中总结的优化点和常见故障的排查方法。5.1 精度优化技巧启用多星系与SBASEM12-G支持GPS、GLONASS、北斗和伽利略。通过AT命令可以启用所有系统并启用星基增强系统SBAS如WAAS、EGNOS这能显著提升在开阔环境的精度。# 通过screen发送命令或使用python脚本 # ATQGPSCFGgpsnmeatype,31 # 启用所有NMEA语句类型 # ATQGPSCFGgnssconfig,3 # 启用GPSGLONASSBeiDou (30b11?) # 更准确的做法是查阅EM12-G的AT命令手册使用正确的参数。实际上更可靠的方法是使用gpsd它默认会从NMEA流中自动识别和融合所有可见星系的数据。设置固定输出频率默认的1Hz输出对于高速移动的机器人可能不够。可以将输出频率设置为5Hz或10Hz。# 示例设置GGA和RMC语句输出频率为5Hz # ATQGPSGNMEAGGA,5 # ATQGPSGNMEARMC,5注意提高输出频率会增加串口数据量请确保波特率足够高如115200或以上。关注信噪比(SNR)与卫星几何分布使用gpsmon或cgps -s查看各卫星的信噪比。信噪比低于20的卫星贡献的定位数据噪声较大。理想的定位需要卫星在天空中以良好的几何形状分布低DOP值如PDOP3.0。如果卫星都挤在一个角落即使数量多精度也会很差。时间同步GNSS提供高精度的时间信息。可以利用chrony或ntpd将J3011的系统时间与GNSS时间同步这对于多传感器融合如相机、激光雷达至关重要。sudo apt install chrony sudo vim /etc/chrony/chrony.conf # 添加一行refclock SHM 0 offset 0.5 delay 0.2 refid GPS sudo systemctl restart chrony # 查看同步状态chronyc sources -v5.2 常见问题与排查表下表罗列了我在集成EM12-G与J3011过程中遇到的最典型问题及解决方法问题现象可能原因排查步骤与解决方案screen无任何输出1. 电源未接通或电压不对。2. TXD/RXD线序接反。3. 波特率不匹配。4. 串口设备号错误。1. 用万用表测量VCC与GND之间电压是否为3.3V。2. 交换TXD和RXD的连接线。3. 尝试9600, 115200, 57600等常见波特率。4. 执行ls /dev/tty*对比插拔模块前后的设备列表变化。输出乱码波特率设置错误。确认EM12-G的当前波特率可尝试所有常见值并与screen或minicom的设置保持一致。有数据输出但$GPGGA语句中定位状态为0无效或1单点定位1. 天线未接或损坏。2. 天线放置位置不佳室内、金属遮挡。3. 冷启动时间不足。1. 检查天线连接是否牢固尝试更换天线。2. 将天线移至户外开阔地带。3. 耐心等待5-15分钟。冷启动后首次定位TTFF需要时间。gpsd启动失败或cgps无数据1. 设备被其他进程占用。2.gpsd服务配置错误。3. 权限问题。1.sudo lsof /dev/ttyTHS1查看占用进程并结束它。2. 检查/etc/default/gpsd中DEVICES路径是否正确。3. 确认当前用户在dialout组或设备权限为666。定位坐标漂移大数十米1. 多路径效应信号经建筑物反射。2. 卫星几何分布差高DOP值。3. 未使用SBAS校正。1. 避免天线靠近大型金属表面或墙体。2. 观察gpsmon中的DOP值等待卫星几何分布改善。3. 通过AT命令确认SBAS已启用。ROS2节点收不到/fix话题1.gpsd未运行或未发布数据。2. NMEA语句类型不包含GGA/RMC。3. ROS2节点命名空间或重映射错误。1. 运行systemctl status gpsd和gpsmon先验证gpsd本身。2. 检查原始NMEA流是否包含$xxGGA语句。3. 使用ros2 topic list查看所有话题确认是否有/fix。5.3 一个关键的实操心得处理串口数据阻塞在早期测试中我发现当Python脚本直接读取串口时如果处理不及时串口缓冲区会积压导致读取的数据严重延迟甚至错过关键数据帧。这对于需要实时定位的机器人是致命的。解决方案是使用双线程一个线程专用于不间断地、非阻塞地读取串口原始数据并放入队列另一个主线程或另一个工作线程从队列中取出数据进行解析和发布。pyserial的readline()在超时设置不当时是阻塞的可以使用serial.Serial的in_waiting属性进行非阻塞读取。import serial import threading import queue import pynmea2 class SerialReader(threading.Thread): def __init__(self, port, baudrate, data_queue): super().__init__() self.ser serial.Serial(port, baudrate, timeout0.1) # 短超时 self.data_queue data_queue self.running True def run(self): buffer while self.running: if self.ser.in_waiting 0: try: raw_data self.ser.read(self.ser.in_waiting).decode(ascii, errorsignore) buffer raw_data lines buffer.split(\n) for line in lines[:-1]: # 最后一段可能不完整 if line.startswith($): self.data_queue.put(line.strip()) buffer lines[-1] # 保留不完整的部分 except Exception as e: print(fSerial read error: {e}) else: time.sleep(0.001) # 短暂让出CPU self.ser.close() # 在主线程中从data_queue里get数据并解析这个模式彻底解决了数据延迟和丢失的问题在后续的ROS2节点开发中我也采用了类似的生产者-消费者模式。