简介面向 ROS 机器人开发者与 GPS 导航学习者这份资料围绕 huace GPCHC 解析模块如何接入 ROS 环境展开核心解决 GPS 接收机数据在机器人系统中的读取、解析与复用问题。讲解从卫星信号到 NavSatFix 标准消息的转换链路涵盖 GPCHC 协议字段解读、Python/C 驱动编写思路、ROS 话题发布与订阅配置以及后续定位、导航模块的衔接方式。压缩包约 20.56MB目前上游未提供文件总数与格式明细内容主要偏向协议解析与集成流程总结。已有 340 人学习浏览适合正在做移动机器人定位、无人机导航、无人车测试或需要将华测 GPS 设备接入 ROS 的软硬件工程师参考。通过这份资料可快速理解 GPCHC 数据的组织方式与解析要点掌握 ROS 中 GPS 消息的定义和发布订阅实现逻辑并借助其中的调试建议定位坐标异常、驱动报错等常见问题为实际项目搭建提供可直接借鉴的落地思路。1. huace GPCHC 解析 ros华测接收机进 ROS 的第一步搞移动机器人、无人车或者测绘无人机的人迟早会遇到一个尴尬场景手上是一台华测CHC Navigation的RTK/GNSS接收机串口里不断吐出一行行以$GPCHC开头的ASCII语句想把位置和速度喂给ROS里的导航栈却发现网上找不到对应的现成驱动。用roslaunch起来一个标准nmea_navsat_driver收到的数据却对不上字段因为 GPCHC 是华测自定义的 NMEA 扩展语句字段布局和$GPGGA、$GPRMC并不一致。这个标题做的事情就是把$GPCHC这一帧拆干净转成sensor_msgs/NavSatFix和geometry_msgs/TwistStamped话题让下游的robot_localization、mapviz或者自己的导航节点能直接消费。这篇笔记适合两类人一类是刚接触 ROS 和 GNSS 的新手想照着代码把串口数据跑起来另一类是已经能收到数据但在坐标系、时间戳、RTK状态字这些细节上踩坑的熟手。先说一个反直觉的结论GPCHC 的字段顺序不是查一次手册就能一劳永逸的固件版本一升级字段索引可能就变了后面会专门讲怎么应对这种“翻车”。2. 拆开 GPCHC 帧字段布局、单位与帧同步逻辑2.1 为什么不是标准 NMEA自定义语句的由来NMEA 0183 协议里定位相关的常用语句是$GPGGA位置、质量因子、$GPRMC位置、速度、航向、$GPVTG对地速度。标准语句的优点是生态好nmea_navsat_driver这类现成驱动直接吃缺点是字段分散在不同语句里一条语句只报一部分内容组合导航场景下还得做时间对齐。华测在部分接收机固件里把位置、高程、北向速度、东向速度、解状态、卫星数、航向压缩进了同一条$GPCHC语句相当于自定义了一条超级 GGA。对使用者来说这是好事一帧就能拿到全部导航信息但代价是没法直接用现成驱动得自己写解析器而且帧结构没有公开标准文档每个固件版本之间还可能微调。帧结构本身还是遵守 NMEA 的基本框架$开头、逗号分隔、*后面跟两位十六进制异或校验和、\r\n结尾。所以“解析 ros”这件事的技术难点不在于读串口而在于把这条语句里的每一个字段确认清楚并且做好半包、错帧、校验失败的兜底。2.2 一帧典型输出长什么样字段表以我常见到的一种 GPCHC 输出为例帧内容大概是这样的$GPCHC,122345.00,3102.123456,N,12112.654321,E,10.25,3.28,-0.12,1,8,0.85,0.72,245.3*5F把字段拆开看含义如下表所示。注意我这里写的是“常见布局”你的接收机固件可能有差异拿到真机后一定要先抓一段原始数据对着核对。字段索引示例含义单位1122345.00UTC 时间时/分/秒hhmmss.ss23102.123456纬度注意是度分格式还是十进制度ddmm.mmmm 或 dd.dddddd3N北纬/南纬-412112.654321经度dddmm.mmmm 或 ddd.dddddd5E东经/西经-610.25椭球高米73.28北向速度米/秒8-0.12东向速度米/秒91解状态标志0无解1单点2RTK浮点3RTK固定108参与定位的卫星数颗110.85位置精度因子 PDOP-120.72垂直精度因子 VDOP-13245.3航向角相对北向度*5F校验和十六进制拿到真机数据后标准做法是先用 minicom 或 cutecom 把串口原始输出存成文件然后按$GPCHC搜出几条完整帧手工数一遍逗号分隔的字段索引再和这张表对照。最容易出问题的字段是第 2、4 位的经纬度格式华测有的固件输出ddmm.mmmm的度分格式有的输出十进制度解析代码里少乘一个 60 就会产生纬度差半度级别的误差这在后续建图和定位里是灾难。2.3 帧同步与校验先能认帧再谈解析GPCHC 解析的第一步不是拆字段而是从串口字节流里切出完整帧。串口是流式传输read()返回的数据长度不确定可能一次读回来半帧也可能一次读回来两帧半。如果只按\n切分并把剩余部分丢弃会发现定位话题时有时无。正确的做法是把读到的数据追加进缓冲区按换行符切出完整行不完整的残帧留在缓冲区等下一次读取。下面这段是帧同步核心逻辑可以直接抄def extract_gpchc(buf: bytes): lines buf.split(b\n) # lines 里的最后一段是不完整的残帧留到下一次 buf lines.pop() for line in lines: line line.rstrip(b\r) if not line.startswith(b$GPCHC): continue star line.find(b*) if star -1: rospy.logwarn_once(incomplete frame, skip) continue if xor_checksum(line) ! int(line[star1:star3], 16): rospy.logwarn_throttle(5, checksum failed) continue yield line为什么要用\n切而不是\r\n切因为某些串口工具或接收机固件在数据量大时可能丢掉\r只按\n切更稳妥最后再统一rstrip掉\r即可。buf lines.pop()这一行是半包处理的关键弹出的最后一段没有换行符属于不完整数据不能丢弃。校验和的算法是 NMEA 标准的异或校验从$后面第一个字节开始到*前面的所有字节逐位异或。代码实现如下def xor_checksum(line: bytes) - int: body line[1:line.find(b*)] checksum 0 for b in body: checksum ^ b return checksum注意索引line[0]是$本身不参与异或*后面的两个 ASCII 十六进制字符才是接收机算出的校验值。如果校验不对不要硬解析宁可丢帧也不要发布脏数据进导航栈。3. 写一个 ROS 解析节点从串口到 NavSatFix 话题3.1 节点骨架与串口参数串口读取我用pyserial配合rospy发布话题。节点骨架保持简单一个类负责读取与解析一个循环发布。需要配置的参数就四个大多数场景不用动参数名默认值说明~port/dev/ttyUSB0串口设备路径~baud115200波特率需与接收机面板一致~frame_idbase_gps经纬度所在的坐标系一般定义成车体上的 GPS 天线安装位置~publish_rate10发布频率上限接收机输出频率高于此时会被降频launch 文件直接写在包下面方便用roslaunch起整条管线launch node namegpchc_parser pkggpchc_driver typegpchc_parser.py outputscreen param nameport value/dev/ttyUSB0/ param namebaud value115200/ param nameframe_id valuebase_gps/ param namepublish_rate value10/ /node /launch启动之前先确认串口权限。Ubuntu 下常见做法是把当前用户加进dialout组或者临时给设备加权限sudo usermod -aG dialout $USER sudo chmod 666 /dev/ttyUSB0加组之后要重新登录一次会话才会生效。如果不想每次插拔都改权限可以写 udev 规则但那是另一篇笔记的事这里不展开。3.2 解析并发布 NavSatFix / TwistStamped完整解析节点代码如下这份代码我测试过基础路径你可以按自己的接收机固件微调字段索引#!/usr/bin/env python3 import serial import rospy from sensor_msgs.msg import NavSatFix, NavSatStatus from geometry_msgs.msg import TwistStamped def check_sum(line: bytes) - bool: body line[1:line.find(b*)] calc 0 for b in body: calc ^ b given int(line[line.find(b*) 1:line.find(b*) 3], 16) return calc given def parse_gpchc(line: bytes): # 去掉 $GPCHC, 前缀后按逗号拆分 fields line.decode(ascii, errorsignore).split(,) data { utc: fields[1], lat: float(fields[2]), lat_dir: fields[3], lon: float(fields[4]), lon_dir: fields[5], alt: float(fields[6]), vn: float(fields[7]), ve: float(fields[8]), status: int(fields[9]), satellites: int(fields[10]), } # 如果固件输出的是度分格式 ddmm.mmmm要转成十进制度 if data[lat] 90: deg int(data[lat] / 100) minutes data[lat] - deg * 100 data[lat] deg minutes / 60.0 if data[lon] 180: deg int(data[lon] / 100) minutes data[lon] - deg * 100 data[lon] deg minutes / 60.0 if data[lat_dir] S: data[lat] -data[lat] if data[lon_dir] W: data[lon] -data[lon] return data def main(): rospy.init_node(gpchc_parser) pub_fix rospy.Publisher(fix, NavSatFix, queue_size10) pub_vel rospy.Publisher(fix_velocity, TwistStamped, queue_size10) port rospy.get_param(~port, /dev/ttyUSB0) baud rospy.get_param(~baud, 115200) frame_id rospy.get_param(~frame_id, base_gps) rate rospy.get_param(~publish_rate, 10) try: ser serial.Serial(port, baud, timeout0.1) except serial.SerialException as e: rospy.logfatal(open serial failed: %s, e) return buf b loop_rate rospy.Rate(rate) while not rospy.is_shutdown(): buf ser.read(256) lines buf.split(b\n) buf lines.pop() # 残帧留在缓冲区 for line in lines: line line.strip() if not line.startswith(b$GPCHC): continue if not check_sum(line): rospy.logwarn_throttle(5, bad checksum) continue data parse_gpchc(line) fix NavSatFix() fix.header.stamp rospy.Time.now() fix.header.frame_id frame_id fix.latitude data[lat] fix.longitude data[lon] fix.altitude data[alt] fix.status.status NavSatStatus.STATUS_FIX if data[status] 1 else NavSatStatus.STATUS_NO_FIX fix.status.service NavSatStatus.SERVICE_GPS pub_fix.publish(fix) tw TwistStamped() tw.header.stamp fix.header.stamp tw.header.frame_id frame_id tw.twist.linear.x data[ve] tw.twist.linear.y data[vn] pub_vel.publish(tw) loop_rate.sleep() ser.close() if __name__ __main__: main()这段代码里最需要说明的是经纬度格式的自动判断。华测固件如果输出度分格式纬度值会大于 90经度值会大于 180所以用阈值判断可以兼容两种输出。status 1映射成STATUS_FIX是保守做法如果你需要严格区分单点解和 RTK 固定解建议把原始状态字单独发布一个 Int32 话题后面接robot_localization时可以用它做观测权重判断。速度方向要注意GPCHC 里面第 7、8 位字段是北向速度和东向速度而TwistStamped的坐标约定是 X 前、Y 左直接塞给融合节点会差一个坐标旋角。常见做法是先把速度旋转到车体系或者干脆只发布速度大小和航向让下游自己处理。这个细节在第 4 章详细讲。3.3 用 rostopic 验证是不是真的通了节点跑起来之后先别急着接导航栈用rostopic验证数据流是最快的排查路径rosrun gpchc_driver gpchc_parser.py rostopic echo -n 5 /fix rostopic hz /fix rostopic bw /fixrostopic echo能看到经纬度数值是否合理rostopic hz判断话题频率是否稳定在接收机输出频率附近rostopic bw看带宽消耗。如果hz输出忽高忽低多半是缓冲区残帧没处理好或者串口超时时间太长导致读取延迟如果完全没话题先用dmesg | grep ttyUSB确认设备在系统里是否存在再用minicom直接看串口原始输出判断是接收机没发数据还是节点解析出错。这一步能省下后面定位不准时排查问题的半天时间。4. 接进导航栈之前坐标转换、时间戳与话题对接4.1 把经纬度变成车体坐标UTM/ENU 的两种接法NavSatFix给的是 WGS84 经纬度导航栈里做里程计融合、路径规划时用的基本是平面直角坐标。两种常见接法方案一是自己写一个经纬度到 UTM 的转换节点维护一个原点把当前位置换算成相对于原点的东北天ENU偏移方案二是用robot_localization包里的navsat_transform_node它内置了经纬度到局部笛卡尔坐标的转换链路能把 GPS 观测和里程计、IMU 一起送进 EKF。方案二更省事我一般用这个。配置注意两点一个是yaw_offset要按 GPS 天线在车体上的安装角设置另一个是magnetic_declination_radians如果你的航向来源是磁罗盘这个值填当地磁偏角如果 GPCHC 里的航向本身就是真北角则填 0。navsat_transform: frequency: 10 delay: 3.0 magnetic_declination_radians: 0.0 yaw_offset: 1.570796327 zero_altitude: false broadcast_utm_transform: true publish_fix_to_odom: false wait_for_datum: truedelay参数很关键。GPS 数据从天线到节点有硬件延迟robot_localization里用延迟补偿来对齐时间戳值设太小时融合结果会抖动设太大时动态响应变差3 秒是个常见起点具体要对比实际跑圈的轨迹看效果。wait_for_datum设为 true 会让转换节点等第一帧有效的经纬度作为原点适合车载场景如果你的车是在固定起点附近工作建议保持这个配置。4.2 时间戳对齐接收机时间转 ROS Time解析 GPCHC 时直接用rospy.Time.now()是最常见但最不严谨的做法。接收机从卫星捕获时间、完成解算、串口发出、ROS 节点收到中间有几十到几百毫秒的延迟在高速移动场景下这段延迟会造成几米级的定位偏差。GPCHC 帧里第一字段就是 UTC 时间应该优先用它作为观测时间戳。from datetime import datetime, timezone def utc_to_ros_time(utc_str: str): # 解析 hhmmss.ss 格式 t datetime.strptime(utc_str, %H%M%S.%f) t t.replace(tzinfotimezone.utc) return rospy.Time.from_sec(t.timestamp())这里有一个容易忽略的点strptime解析122345.00得到的%f部分是 00精确到百分之一秒但如果接收机输出的是整秒没有小数部分解析出来的时间戳精度不够建议在节点里先看一下原始帧里有没有小数点。另一个坑是时区GPCHC 的 UTC 时间不带时区必须按 UTC 解析。如果你在 ROS 里用use_sim_time要记得这个时间转的是 wall clock不是模拟时钟回放 bag 时这个差异尤其明显。ROS 1 和 ROS 2 在包结构上差异明显但 GPCHC 解析逻辑本身可以完全复用。ROS 2 版本里把rospy换成rclpyNavSatFix的包路径从sensor_msgs变成sensor_msgs/msg/NavSatFix串口部分不用动。如果团队里有人想把它跑在 ROS 2 的 Nav2 上建议话题名保持/fix和/fix_velocity这样navsat_transform_node的 ROS 2 迁移版可以直接衔接。5. 解析 GPCHC 的 5 个高频坑现象、原因与修法5.1 串口读到乱码或断流现象节点启动后/fix话题没有数据或者rostopic echo里偶尔出现乱码字符。原因最常见的是波特率不匹配。接收机面板设置为 460800代码里还写 115200读出来全是乱码。另一个原因是 USB 转串口芯片在电压不稳时进入异常状态表现为断流。解决先用 minicom 手工连接确认波特率、数据位 8、停止位 1、无校验流控然后核对代码里的baud参数。最好在代码里加一句串口打开时的参数打印避免param配错了却在代码里写死另一个值。如果是 USB 转串口芯片异常重新插拔设备或者stty -F /dev/ttyUSB0 115200复位串口参数即可恢复。5.2 经纬度差了十倍现象解析出来的经度是 1211.2654321但真实经度应该在 121.12 左右绘制到地图上偏移 10 倍。原因GPCHC 输出的是度分格式ddmm.mmmm直接float()解析后没有除以 60 转换成十进制度。这个坑在 3.2 节代码里已处理但如果你用的是别的解析脚本很容易漏掉。解决解析完判断数值范围纬度值大于 90、经度值大于 180 时按度分转换。更保险的做法是在配置文件里加一个lat_format参数手动指定是degree还是dm不要完全依赖自动判断因为有些边界地区的纬度可能接近 90 度但确实是十进制度。5.3 校验和老是不过现象日志里频繁出现bad checksum但 minicom 里看原始数据帧很正常。原因帧切分时把\r混进了校验区域。有些接收机在换行符前会输出\rsplit(b\n)出来的行尾还带着\rxor_checksum计算时把\r也参与异或校验值当然对不上。解决strip()或rstrip(b\r)处理完行尾再判定帧头确保参与校验计算的部分只有$GPCHC...*两星号之间的内容。建议在extract_gpchc里统一处理好不要在多个地方重复清理。5.4 USB 转串口掉线后节点不恢复现象节点跑着跑着串口拔掉或者 USB hub 供电异常serial.SerialException抛出来后节点直接退出重新插上设备也不会自动恢复。原因代码里ser serial.Serial(...)在main()里只执行一次异常没做重试。解决把串口的打开和读取包在一个重试循环里检测到异常就等待、关闭旧句柄、重新打开。while not rospy.is_shutdown(): try: ser serial.Serial(port, baud, timeout0.1) rospy.loginfo(serial opened: %s, port) break except serial.SerialException as e: rospy.logwarn_throttle(5, open failed: %s, retrying, e) rospy.sleep(2.0)读取循环里也要包一层try/except捕获serial.SerialException后重新执行打开逻辑否则掉线一次就永远沉默这个坑在长时间车载测试时非常容易踩到。5.5 状态位和 RTK 解状态对不上现象接收机面板显示 RTK 固定解但解析出来的NavSatFix.status.status还是STATUS_FIX没有区分固定解和浮点解或者 RTK 状态变成了浮点解导航栈没有感知到精度下降导致融合轨迹漂移。原因在 3.2 节代码里用status 1统一映射成STATUS_FIX把单点解和 RTK 固定解混为一谈。NavSatStatus枚举本来就只有STATUS_NO_FIX、STATUS_FIX、STATUS_SBAS_FIX、STATUS_GBAS_FIX信息粒度不够。解决单独发布一个std_msgs/Int32话题比如/fix_status把原始状态字原样发布出去同时在NavSatFix.status.status里做更细致映射状态字 3RTK 固定映射为STATUS_GBAS_FIX2浮点映射为STATUS_FIX1单点映射为STATUS_SBAS_FIX。下游融合节点再根据状态字决定协方差是收紧还是放宽。这里其实是一种“玄学”做法不同华测固件对状态字的定义略有出入拿到真机后先用卫星数、状态字、PDOP 三者交叉比对一遍再固化映射关系。6. 用 rosbag 回放验证管线离线复现并不难解析节点写完在真车实测之前还有一件事值得做把串口原始数据录成 bag离线回放验证解析逻辑。好处是定位问题不依赖真实场地改一行代码重新跑一次 bag 就能看效果。录 bag 不需要解析节点跑起来直接用rosbag record录原始串口是很别扭的因为串口不是话题。所以实践中是先把 GPCHC 解析节点跑起来同时录它发布的话题rosbag record -O gpchc_test /fix /fix_velocity /fix_status在场地里跑一小段直线和一小段八字录下来的 bag 就是后续调参的“后悔药”。回到实验室后用rosbag play gpchc_test.bag --rate 1.0回放数据配合plotjuggler查看经纬度轨迹、北向速度曲线、状态字变化比在真车上反复改参数效率高得多。验证完解析正确性后我还习惯加一个最简单的单元测试把 3.2 节的parse_gpchc函数单独拎出来喂一条手工构造的校验和正确的帧断言输出纬度和经度在预期误差内。这个测试不需要串口、不需要 ROS master本地python3 test_gpchc.py就能跑防止后续改代码时把字段索引改坏而自己毫不知情。回放时有一个细节如果回放文件里的时间戳是rospy.Time.now()回放出来的轨迹会有时间跳跃如果用接收机 UTC 时间做的时间戳回放时点选--clock参数可以模拟真实的时序。配合robot_localization的delay参数调延迟补偿用同一段 bag 反复对比不同delay值下的轨迹误差是我觉得最省时间的调参方式。这段经验总结成一句话就是先把数据录下来再谈调参。串口数据一旦丢了解析上下文就很难复现而 bag 文件可以让你在办公室里把场地里的问题慢慢剖析。希望这篇笔记能帮你在接入 GPCHC 时少踩几个坑把精力花在真正该花的地方。本文还有配套的精品资源点击获取