最近有项目需要用高精度定位模组,原来用的普通北斗定位模组换成了高精度RTK模组,也是第一次使用,现在把调试过程记录下。
一、RTK工作原理
RTK工作原理:
传统RTK技术与网络RTK技术:
网络RTK的特点:
二、RTK与NTRIP
三、RTCM3协议
字段 | 比特 | 说明 |
Preamble 前导码 | 8bit | 固定 0xD3,帧起始标志 |
Reserved 保留位 | 6bit | 默认填 0 |
Message Length 消息长度 | 10bit | 载荷 Data 字段字节数,最大 1023 字节 |
Data 数据载荷 | N×8bit | 变长,内部第一个 12bit 是消息类型号 |
CRC24Q 校验 | 24bit(3 字节) | 校验范围:前导码 + 保留 + 长度 + Data 载荷,不包含自身 CRC3 字节 |
整帧总字节 =3 字节头 + N 字节载荷 +3 字节 CRC总帧最小 6 字节,最大:3+1023+3 =1029 字节
[0xD3][Resv+Len(2B)][Data(N字节)][CRC24Q(3B)]
Data 载荷内部开头
Data 最开始12bit = Message Type 消息号,例如 1005、1074、1124 等。
⚠️TCP 是字节流,会粘包、拆包;不能按固定长度读取,必须写状态机循环扫描 0xD3 同步头,取出完整帧,做 CRC24Q 校验,校验成功才解析,失败丢弃。
计算法:
uint32_t rtcm_crc24q(const uint8_t *data, size_t len) { const uint32_t POLY = 0x1864CFB; // CRC-24Q 多项式 uint32_t crc = 0; size_t i, j; for (i = 0; i < len; i++) { crc ^= ((uint32_t)data[i]) << 16; // 字节与CRC高8位异或 for (j = 0; j < 8; j++) { crc <<= 1; // 左移一位 if (crc & 0x01000000) { // 检查最高位(第24位) crc ^= POLY; // 异或多项式 } } } return crc & 0x00FFFFFF; // 只保留低24位 }查表法:
const uint32_t wCrc24qTable[] = { 0x00000000,0x00864CFB,0x008AD50D,0x000C99F6, 0x0093E6E1,0x0015AA1A,0x001933EC,0x009F7F17, 0x00A18139,0x0027CDC2,0x002B5434,0x00AD18CF, 0x003267D8,0x00B42B23,0x00B8B2D5,0x003EFE2E, 0x00C54E89,0x00430272,0x004F9B84,0x00C9D77F, 0x0056A868,0x00D0E493,0x00DC7D65,0x005A319E, 0x0064CFB0,0x00E2834B,0x00EE1ABD,0x00685646, 0x00F72951,0x007165AA,0x007DFC5C,0x00FBB0A7, 0x000CD1E9,0x008A9D12,0x008604E4,0x0000481F, 0x009F3708,0x00197BF3,0x0015E205,0x0093AEFE, 0x00AD50D0,0x002B1C2B,0x002785DD,0x00A1C926, 0x003EB631,0x00B8FACA,0x00B4633C,0x00322FC7, 0x00C99F60,0x004FD39B,0x00434A6D,0x00C50696, 0x005A7981,0x00DC357A,0x00D0AC8C,0x0056E077, 0x00681E59,0x00EE52A2,0x00E2CB54,0x006487AF, 0x00FBF8B8,0x007DB443,0x00712DB5,0x00F7614E, 0x0019A3D2,0x009FEF29,0x009376DF,0x00153A24, 0x008A4533,0x000C09C8,0x0000903E,0x0086DCC5, 0x00B822EB,0x003E6E10,0x0032F7E6,0x00B4BB1D, 0x002BC40A,0x00AD88F1,0x00A11107,0x00275DFC, 0x00DCED5B,0x005AA1A0,0x00563856,0x00D074AD, 0x004F0BBA,0x00C94741,0x00C5DEB7,0x0043924C, 0x007D6C62,0x00FB2099,0x00F7B96F,0x0071F594, 0x00EE8A83,0x0068C678,0x00645F8E,0x00E21375, 0x0015723B,0x00933EC0,0x009FA736,0x0019EBCD, 0x008694DA,0x0000D821,0x000C41D7,0x008A0D2C, 0x00B4F302,0x0032BFF9,0x003E260F,0x00B86AF4, 0x002715E3,0x00A15918,0x00ADC0EE,0x002B8C15, 0x00D03CB2,0x00567049,0x005AE9BF,0x00DCA544, 0x0043DA53,0x00C596A8,0x00C90F5E,0x004F43A5, 0x0071BD8B,0x00F7F170,0x00FB6886,0x007D247D, 0x00E25B6A,0x00641791,0x00688E67,0x00EEC29C, 0x003347A4,0x00B50B5F,0x00B992A9,0x003FDE52, 0x00A0A145,0x0026EDBE,0x002A7448,0x00AC38B3, 0x0092C69D,0x00148A66,0x00181390,0x009E5F6B, 0x0001207C,0x00876C87,0x008BF571,0x000DB98A, 0x00F6092D,0x007045D6,0x007CDC20,0x00FA90DB, 0x0065EFCC,0x00E3A337,0x00EF3AC1,0x0069763A, 0x00578814,0x00D1C4EF,0x00DD5D19,0x005B11E2, 0x00C46EF5,0x0042220E,0x004EBBF8,0x00C8F703, 0x003F964D,0x00B9DAB6,0x00B54340,0x00330FBB, 0x00AC70AC,0x002A3C57,0x0026A5A1,0x00A0E95A, 0x009E1774,0x00185B8F,0x0014C279,0x00928E82, 0x000DF195,0x008BBD6E,0x00872498,0x00016863, 0x00FAD8C4,0x007C943F,0x00700DC9,0x00F64132, 0x00693E25,0x00EF72DE,0x00E3EB28,0x0065A7D3, 0x005B59FD,0x00DD1506,0x00D18CF0,0x0057C00B, 0x00C8BF1C,0x004EF3E7,0x00426A11,0x00C426EA, 0x002AE476,0x00ACA88D,0x00A0317B,0x00267D80, 0x00B90297,0x003F4E6C,0x0033D79A,0x00B59B61, 0x008B654F,0x000D29B4,0x0001B042,0x0087FCB9, 0x001883AE,0x009ECF55,0x009256A3,0x00141A58, 0x00EFAAFF,0x0069E604,0x00657FF2,0x00E33309, 0x007C4C1E,0x00FA00E5,0x00F69913,0x0070D5E8, 0x004E2BC6,0x00C8673D,0x00C4FECB,0x0042B230, 0x00DDCD27,0x005B81DC,0x0057182A,0x00D154D1, 0x0026359F,0x00A07964,0x00ACE092,0x002AAC69, 0x00B5D37E,0x00339F85,0x003F0673,0x00B94A88, 0x0087B4A6,0x0001F85D,0x000D61AB,0x008B2D50, 0x00145247,0x00921EBC,0x009E874A,0x0018CBB1, 0x00E37B16,0x006537ED,0x0069AE1B,0x00EFE2E0, 0x00709DF7,0x00F6D10C,0x00FA48FA,0x007C0401, 0x0042FA2F,0x00C4B6D4,0x00C82F22,0x004E63D9, 0x00D11CCE,0x00575035,0x005BC9C3,0x00DD8538, }; /** * 查表法核心实现 * * 算法原理: * crc = (crc << 8) ^ table[((crc >> 16) ^ data[i]) & 0xFF] * * 其中: * - (crc >> 16): 取出CRC高8位 * - ^ data[i]: 与当前字节异或 * - & 0xFF: 取低8位作为表索引 * - table[index]: 预计算好的CRC增量 * - crc << 8: CRC左移8位 * - ^ table[index]: 异或增量 */ uint32_t rtcm_crc24q_table(const uint8_t *data, size_t len) { // 确保表已初始化 // if (!table_initialized) { // rtcm_crc24q_init_table(); // } uint32_t crc = 0; for (size_t i = 0; i < len; i++) { uint8_t index = (uint8_t)(((crc >> 16) ^ data[i]) & 0xFF); crc = (crc << 8) ^ wCrc24qTable[index]; crc &= 0x00FFFFFF; // 保持24位 } return crc; }四、嵌入式开发流程
RTK服务器可以选用千寻位置、中移定位、六分科技等,需要花钱购买账号。这里以千寻位置为例,其他类似。
IP网址和端口号:rtk.ntrip.qxwz.com:8003,这里选择8003
挂载点有两个:AUTO(五星十六频);RTCM32_GGB(三星八频),这里选择AUTO
千寻位置账号使用有两种方式:SDK和差分账号。SDK需要提供编译信息向官方申请,这里选用差分账号。
HTTP链接登入格式:
GET /{MountPoint} HTTP/1.0
User-Agent: NTRIP ClientName/Version
Authorization: Basic {Base64EncodedCredentials}
Ntrip-Version: Ntrip/2.0
Connection: close
Ntrip-GGA: {GPGGA_Sentence} // 可选,用于发送移动端位置
字段 说明
MountPoint 挂载点名称(如RTCM32_GGB),标识特定数据流
User-Agent 客户端标识(需符合NTRIP规范,如NTRIP RTKLIB/1.0)
Authorization Basic认证(格式:Basic base64(user:password))
Ntrip-Version 协议版本声明(v2.0必须包含此头)
Ntrip-GGA 可选的NMEA GGA语句,用于向服务器报告客户端位置
认证格式为user:password,然后用 base64格式化:
static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // **Base64 编码实现** uint16_t base64_encode(unsigned char* pchOutput, uint16_t hwOutputBufferSize,const unsigned char* c_pchInput, uint16_t hwInputLen) { uint16_t output_len = 4 * ((hwInputLen + 2) / 3); // 计算输出长度 //char* output = (char*)malloc(output_len + 1); // 为输出分配内存 if(NULL == pchOutput || !hwOutputBufferSize || NULL == c_pchInput || !hwInputLen){ return 0; } if(output_len+1 > hwOutputBufferSize){ return 0; } //if (!pchOutput) return 0; // 内存分配失败 uint16_t i, j; for (i = 0, j = 0; i < hwInputLen;) { uint32_t octet_a = i < hwInputLen ? c_pchInput[i++] : 0; uint32_t octet_b = i < hwInputLen ? c_pchInput[i++] : 0; uint32_t octet_c = i < hwInputLen ? c_pchInput[i++] : 0; uint32_t combined = (octet_a << 16) | (octet_b << 8) | octet_c; pchOutput[j++] = base64_table[(combined >> 18) & 0x3F]; pchOutput[j++] = base64_table[(combined >> 12) & 0x3F]; pchOutput[j++] = i > hwInputLen + 1 ? '=' : base64_table[(combined >> 6) & 0x3F]; pchOutput[j++] = i > hwInputLen ? '=' : base64_table[combined & 0x3F]; } pchOutput[output_len] = '\0'; // 添加 NULL 终止符 return output_len; } // **Base64 解码实现** uint16_t base64_decode(unsigned char* pchOutput,uint16_t hwOutputBufferSize,const char* c_pcInput) { uint16_t hwOutputLen = 0; uint16_t hwInputLen = 0; if(NULL == pchOutput || !hwOutputBufferSize || NULL == c_pcInput){ return 0; } hwInputLen = strlen(c_pcInput); if (hwInputLen % 4 != 0) return 0; // 输入长度必须是4的倍数 hwOutputLen = hwInputLen / 4 * 3; if (c_pcInput[hwInputLen - 1] == '=') (hwOutputLen)--; if (c_pcInput[hwInputLen - 2] == '=') (hwOutputLen)--; if(hwOutputLen +1 > hwOutputBufferSize){ return 0; } //unsigned char* output = (unsigned char*)malloc(*output_len + 1); // 分配额外空间以存储 NULL 终止符 //if (!pchOutput) return 0; size_t i, j; for (i = 0, j = 0; i < hwInputLen;) { int sextet_a = c_pcInput[i] == '=' ? 0 : strchr(base64_table, c_pcInput[i++]) - base64_table; int sextet_b = c_pcInput[i] == '=' ? 0 : strchr(base64_table, c_pcInput[i++]) - base64_table; int sextet_c = c_pcInput[i] == '=' ? 0 : strchr(base64_table, c_pcInput[i++]) - base64_table; int sextet_d = c_pcInput[i] == '=' ? 0 : strchr(base64_table, c_pcInput[i++]) - base64_table; uint32_t combined = (sextet_a << 18) | (sextet_b << 12) | (sextet_c << 6) | sextet_d; if (j < hwOutputLen) pchOutput[j++] = (combined >> 16) & 0xFF; if (j < hwOutputLen) pchOutput[j++] = (combined >> 8) & 0xFF; if (j < hwOutputLen) pchOutput[j++] = combined & 0xFF; } pchOutput[hwOutputLen] = '\0'; // 添加 NULL 终止符 return hwOutputLen; }登入流程如下:
1、获取RTK模组的GGA信息:如
$GNGGA,071516.000,3149.301159,N,11706.919870,E,1,16,0.95,70.2,M,-0.3,M,,*592、设备登入RTK服务器
(1)TCP登入:rtk.ntrip.qxwz.com:8003
(2)发送HTTP登入帧:
返回:ICY 200 OK,说明登入成功。
(注意:发送帧后面要跟两个回车换行符)
(3)设备直接发送GGA信息给RTK服务器,得到差分数据,转发给RTK模组即可。
$GNGGA,071516.000,3149.301159,N,11706.919870,E,1,16,0.95,70.2,M,-0.3,M,,*59\r\n\r\n(注意:发送帧后面要跟两个回车换行符)
(4)等到二进制RTCM3数据。
(5)RTK模组成功后:
2026.08.25
今天测试RTK模组,用官方提供的软件可以成功,用自己搭建的测试环境,始终不行。分析原因,是自己搭建的环境只是把RTK服务器的一帧数据返回给了模组,应该是把模组没秒的GGA数据给RTK服务器,然后把服务器每秒返回的数据给模组,让模组不停地修正,这样才能进入RTK模组。