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 'KERNEL=="ttyTHS1", MODE="0666"' | 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按Ctrl+A,然后按K,再按Y可以退出screen。
如果屏幕一片空白,请按顺序排查:
- 硬件连接:再次确认VCC、GND、TXD、RXD交叉连接无误。
- 设备节点:确认使用的是
/dev/ttyTHS1,而不是其他。 - 波特率:尝试其他常见波特率,如115200、57600。EM12-G也支持这些速率,但需要后续通过AT命令配置。
- 天线状态:确保天线已连接且放置在窗外或开阔地。首次定位(冷启动)可能需要几分钟。
配置固定波特率:为了获得更高的数据刷新率(例如10Hz),我们通常会将波特率设置为115200。这需要通过串口向EM12-G发送AT命令。由于我们只有一路串口连接,可以借助echo命令:
# 先以默认9600波特率配置模块 echo -e "AT+QGPSAT=115200\r\n" | sudo tee /dev/ttyTHS1然后,立即以115200波特率重新连接screen:
sudo screen /dev/ttyTHS1 115200如果看到OK响应,并且NMEA数据流正常,说明波特率修改成功。注意:这个修改是掉电非易失的,模块会记住这个设置。
4. 软件层集成与数据解析
获得稳定的原始数据流后,我们需要在应用层使用这些数据。通常有两种方式:直接读取串口解析,或者使用GNSSD(GNSS Daemon)这类服务。
4.1 方案一:直接串口读取与解析
对于Python应用,使用pyserial库是最直接的方式。下面是一个简单的示例脚本,它读取、解析GGA语句(包含时间、经纬度、海拔、卫星数等信息)并打印。
import serial import pynmea2 import time def parse_gnss_data(port='/dev/ttyTHS1', baudrate=115200): ser = serial.Serial(port, baudrate, timeout=1) print(f"Connected to {port} at {baudrate} baud.") try: while True: line = ser.readline().decode('ascii', errors='ignore').strip() if line.startswith('$GNGGA') or line.startswith('$GPGGA'): # 兼容单系统和多系统 try: msg = pynmea2.parse(line) print(f"Time: {msg.timestamp} | " f"Lat: {msg.latitude:.6f} {msg.lat_dir} | " f"Lon: {msg.longitude:.6f} {msg.lon_dir} | " f"Alt: {msg.altitude} {msg.altitude_units} | " f"Satellites: {msg.num_sats} | " f"Fix Quality: {msg.gps_qual}") except pynmea2.ParseError as e: print(f"Parse 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/gpsd:
START_DAEMON="true" USBAUTO="false" 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.py:
from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ # 方案A:通过gpsd桥接(推荐,更稳定) Node( package='nmea_navsat_driver', executable='gpsd_client_node', name='gpsd_client', output='screen', parameters=[{'use_polling': True}] # 主动轮询,更及时 ), # 方案B:直接读取串口(备选) # Node( # package='nmea_navsat_driver', # executable='nmea_serial_driver_node', # name='nmea_serial_driver', # output='screen', # parameters=[{ # 'port': '/dev/ttyTHS1', # 'baud': 115200, # 'frame_id': 'gps_link' # }] # ) ])启动后,你可以通过ros2 topic echo /fix来查看高精度的经纬度、海拔和协方差信息。这个NavSatFix消息可以直接被ROS2导航栈(如robot_localization包)融合到机器人的定位中。
5. 精度优化与故障排查实录
即使一切连通,定位数据也可能存在跳变、漂移或延迟大的问题。以下是我在实践中总结的优化点和常见故障的排查方法。
5.1 精度优化技巧
启用多星系与SBAS:EM12-G支持GPS、GLONASS、北斗和伽利略。通过AT命令可以启用所有系统,并启用星基增强系统(SBAS,如WAAS、EGNOS),这能显著提升在开阔环境的精度。
# 通过screen发送命令,或使用python脚本 # AT+QGPSCFG="gpsnmeatype",31 # 启用所有NMEA语句类型 # AT+QGPSCFG="gnssconfig",3 # 启用GPS+GLONASS+BeiDou (3=0b11?) # 更准确的做法是查阅EM12-G的AT命令手册,使用正确的参数。实际上,更可靠的方法是使用
gpsd,它默认会从NMEA流中自动识别和融合所有可见星系的数据。设置固定输出频率:默认的1Hz输出对于高速移动的机器人可能不够。可以将输出频率设置为5Hz或10Hz。
# 示例:设置GGA和RMC语句输出频率为5Hz # AT+QGPSGNMEA="GGA,5" # AT+QGPSGNMEA="RMC,5"注意:提高输出频率会增加串口数据量,请确保波特率足够高(如115200或以上)。
关注信噪比(SNR)与卫星几何分布:使用
gpsmon或cgps -s查看各卫星的信噪比。信噪比低于20的卫星贡献的定位数据噪声较大。理想的定位需要卫星在天空中以良好的几何形状分布(低DOP值,如PDOP<3.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 -v
5.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, timeout=0.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', errors='ignore') 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(f"Serial read error: {e}") else: time.sleep(0.001) # 短暂让出CPU self.ser.close() # 在主线程中,从data_queue里get数据并解析这个模式彻底解决了数据延迟和丢失的问题,在后续的ROS2节点开发中,我也采用了类似的生产者-消费者模式。