RTK Ultra 定位定向模块
RTK Ultra®
版本: V1.0
本手册内容仅用于 RTK Ultra 产品的安装、配置和使用,不作为其他用途。
阅读提示
- 建议先观看官方教学视频
- 再阅读本手册及随产品附带的文档
- 如有问题请联系官方技术支持工程师
产品介绍
RTK Ultra 集成 RTK 高精度定位与双天线航向测向技术,支持串口 RTCM 差分数据输入、4G CORS 网络 RTK,并可选配 星基 RTK 高精度定位服务,实现 厘米级精准定位与高可靠航向解算,满足复杂环境下移动机器人对高精度位置与姿态信息的需求。 模块采用 小型化、高集成度设计,兼顾性能、可靠性与系统集成效率,广泛适用于 无人机、机器狗、人形机器人、无人驾驶车辆、无人船等智能移动平台。 支持 DroneCAN、UART、USB 等多种通信接口,可快速适配 PX4、ArduPilot(APM) 无人机生态,以及 宇树、云深处、智身智能等主流机器人平台。支持 5–26 VDC 宽电压输入,降低二次开发与系统集成难度,为各类无人系统提供稳定、可靠的高精度定位与航向解决方案。
参数指标
| 项目 | 分类 | 参数 |
|---|---|---|
| 星座 | BDS / GPS / GLONASS / Galileo / QZSS | |
| 主 / 从天线频点 | BDS | B1I、B2I、B3I |
| GPS | L1C/A、L2P(Y)、L2C、L5 | |
| GLONASS | G1、G2 | |
| Galileo | E1、E5a、E5b | |
| QZSS | L1、L2、L5 | |
| 定位精度 | 单点定位 (RMS) | 平面:1.5 m 高程:2.5 m |
| DGPS | 平面:0.4 m + 1 ppm 高程:0.8 m + 1 ppm |
|
| RTK | 平面:0.8 cm + 1 ppm 高程:1.5 cm + 1 ppm |
|
| PPP | 平面:5 cm 高程:10 cm |
|
| 定向精度 (RMS) | 0.1° / 1 m 基线 | |
| PPS 精度 (RMS) | 20 ns | |
| 速度精度 (RMS) | 0.03 m/s | |
| 首次定位时间 | 冷启动 | < 30 s |
| 热启动 | < 4 s | |
| 数据更新率 | 调试软件可修改 | 默认 10 Hz,最大 20 Hz |
接口定义
该模块预留 CAN 与 UART 通信接口,接口定义如图所示。
其中,CAN 接口采用 DroneCAN 标准协议,可便捷接入 PX4、ArduPilot(APM) 等主流开源飞控平台,实现定位与航向信息的高效传输。
模块同时提供两路串口:
- UART1:用于输出定位与航向数据;
- UART2:用于接收 RTCM 差分校正数据,便于接入外部差分源或其他设备。
| 名称 | 定义 | 备注说明 |
|---|---|---|
| VCC | 电源正 | 5–26 V,绝对值不可超过 28 V |
| GND | 电源地 | 同时作为信号地 |
| CAN_H | CAN 总线 H | CAN 标准信号 |
| CAN_L | CAN 总线 L | CAN 标准信号 |
| TX1 | 串口 1 发送 | 支持3.3 V / 5 V TTL 信号 |
| RX1 | 串口 1 接收 | 支持3.3 V / 5 V TTL 信号 |
| TX2 | 串口 2 发送 | 支持3.3 V / 5 V TTL 信号 |
| RX2 | 串口 2 接收 | 支持3.3 V / 5 V TTL 信号 |
参数设置
通过本模块配套的调参软件可对模块的参数进行设置。RTK 通过 Type-C 接口连接至电脑,运行 RTK-Manager-V2.1.exe 后选择正确的端口号并打开串口。正常连接后切换到配置页面,上位机会自动读取模块参数,也可以点击参数读取按钮手动读取模块的参数,如下图所示。
⚙️ 参数说明
-
设备模式
模块支持 “移动端”、“普通基站”、“RTK基站” 和 “自定义” 四种工作模式,默认配置为 “移动端”,可根据不同应用场景快速切换。
- 移动端:作为 RTK 移动站使用,串口 1 输出 FIX3 协议数据,CAN 接口输出 DroneCAN 协议数据。
- 普通基站:作为普通基准站使用,该基站的坐标为普通定位精度,每次重新上电后基站的坐标可能会有较大的变化。串口 1 输出 RTCM 差分校正数据,可配合数传设备,将差分数据发送至一个或多个移动端,实现 RTK 差分定位。
- RTK 基站:作为 RTK 基站使用,该基站会通过 CORS 获取校正数据,自身达到 RTK 固定解状态,每次重新上电后基站的坐标基本一致。串口 1 输出 RTCM 差分校正数据,可配合数传设备,将差分数据发送至一个或多个移动端,实现 RTK 差分定位。
- 自定义:用户可根据实际需求灵活配置数据输出内容,支持 GGA、ZDA 等 NMEA 0183 标准报文以及 RTCM 差分数据等。更多自定义报文及配置方式,请联系技术支持。
-
基础配置
模块支持对 数据频率、航向偏置、CAN 波特率、串口 1 波特率、串口 2 波特率 等基础参数进行配置。参数修改完成后,点击 “参数保存”,设备重启后生效。
- 数据频率:设置定位数据的输出频率,支持 1 / 2 / 5 / 10 / 20 Hz,可根据飞控、机器人控制器或上位机的数据刷新需求进行选择。
- 航向偏置:用于设置双天线安装方向与设备机体坐标系之间的角度偏差,当 天线未按照标准方向安装时,可通过航向偏置进行角度补偿。
- CAN 波特率:设置 CAN 总线通信波特率,需与飞控或控制器的 CAN 通信参数保持一致,默认设置为 1000 kbps。
- 串口 1 波特率:设置 UART1 的通信波特率,主要用于输出定位、航向等 GNSS 数据,也可接收或输出 RTCM 差分校正数据,需与接收设备的串口波特率保持一致。
- 串口 2 波特率:设置 UART2 的通信波特率,主要用于输出定位、航向等 GNSS 数据,也可接收或输出 RTCM 差分校正数据,需与接收设备的串口波特率保持一致。
-
CORS 配置
模块支持通过 4G 网络接入 CORS / NTRIP 差分服务。启用后,设备可自动连接指定的差分服务器并接收 RTCM 校正数据,实现网络 RTK 高精度定位。
- 使能:开启或关闭 CORS 网络 RTK 功能
- 域名:填写 CORS / NTRIP 服务器的 域名或 IP 地址
- 挂载点:填写差分服务对应的 Mountpoint(挂载点)
- 端口:填写 CORS / NTRIP 服务器的通信端口
- 账号:填写差分服务平台提供的登录账号
- 密码:填写与账号对应的登录密码
说明: 请依次输入服务器地址、端口、挂载点、账号及密码等参数。每项参数输入完成后均需按下回车键确认,界面提示 “写入成功” 表示该项参数已成功写入。全部参数配置完成后,点击 “参数保存”,设备重启后生效。
-
MQTT 配置
模块支持通过 MQTT 协议连接远程服务器,可用于上传定位数据、设备状态等信息,便于接入云平台或用户自建服务器。
- 使能:开启或关闭 MQTT 通信功能
- 域名:填写 MQTT Broker 的 域名或 IP 地址
- 账号:填写 MQTT 服务器的登录账号
- 密码:填写与账号对应的登录密码
- 端口:填写 MQTT Broker 的通信端口
说明: 请依次输入域名、端口、账号及密码等参数。每项参数输入完成后均需按下回车键确认,界面提示 “写入成功” 表示该项参数已成功写入。全部参数配置完成后,点击 “参数保存”,设备重启后生效。
⚠️ 注意
- 对于输入型参数(如 CORS 账号),输入新参数后每次均需按 回车键 写入模块
- 写入成功后将弹出 “写入成功” 提示框
- 所有参数配置完成后,请点击 “参数保存”
- 若未保存,重新断电后参数将恢复为上一次配置
安装配置
-
天线安装
根据飞行器 / 机器人尺寸,尽可能增大天线安装跨度。建议 Master 与 Slave 天线间距 ≥ 30 cm,以获得最佳定向性能。
-
天线固定
安装天线支架后,旋紧螺旋天线,确保牢固可靠。
-
模块固定
使用双面胶将 RTK Ultra 模块固定在机体表面,避免振动或松动,也可以使用 M3 螺栓固定。
-
通信连接
根据控制器需求,通过 CAN 或 UART 与主控系统建立通信。
偏移设置
本模块的定位主要参考 Master 天线,定向结果为 Master 天线指向 Slave 天线与正北方向的夹角。如需设置定位天线的偏移量,请以 Master 天线 的偏移量为准。模块默认采用 NED 坐标系,定义如下(见示意图):
- 北偏(dx):正值,南偏(dx):负值
- 右偏(dy):正值,左偏(dy):负值
- 下偏(dz):正值,上偏(dz):负值
正常情况下将 Master 天线安装在正后方,Slave 天线安装在正前方,模块输出的航向角和机头方向保持一致。如果安装条件受限,需要将两个天线旋转一定角度安装,则按照 Master 天线指向 Slave 天线与机头方向逆时针方向的夹角来设置航向偏移量。下图所示为四种典型的安装方式及相应的航向偏移量。注意,航向偏移量需通过 RTK Manager 调参软件设置,详见参数设置中对默认数据的描述(设置 Offset 的值)。
DroneCAN 配置
本模块的 CAN 接口通过 DroneCAN 协议适配当前主流的开源飞控。下面分别对使用 APM 和 PX4 固件的开源飞控应用该模块的配置做详细说明。
APM 固件参数配置:(上位机软件采用 MissionPlanner,固件版本 4.6 以上)
CAN_P1_DRIVER = 1 (启用 CAN1 驱动)
GPS_AUTO_CONFIG = 0 (关闭 DroneCAN GPS 自动配置,模块出厂已完成配置,建议保持关闭)
GPS1_TYPE = 9 (设置 GPS 类型为 **DroneCAN**)
GPS1_MB_TYPE=1 (设置该参数并保存后,重新刷新参数列表,才会出现 GPS1_MB_OFS)
GPS1_MB_OFS_X=X (主天线相对位置X,主天线在从天线前方为正)
GPS1_MB_OFS_Y=Y (主天线相对位置Y,主天线在从天线右方为正)
GPS1_MB_OFS_Z=Z (主天线相对位置Z,主天线在从天线下方为正)
GPS1_POS_X=X(主天线相对重心的X偏移,NED坐标,前偏移为正,后偏移为负)
GPS1_POS_Y=Y(主天线相对重心的Y偏移,NED坐标,右偏移为正,左偏移为负)
GPS1_POS_Z=Z(主天线相对重心的Z偏移,NED坐标,下偏移为正,上偏移为负)
EK2_SRC1_YAW = 2 / 3
- 2:仅使用 GPS 作为航向源
- 3:优先使用 GPS,罗盘作为备份
EK3_OGN_HGT_MASK = 5 (GPS修正气压计高度)
PX4 固件参数配置:(上位机软件采用 QGC)
UAVCAN_ENABLE="Sensors Automatic Config"
UAVCAN_SUB_GPS="Enable"
UAVCAN_SUB_GPS_R="Enable"
EKF2_GPS_CTRL=15
EKF2_GPS_POS_X(主天线相对重心的X偏移,NED坐标,前偏移为正,后偏移为负)
EKF2_GPS_POS_Y(主天线相对重心的Y偏移,NED坐标,右偏移为正,左偏移为负)
EKF2_GPS_POS_Z(主天线相对重心的Z偏移,NED坐标,下偏移为正,上偏移为负)
EKF2_MAG_TYPE="Automatic" / "None" / "Init"
- "Automatic"(通常情况可选用该模式,磁罗盘会作为备份参与解算)
- "None"(完全不使用磁罗盘)
- "Init"(只用电子罗盘做初始化)
关于EKF2_MAG_TYPE的设置,如果选择"Automatic"或者"Init",在PX4飞控中可能会因为磁罗盘受干扰,
出现航向角间歇性跳变的问题(这大概率是 PX4 飞控算法本身的问题)。如果出现该问题,建议将 EKF2_MAG_TYPE 设置为 "None"。
系统检查
安装及部件检查
- 确认 RTK Ultra 模块 安装正确且牢固。
- 确认 通信线缆与螺旋天线 连接正确、可靠。
工作状态指示检查
- 检查 LED 状态指示灯 是否与当前工作状态对应。
- 确认设定的 螺旋天线间距 以及 主 / 副天线位置 是否正确。
- 在地面站软件中检查 RTK 是否获得固定解(RTK Fixed)。
💡 LED 指示灯速查
| 状态 | 绿灯常亮 | 红灯常亮 |
|---|---|---|
| RTK | RTK 固定解,输出高精度厘米级定位信息 | 非 RTK 固定解,输出常规定位信息 |
| PVT | 卫星接收个数正常,输出常规定位信息 | 未定位,无有效定位信息 |
| ERR | 系统正常 | 系统故障 |
常见问题排查与解决
| 问题描述 | 解决办法 |
|---|---|
| PVT亮红灯 | 此时卫星接收数量不足,检查周围环境是否有严重遮挡。移动新的位置进行测试 |
| 室内是无法接收卫星信息,需要到户外测试 | |
| 螺旋天线线缆是否拧紧,或SMA线缆是否断裂 | |
| RTK亮红灯,始终无法进入固定解 | 检查SMA接头是否拧紧;射频线是否断裂(射频线比较脆弱不能小半径折弯) |
| 可以尝试将slave和master的天线互换,判断是否是射频线损坏 | |
| 检查CORS的账号输入是否正确,每次输入后都要敲一个回车,界面会跳出来【写入成功】 | |
| 螺旋天线的45°的范围内,尽量不要有遮挡,会影响卫星的接收能力 | |
| 物联网卡的每月免费流量是2GB,用完后次月恢复。可提供SN码查询剩余流量 | |
| 检查CORS账号是否到期 | |
| 检查CORS账号是否输入正确 | |
| 检查‘设备模式’是否为 ‘移动端’ | |
| 串口调试助手乱码 | 不要使用TypeC接口,这是上位机的调试口,不能拿来解析Fix3的协议 |
| 需要用【串口1】,可以使用USB转TTL模块接入PC查看输出数据 | |
| 请更换具备数据传输功能的TypeC线后再次测试 | |
| 机器人走动的时候,RTK灯就会灭掉,停下来就恢复 | 检查master接口的SMA线是否过度折弯损坏,接口是否松动? |
| 可将slave和master的天线互换,判断射频线是否损坏 |
ULINK 协议定义
| 区域 | 定义 | 数据类型 | 起始字节 | 字节长度 | 说明 |
|---|---|---|---|---|---|
| 帧头 | head1 | uint8 | 1 | 1 | 帧头1 |
| 帧头 | head2 | uint8 | 2 | 1 | 帧头2 |
| 帧头 | id | uint16 | 3 | 2 | 消息ID |
| 帧头 | addr | uint16 | 5 | 2 | 地址 |
| 帧头 | len | uint16 | 7 | 2 | 数据长度字段:最高位 len[15] 表示是否存在 data 域;len[14:0] 表示 data 域长度。• len[15] = 1:存在 data,实际长度为 len[14:0]• len[15] = 0:无 data,data 长度为 0 |
| 帧头 | check | uint16 | 9 | 2 | 校验值:除 check 字段外所有字节求和,结果取 uint16 |
| 数据域 | data | 参见具体消息定义 | 11 | len[14:0] 指定 |
当 len[15] = 0 时,数据长度为 0 |
FIX3 消息定义
| 区域 | 定义 | 数据类型 | 起始字节 | 字节长度 | 实际数据 | 说明 |
|---|---|---|---|---|---|---|
| 帧头 | head1 | uint8 | 1 | 1 | 0xFA | 帧头 |
| head2 | uint8 | 2 | 1 | 0xAA | 帧头 | |
| id | uint16 | 3 | 2 | 0x0427 | 消息 ID = 1063 | |
| addr | uint16 | 5 | 2 | 0x0000 | 消息地址 0 | |
| len | uint16 | 7 | 2 | 0x803F | 数据域长度为 63 | |
| check | uint16 | 9 | 2 | B9~B10 | 除 check 外所有字节求和,结果取 uint16 |
|
| 数据域 | gps_time | uint64 | 11 | 8 | B11~B18 | UTC 时间,自 1970-01-01 00:00:00 起计,单位 μs(本地时间需加时区偏移) |
| lon | int40 | 19 | 5 | B19~B23 | 经度,保留 8 位小数,实际数据放大10的8次方倍,单位 0.00000001° | |
| lat | int40 | 24 | 5 | B24~B28 | 纬度,保留 8 位小数,实际数据放大10的8次方倍,单位 0.00000001° | |
| alt | int24 | 29 | 3 | B29~B31 | 海拔高度,保留 3 位小数,实际数据放大1000倍,单位 mm | |
| vel_n | int32 | 32 | 4 | B32~B35 | 北向速度,保留 3 位小数,实际数据放大1000倍,单位 mm/s | |
| vel_e | int32 | 36 | 4 | B36~B39 | 东向速度,保留 3 位小数,实际数据放大1000倍,单位 mm/s | |
| vel_d | int32 | 40 | 4 | B40~B43 | 下向速度,保留 3 位小数,实际数据放大1000倍,单位 mm/s | |
| heading | int16 | 44 | 2 | B44~B45 | 双天线航向角,保留 2 位小数,实际数据放大100倍,单位0.01°,角度范围 [-180°, 180°],无效值 32767 | |
| heading_std | uint16 | 46 | 2 | B46~B47 | 双天线航向标准差,保留 2 位小数,实际数据放大100倍,无效值 32767 | |
| pos_n_std | uint24 | 48 | 3 | B48~B50 | 北向位置标准差,保留 3 位小数,实际数据放大1000倍,单位 mm | |
| pos_e_std | uint24 | 51 | 3 | B51~B53 | 东向位置标准差,保留 3 位小数,实际数据放大1000倍,单位 mm | |
| pos_d_std | uint24 | 54 | 3 | B54~B56 | 下向位置标准差,保留 3 位小数,实际数据放大1000倍,单位 mm | |
| vel_n_std | uint24 | 57 | 3 | B57~B59 | 北向速度标准差,保留 3 位小数,实际数据放大1000倍,单位 mm/s | |
| vel_e_std | uint24 | 60 | 3 | B60~B62 | 东向速度标准差,保留 3 位小数,实际数据放大1000倍,单位 mm/s | |
| vel_d_std | uint24 | 63 | 3 | B63~B65 | 下向速度标准差,保留 3 位小数,实际数据放大1000倍,单位 mm/s | |
| pdop | uint16 | 66 | 2 | B66~B67 | 位置精度因子,保留 1 位小数,实际数据放大10倍,单位 0.1 | |
| vdop | uint16 | 68 | 2 | B68~B69 | 速度精度因子,保留 1 位小数,实际数据放大10倍,单位 0.1 | |
| hdop | uint16 | 70 | 2 | B70~B71 | 垂直精度因子,保留 1 位小数,实际数据放大10倍,单位 0.1 | |
| sats_used | uint8 | 72 | 1 | B72 | 使用的卫星数量 | |
| fix_type | uint8 | 73 | 1 | B73 | 定位状态:0 无效,1 单点,2 差分,3 PPP,4 RTK FLOAT,5 RTK FIXED |
解析过程示例(C / C++)
#include <stdint.h>
uint64_t val;
int64_t lon;
/*
* 原始字节(小端):
* 98 DE BE 6B 02
*/
val = ((uint64_t)0x02 << 56)
+ ((uint64_t)0x6B << 48)
+ ((uint64_t)0xBE << 40)
+ ((uint64_t)0xDE << 32)
+ ((uint64_t)0x98 << 24);
/* 右移 24 位,得到 40-bit 有符号整数 */
lon = (int64_t)val >> 24;
说明
- 所有字段均采用 小端模式(Little Endian);
- 解析时应先将字节 按无符号整数方式组合;
- 对于有符号数据,应在组合完成后再转换为 有符号整数;
- 本示例中,经纬度字段为 40-bit 有符号整数,实际物理值需再按协议约定比例缩放。
FIX3 解析代码示例(Python)
import datetime
def read_uint16_le(b, p):
return int.from_bytes(b[p:p + 2], "little", signed=False)
def read_uint24_le(b, p):
return b[p] | (b[p + 1] << 8) | (b[p + 2] << 16)
def read_int24_le(b, p):
v = read_uint24_le(b, p)
return v - (1 << 24) if v & 0x800000 else v
def read_uint40_le(b, p):
return (
b[p]
| (b[p + 1] << 8)
| (b[p + 2] << 16)
| (b[p + 3] << 24)
| (b[p + 4] << 32)
)
def read_int40_le(b, p):
v = read_uint40_le(b, p)
return v - (1 << 40) if (v & (1 << 39)) else v
def checksum16(data: bytes) -> int:
return sum(data) & 0xFFFF
def parse_ulink_frame(frame: bytes):
if len(frame) < 11:
raise ValueError("Frame too short")
if frame[0] != 0xFA or frame[1] != 0xAA:
raise ValueError("Invalid frame header")
msg_id = read_uint16_le(frame, 2)
addr = read_uint16_le(frame, 4)
len_raw = read_uint16_le(frame, 6)
has_data = bool(len_raw & 0x8000)
data_len = (len_raw & 0x7FFF) if has_data else 0
total_len = 11 + data_len
if len(frame) < total_len:
raise ValueError("Frame length mismatch")
check = read_uint16_le(frame, 8)
calc = checksum16(frame[:8] + frame[10:10 + data_len])
check_ok = (check == calc)
data = frame[10:10 + data_len] if has_data else b""
return {
"id": msg_id,
"addr": addr,
"data_len": data_len,
"check_ok": check_ok,
"data": data,
"total_len": total_len,
}
def parse_gnss_fix3(data: bytes):
if len(data) != 63:
raise ValueError("GNSS_FIX3 payload length error")
gps_time_us = int.from_bytes(data[0:8], "little", signed=False)
gps_utc = datetime.datetime(1970, 1, 1) + datetime.timedelta(
microseconds=gps_time_us
)
lon = read_int40_le(data, 8) / 1e8
lat = read_int40_le(data, 13) / 1e8
alt = read_int24_le(data, 18) / 1000.0
vn = int.from_bytes(data[21:25], "little", signed=True) / 1000.0
ve = int.from_bytes(data[25:29], "little", signed=True) / 1000.0
vd = int.from_bytes(data[29:33], "little", signed=True) / 1000.0
heading_raw = int.from_bytes(data[33:35], "little", signed=True)
heading = None if heading_raw == 32767 else heading_raw / 100.0
heading_acc_raw = int.from_bytes(data[35:37], "little", signed=False)
heading_acc = None if heading_acc_raw == 32767 else heading_acc_raw / 100.0
posN = read_uint24_le(data, 37) / 1000.0
posE = read_uint24_le(data, 40) / 1000.0
posD = read_uint24_le(data, 43) / 1000.0
vNacc = read_uint24_le(data, 46) / 1000.0
vEacc = read_uint24_le(data, 49) / 1000.0
vDacc = read_uint24_le(data, 52) / 1000.0
pdop = int.from_bytes(data[55:57], "little") / 10.0
vdop = int.from_bytes(data[57:59], "little") / 10.0
hdop = int.from_bytes(data[59:61], "little") / 10.0
sats = data[61]
fix = data[62]
return {
"time": gps_utc,
"lat": lat,
"lon": lon,
"alt": alt,
"vn": vn,
"ve": ve,
"vd": vd,
"heading": heading,
"heading_acc": heading_acc,
"pdop": pdop,
"vdop": vdop,
"hdop": hdop,
"sats": sats,
"fix_type": fix,
}
def scan_frames(stream: bytes):
frames = []
i = 0
n = len(stream)
while i + 11 <= n:
if stream[i] == 0xFA and stream[i + 1] == 0xAA:
try:
fr = parse_ulink_frame(stream[i:])
frames.append(fr)
i += fr["total_len"]
continue
except Exception:
i += 1
else:
i += 1
return frames
if __name__ == "__main__":
with open("Serial Debug.txt", "r", encoding="utf-8") as f:
hex_text = f.read().strip()
raw = bytes(int(x, 16) for x in hex_text.split())
frames = scan_frames(raw)
for fr in frames:
if fr["id"] == 0x0427 and fr["data_len"] == 63 and fr["check_ok"]:
fix = parse_gnss_fix3(fr["data"])
print(
f"time={fix['time']} "
f"lat={fix['lat']:.8f} lon={fix['lon']:.8f} "
f"alt={fix['alt']:.3f}m "
f"sats={fix['sats']} fix={fix['fix_type']}"
)