GPS 解析与 PC 命令:UBX 协议、小端拼接与命令系统¶
配套源码: usart.c / main.cpp (PCCMD) / stm32f4xx_it.c (中断) / mcu_init.h (结构体) 所属层级: 嵌入式落地篇 · 驱动层 + 衔接层 前置依赖: 00 总览与架构 / 01 中断驱动与数据流 学习目标: 读完后你应能回答: 1. UBX-NAV-PVT 帧从
0xB5开始到校验和结束共多少字节?payload offset 和Rx2_dataindex 的换算关系是什么? 2.Rx2_data[30]对应 payload 第几字节?存的是什么物理量? 3. GPS 速度的velD / -1000.0为什么除以负数? 4. PCCMD 结构体怎么用 4 字节实现"选模式 + 退模式"两种命令? 5. GPS_Delay 是怎么算出来的?为什么减 0.25/0.5/0.75?
本篇为什么重要
这一篇是坐标系争议的终结篇。在 00 总览第七节 中,我们逐行验证了算法层是 ENU。本篇从字节级验证 GPS 驱动层的分量顺序——结论是:GPS 速度和位置的分量顺序与算法层期望完全匹配,没有 bug。
修正声明
00 篇第七节 7.3 节曾认为 GPS 位置数组存在"经纬度互换 bug"。经本篇字节级重新核对 UBX-NAV-PVT 协议后确认:payload offset 24 = lon,offset 28 = lat,代码读取方式正确。00 篇的结论需要修正,本篇 7.4 节给出正确分析。
前置阅读链接
- 坐标系完整结论:00 总览 §七
- 传感器原理:03 传感器原理
- C/C++ 桥接
*(CVect3*)强转:留坑 → 06 C与CPP桥接
一、GPS 数据接收流程¶
1.1 硬件链路¶
| 配置项 | 值 | 源码 |
|---|---|---|
| 波特率 | 115200 | mcu_init.c L268 |
| 数据格式 | 8N1 (8 bit, 无校验, 1 停止位) | mcu_init.c L269-273 |
| 触发方式 | RXNE 中断(每字节) | mcu_init.c L277 |
| GPS 输出 | UBX-NAV-PVT, 4 Hz (250ms/帧) | u-center 配置 |
| 接收缓冲区 | Rx2_data[120] + Rx2_data1[120] | mcu_init.c L6 |
1.2 USART2 中断接收(帧同步)¶
void USART2_IRQHandler(void) // GPS input IRQ, 每字节触发
{
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
{
USART_ClearITPendingBit(USART2, USART_IT_RXNE);
temp = USART_ReceiveData(USART2);
Rx2_data1[Length2] = temp;
Length2++;
// 帧头检测
if((Length2 == 1) && (Rx2_data1[0] != 0xb5)) { Rx2_data1[0]=0; Length2=0; }
if((Length2 == 2) && (Rx2_data1[1] != 0x62)) { Rx2_data1[0]=Rx2_data1[1]=0; Length2=0; }
// 帧长检测
if(Length2 == 100)
{
if(Rx2_data1[2]==0x01 && Rx2_data1[3]==0x07) // Class=0x01, ID=0x07 = NAV-PVT
{
memcpy(Rx2_data, Rx2_data1, 100); // 复制到正式缓冲区
Rx2_complete = 1; // 通知 TIM2 中断
}
Length2 = 0;
}
}
}
帧同步三步走:
| 步骤 | 检查内容 | 失败处理 |
|---|---|---|
| ① 帧头首字节 | Rx2_data1[0] == 0xB5 | 丢弃,Length 归零 |
| ② 帧头次字节 | Rx2_data1[1] == 0x62 | 丢弃,Length 归零 |
| ③ 帧长+类型 | Length == 100 且 [2]=0x01, [3]=0x07 | 复制到 Rx2_data,置 Rx2_complete=1 |
为什么用两个缓冲区?
Rx2_data1 是"接收缓冲区"——ISR 持续往里写字节;Rx2_data 是"解析缓冲区"——只有帧完全接收并通过校验后才 memcpy 过去。这样解析过程不会和接收过程冲突。
1.3 TIM2 中断触发解析¶
Rx2_complete 在 TIM2 中断里被检测(stm32f4xx_it.c L206-212):
if(Rx2_complete == 1)
{
Rx2_complete = 0;
GPS_OK_flag = 1;
GPS_Delay = MCU_ms_cnt*10 - PPS_cnt; // GPS 时间延迟 (100μs 单位)
GPS_PVT_Decode();
}
二、UBX-NAV-PVT 协议结构¶
u-blox 接收器输出的 UBX-NAV-PVT 帧共 100 字节:
偏移 字节 内容
────────────────────────────────────
0 0xB5 同步字 1
1 0x62 同步字 2
2 0x01 Class = NAV
3 0x07 ID = PVT
4-5 len Payload 长度 = 92 (小端 u16)
─────────────────────── Payload (92 字节) ───────────────────────
6 [0] iTOW (U4, ms)
...
30 [24] lon (I4, 1e-7 deg) ← 经度
34 [28] lat (I4, 1e-7 deg) ← 纬度
42 [36] hMSL (I4, mm) ← 海平面高度
...
54 [48] velN (I4, mm/s) ← 北速度
58 [52] velE (I4, mm/s) ← 东速度
62 [56] velD (I4, mm/s) ← 地速度
...
────────────────────────────────────
98 CK_A 校验和 A
99 CK_B 校验和 B
Rx2_data index → payload offset 换算
payload_offset = Rx2_data_index - 6
因为 Rx2_data[0..5] 是同步字(2B) + Class/ID(2B) + 长度(2B) = 帧头 6 字节,payload 从 Rx2_data[6] 开始。
三、GPS_PVT_Decode() 逐字段解析¶
usart.c L7-52 的完整解析:
3.1 小端序 32 位拼接¶
// 例:iTOW (payload offset 0)
GPS_ITOW = (Rx2_data[9]<<24) + (Rx2_data[8]<<16) + (Rx2_data[7]<<8) + Rx2_data[6];
// ↑ ↑ ↑ ↑
// byte3(H) byte2 byte1 byte0(L)
UBX 协议所有多字节整数都是小端序(Little-Endian):低字节在低地址。Rx2_data[6] 是最低字节,Rx2_data[9] 是最高字节。
小端 vs 大端
UBX 用小端:(b3<<24)|(b2<<16)|(b1<<8)|b0,低地址存低位。 MPU9250 用大端:(H<<8)|L,低地址存高位。 两者的拼接方向相反,初学者容易混淆。
3.2 完整字段映射表¶
| UBX 字段 | payload offset | Rx2_data index | 类型 | 标度 | 存储位置 | 物理量 |
|---|---|---|---|---|---|---|
| iTOW | 0 | 6-9 | U4 LE | 原值 (ms) | GPS_ITOW | GPS 时间戳 |
| lon | 24 | 30-33 | I4 LE | ×DEG1/1e7 | GPS_Pos[1] | 经度 (rad) |
| lat | 28 | 34-37 | I4 LE | ×DEG1/1e7 | GPS_Pos[0] | 纬度 (rad) |
| hMSL | 36 | 42-45 | I4 LE | /1000 | GPS_Pos[2] | 海拔高度 (m) |
| hAcc | 40 | 46-49 | U4 LE | /1000 | GPS_hAcc | 水平精度 (m) |
| vAcc | 44 | 50-53 | U4 LE | /1000 | GPS_vAcc | 垂直精度 (m) |
| velN | 48 | 54-57 | I4 LE | /1000 | GPS_Vn[1] | 北速度 (m/s) |
| velE | 52 | 58-61 | I4 LE | /1000 | GPS_Vn[0] | 东速度 (m/s) |
| velD | 56 | 62-65 | I4 LE | /-1000 | GPS_Vn[2] | 天速度 (m/s) |
| gSpeed | 60 | 66-69 | I4 LE | /1000 | GPS_gSpeed | 地速 (m/s) |
| headMot | 64 | 70-73 | U4 LE | /1e5 | GPS_Mot | 航向角 (rad) |
| sAcc | 68 | 74-77 | U4 LE | /1000 | GPS_sAcc | 速度精度 (m/s) |
| headAcc | 72 | 78-81 | U4 LE | /1e5 | GPS_headAcc | 航向精度 (rad) |
| pDOP | 76 | 82-83 | U2 LE | /100 | GPS_pDOP | 精度衰减因子 |
| fixType | 20 | 26 | U1 | &0x07 | GPS_fixType | 定位类型 (0=无,2=2D,3=3D) |
| flags | 21 | 27 | U1 | &0x01 | GPS_flags | 定位标志 |
| numSV | 23 | 29 | U1 | 原值 | GPS_numSV | 可见卫星数 |
3.3 速度解析详解¶
// usart.c L13-18
// velE → GPS_Vn[0] (东速度)
GPS_temp = ((Rx2_data[61]<<24)+(Rx2_data[60]<<16)+(Rx2_data[59]<<8)+(Rx2_data[58]));
gps_Data_value.GPS_Vn[0] = GPS_temp / 1000.0; // mm/s → m/s
// velN → GPS_Vn[1] (北速度)
GPS_temp = ((Rx2_data[57]<<24)+(Rx2_data[56]<<16)+(Rx2_data[55]<<8)+(Rx2_data[54]));
gps_Data_value.GPS_Vn[1] = GPS_temp / 1000.0; // mm/s → m/s
// velD → GPS_Vn[2] (天速度 = -地速度)
GPS_temp = ((Rx2_data[65]<<24)+(Rx2_data[64]<<16)+(Rx2_data[63]<<8)+(Rx2_data[62]));
gps_Data_value.GPS_Vn[2] = GPS_temp / -1000.0; // mm/s → m/s, 取负
velD / -1000.0 为什么除以负数?
UBX 协议的 velD 是地速(Down),正值表示向下。而 PSINS 用的是 ENU(天向为正,Up)。所以要把 velD 取负变成 vU(天速度):
除以 -1000.0 等价于除以 1000.0 再取负,一步到位。
3.4 位置解析详解¶
// usart.c L20-25
// lat → GPS_Pos[0] (纬度)
GPS_temp = ((Rx2_data[37]<<24)+(Rx2_data[36]<<16)+(Rx2_data[35]<<8)+(Rx2_data[34]));
gps_Data_value.GPS_Pos[0] = GPS_temp * DEG1 / 10000000.0; // 1e-7 deg → rad
// lon → GPS_Pos[1] (经度)
GPS_temp = ((Rx2_data[33]<<24)+(Rx2_data[32]<<16)+(Rx2_data[31]<<8)+(Rx2_data[30]));
gps_Data_value.GPS_Pos[1] = GPS_temp * DEG1 / 10000000.0; // 1e-7 deg → rad
// hMSL → GPS_Pos[2] (高度)
GPS_temp = ((Rx2_data[45]<<24)+(Rx2_data[44]<<16)+(Rx2_data[43]<<8)+(Rx2_data[42]));
gps_Data_value.GPS_Pos[2] = GPS_temp / 1000.0; // mm → m
其中 DEG1 = π/180(usart.c L3),把角度转弧度。
注意:这里用的是
hMSL(payload offset 36,海平面高度),不是height(offset 32,椭球高)。两者差一个大地水准面起伏(geoid undulation),在中国约 -20 ~ +60 m。
四、坐标系验证(GPS 解析 vs 算法层期望)¶
4.1 速度分量匹配¶
| GPS_Vn index | 存的值 | CVect3 成员 (强转后) | 算法层期望 | 是否匹配 |
|---|---|---|---|---|
GPS_Vn[0] | velE (东速度) | gvn.i | vE (东) | ✅ |
GPS_Vn[1] | velN (北速度) | gvn.j | vN (北) | ✅ |
GPS_Vn[2] | -velD = vU (天速度) | gvn.k | vU (天) | ✅ |
4.2 位置分量匹配¶
| GPS_Pos index | 存的值 | CVect3 成员 (强转后) | 算法层期望 | 是否匹配 |
|---|---|---|---|---|
GPS_Pos[0] | lat (纬度, rad) | gpos.i | lat (纬度) | ✅ |
GPS_Pos[1] | lon (经度, rad) | gpos.j | lon (经度) | ✅ |
GPS_Pos[2] | hMSL (海拔, m) | gpos.k | h (高度) | ✅ |
结论:GPS 驱动层分量顺序与算法层 ENU 约定完全匹配
GPS_Vn[0,1,2] = [vE, vN, vU] → CVect3(i,j,k) 完全对齐。 GPS_Pos[0,1,2] = [lat, lon, h] → CVect3(i,j,k) 完全对齐。
修正 00 篇第七节 7.3-7.4 节
00 篇曾错误地认为 GPS 位置存在"经纬度互换 bug"。经本篇字节级核对 UBX-NAV-PVT 协议,确认:
- payload offset 24 = lon(经度),代码存入
GPS_Pos[1]→CVect3.j = lon✅ - payload offset 28 = lat(纬度),代码存入
GPS_Pos[0]→CVect3.i = lat✅ - payload offset 48 = velN(北速度),代码存入
GPS_Vn[1]→CVect3.j = vN✅ - payload offset 52 = velE(东速度),代码存入
GPS_Vn[0]→CVect3.i = vE✅
速度和位置的分量顺序均正确,不存在"经纬度互换" bug。 00 篇的 7.3-7.5 节相关结论应以此为准修正。
4.3 航迹角初始化验证¶
代入已核实的 gvn.(i,j) = (vE, vN):
正北 (vE=0, vN>0) → yaw=0;正东 (vE>0, vN=0) → yaw=−π/2。与 vn2att 公式(PSINS.cpp L695)一致 ✅。
五、PC 命令系统:PCCMD 结构体¶
5.1 结构体定义¶
PC_cmd 是 32 字节的全局数组(mcu_init.c L24),通过 USART1 中断接收填充。
pcmd 是一个指针强转——把 PC_cmd 数组的首地址解释为 PCCMD*,即把前 4 字节当作两个 u16:
PC_cmd[0..1] → pcmd->cmd1 (前 2 字节, 小端 u16)
PC_cmd[2..3] → pcmd->cmd2 (后 2 字节, 小端 u16)
PC_cmd[4..31] → 未使用
语法详解(留坑 → 06 C与CPP桥接)
*(PCCMD*)&PC_cmd 的含义:取 PC_cmd 数组首地址 &PC_cmd,强转为 PCCMD* 类型指针,然后通过 pcmd->cmd1/cmd2 访问。本质是同一块内存的不同类型解释,C/C++ 中最常用的"零拷贝"桥接手法。
5.2 命令体系¶
| 命令 | 写入方式 | 触发动作 | 帧头 |
|---|---|---|---|
cmd2 = 0x1111 | PC 发 4 字节 0x11 0x11 0x00 0x00 | 进入 Mahony 模式 | outFrame.head = 0x56aa55aa |
cmd2 = 0x2222 | PC 发 4 字节 0x22 0x22 0x00 0x00 | 进入 SINSGPS 静态模式 | outFrame.head = 0x57aa55aa |
cmd2 = 0x3333 | PC 发 4 字节 0x33 0x33 0x00 0x00 | 进入 SINSGPS 动态模式 | outFrame.head = 0x58aa55aa |
cmd2 = 0x4444 | PC 发 4 字节 0x44 0x44 0x00 0x00 | 进入 GPS 配置透传模式 | — |
cmd1 = 0xa5a5 | PC 发 2 字节 0xa5 0xa5 | 退出当前模式,回 switch | — |
5.3 主循环 dispatch¶
int main(void)
{
mcu_init(); // 初始化所有硬件
pcmd->cmd1 = 0; pcmd->cmd2 = 0x1111; // 默认进入 Mahony 模式
while(1) {
switch(pcmd->cmd2) {
case 0x1111: pcmd->cmd2=0; outFrame.head=0x56aa55aa; Mahony_main(); break;
case 0x2222: pcmd->cmd2=0; outFrame.head=0x57aa55aa; SINSGPS_static_main(); break;
case 0x3333: pcmd->cmd2=0; outFrame.head=0x58aa55aa; SINSGPS_moving_main(); break;
case 0x4444: pcmd->cmd2=0; GPSCFG_main(); break;
}
}
}
执行流程: 1. mcu_init() 后中断已在后台运行 2. 默认 cmd2 = 0x1111 → 直接进入 Mahony 模式 3. 各模式内部 while(1) 循环,每轮检查 cmd1 == 0xa5a5 是否要求退出 4. 退出后回到外层 switch(pcmd->cmd2),等 PC 发新命令
cmd2 清零的含义
进入模式后 pcmd->cmd2 = 0 立即清零,防止退出本模式后重复进入。下次进入需要 PC 重新发命令。
5.4 GPS 配置透传模式(GPSCFG)¶
void GPSCFG_main(void)
{
USART1_Configuration(1); // 切换 USART1 到 115200 (GPS 配置波特率)
while(1) {
if(pcmd->cmd1 == 0xa5a5) { pcmd->cmd1=0; break; }
}
}
此模式下: - USART1 波特率从 460800 切到 115200 - USART2(GPS)收到的数据透传到 USART1(PC)——在 stm32f4xx_it.c L256-264 的 USART2 ISR 中实现 - PC 通过 USART1 发的命令也透传到 USART2(GPS)——在 stm32f4xx_it.c L304 的 USART1 ISR 中实现
用途:用 u-blox 的 u-center 软件配置 GPS 模块(改输出频率、改协议、改波特率等)。
六、GPS 延迟补偿¶
6.1 PPS 时间戳¶
void EXTI0_IRQHandler(void) // PPS 下降沿中断, 1 Hz
{
PPS_cnt = MCU_ms_cnt * 10 + TIM2->CNT; // PPS 时刻 (100μs 单位)
}
PPS_cnt 精确记录了 GPS 秒脉冲到达时刻,单位 100 μs。
6.2 延迟计算¶
MCU_ms_cnt 是当前系统时间(ms),乘 10 得 100μs 单位,减去 PPS 时刻就是 GPS 数据从 PPS 到被处理的延迟。
6.3 输出层延迟修正¶
outFrame.GPS_delay = GPS_Delay / 10000.0; // 100μs → s
if(outFrame.GPS_delay < 1.0)
{
if(outFrame.GPS_delay > 0.75) outFrame.GPS_delay -= 0.75;
else if(outFrame.GPS_delay > 0.5) outFrame.GPS_delay -= 0.5;
else if(outFrame.GPS_delay > 0.25) outFrame.GPS_delay -= 0.25;
}
为什么减 0.25 / 0.5 / 0.75?
GPS 以 4 Hz 输出(每 250 ms 一帧),GPS_Delay 通常在 0 ~ 250 ms 范围内。但有时候接收到的帧属于上一个 250 ms 周期,需要减去一个或多个 250 ms:
| 原始延迟范围 | 减去 | 结果 | 含义 |
|---|---|---|---|
| 0 ~ 0.25 s | 0 | 0 ~ 0.25 s | 当前周期帧 |
| 0.25 ~ 0.5 s | 0.25 | 0 ~ 0.25 s | 上一周期帧 |
| 0.5 ~ 0.75 s | 0.5 | 0 ~ 0.25 s | 上上周期帧 |
| 0.75 ~ 1.0 s | 0.75 | 0 ~ 0.25 s | 上上上周期帧 |
这是一个粗糙的"GPS 帧龄"估计,用于
kf.posGNSSdelay/kf.vnGNSSdelay(main.cpp L112),让 KF 知道 GPS 数据有多"旧"。
七、航向角处理¶
// usart.c L27-33
GPS_temp = ((Rx2_data[73]<<24)+(Rx2_data[72]<<16)+(Rx2_data[71]<<8)+(Rx2_data[70]));
gps_Data_value.GPS_Mot = GPS_temp / 100000.0; // 1e-5 deg → deg
if(gps_Data_value.GPS_Mot > 180.0)
gps_Data_value.GPS_Mot -= 360.0; // 180~360 → -180~0
gps_Data_value.GPS_Mot *= DEG1; // deg → rad
UBX 的 headMot 是 0~360° 的地理航向(正北=0,顺时针)。代码把 180~360° 映射到 -180~0°,然后转弧度。但这个值在 main.cpp 中没有被使用——SINS 动态模式的 yaw 初始化用的是 atan2(-gvn.i, gvn.j) 而不是 GPS 航向角。
八、H743 移植要点¶
8.1 需要改的¶
| 项目 | F4 原工程 | H743 移植 | 说明 |
|---|---|---|---|
| USART 外设 | USART2 (PA2/PA3) | 可沿用或换 LPUART | H743 有更多 USART 资源 |
| DMA 透传 | 无(字节级中断) | 可改 DMA 接收 | 释放中断负载 |
| 校验和检查 | 未实现(仅检查 Class/ID) | 建议加 CRC 校验 | 防止偶发误码导致位置跳变 |
原工程未做 UBX 校验和验证
UBX 帧末尾有 CK_A/CK_B 两个校验字节,但代码只检查了 Class=0x01, ID=0x07,没有校验 checksum。如果 GPS 数据有误码,可能会解析出错误的位置/速度。H743 移植时建议补上校验。
8.2 不需要改的¶
| 项目 | 说明 |
|---|---|
| UBX-NAV-PVT 协议 | u-blox 固有协议,与 MCU 无关 |
| 小端拼接逻辑 | 与 GPS 模块约定,不变 |
| 标度转换公式 | 1e-7 deg, mm/s 等不变 |
| GPS 延迟补偿逻辑 | PPS + MCU_ms_cnt 机制不变 |
| PCCMD 命令体系 | 纯软件协议,不变 |
自测题
Rx2_data[30]对应 payload 第几字节?存的是什么物理量?- GPS 速度解析中,为什么
velD / -1000.0要除以负数? - PCCMD 结构体只有 4 字节(两个 u16),但 PC_cmd 数组有 32 字节。多出来的字节有用吗?
- GPS_Delay 的单位是什么?为什么在输出时要减 0.25/0.5/0.75?
- GPSCFG 模式下 USART1 波特率从 460800 切到 115200,为什么?
参考答案
- payload offset = 30 - 6 = 24,对应 UBX-NAV-PVT 的
lon(经度),I4 小端,1e-7 deg。 - velD 是地速度(Down,向下为正),而 PSINS 用 ENU(Up 为正)。除以 -1000 等于取负再除以 1000,把 vD 转成 vU。
- 没有直接用途。PCCMD 只用了前 4 字节,剩余 28 字节是预留的缓冲空间。USART1 ISR 用
ii计数器循环填充 32 字节,但只通过 pcmd 指针访问前 4 字节。 - 原始单位是 100μs(因为
MCU_ms_cnt*10和PPS_cnt都是 100μs 单位)。减 0.25/0.5/0.75 是因为 GPS 4Hz 输出(250ms 周期),如果收到的帧延迟超过 250ms,说明是上一个周期的旧数据,需要减去整数个周期。 - 因为 u-blox GPS 模块默认配置波特率是 9600 或 115200,而正常运行时的 USART1 输出波特率是 460800。GPSCFG 模式需要和 u-center 软件通信,所以切回 115200。
参考资料¶
- u-blox UBX-NAV-PVT 接口说明 — UBX-NAV-PVT 报文 payload 偏移、lon/lat/velN/velE/velD 字段定义
- u-blox NEO-M8 接口手册 — u-blox M8 系列 GNSS 模块通信协议
- 严恭敏教授 CSDN 博客 — PSINS 工程中 GPS 数据字节序处理与坐标系约定
- STM32F4xx 参考手册 RM0090 — USART 章节 — USART2 接收中断、DMA 配置参考
- PSINS 官网(严恭敏教授) — GPS_Pos=[lat,lon,h] / GPS_Vn=[vE,vN,vU] 约定来源
参考体系:00 总览与架构 / 01 中断驱动与数据流 / 配套源码 usart.c / main.cpp / stm32f4xx_it.c