3. GLOBAL_POSITION_INT 消息深度解析

好,咱们进入正题。GLOBAL_POSITION_INT 这条消息,可以说是 MAVLink 里最常用的定位数据载体。我做了这么多年无人机,处理过的定位数据里,十有八九都是从这条消息里扒出来的。你想想看,飞控要告诉地面站「我在哪、飞多快、朝哪飞」,全靠它。

3.1 消息 ID 与结构体

先看它的身份证。GLOBAL_POSITION_INT 的消息 ID 是 33,十六进制就是 0x21。这个 ID 在 MAVLink 协议里是唯一的,解析的时候就是靠它来区分是哪条消息。

它的结构体长这样(我简化了一下,只留核心字段):

// MAVLink 2 标准定义
typedef struct __mavlink_global_position_int_t {
    uint32_t time_boot_ms;    // 时间戳,单位 ms
    int32_t  lat;             // 纬度,单位 1e7 度
    int32_t  lon;             // 经度,单位 1e7 度
    int32_t  alt;             // 海拔高度,单位 mm
    int32_t  relative_alt;    // 相对高度,单位 mm
    int16_t  vx;              // 北向速度,单位 cm/s
    int16_t  vy;              // 东向速度,单位 cm/s
    int16_t  vz;              // 垂向速度,单位 cm/s
    uint16_t hdg;             // 航向角,单位 cdeg(百分之一度)
} mavlink_global_position_int_t;

嗯,这里要注意一个细节。所有字段都是整数,没有浮点数。为什么?因为 MAVLink 设计之初就考虑到了嵌入式设备的计算能力,整数运算比浮点快得多,而且传输时不会丢精度。代价就是——你得自己换算单位。

核心换算公式

  • 纬度/经度:实际值 = 原始值 / 1e7(度)
  • 高度:实际值 = 原始值 / 1000(米)
  • 速度:实际值 = 原始值 / 100(m/s)
  • 航向:实际值 = 原始值 / 100(度)

3.2 字段含义详解

每个字段我都踩过坑,一个一个说。

lat / lon —— 经纬度

这两个字段用 int32_t 存储,单位是 1e7 度。说白了,就是把小数点往后挪了 7 位。比如北京的纬度是 39.9042°,存进去就是 399042000。

我遇到过一个问题:有些飞控在 GPS 信号不好时会输出 0。0 度经度、0 度纬度,那位置就在非洲西海岸的大西洋里。所以解析时一定要做有效性检查,别把飞机画到海里去。

alt —— 海拔高度

单位是毫米。注意,这个高度是相对于 WGS84 椭球体 的,不是海平面。我刚开始做数据可视化时,拿 alt 和地图上的海拔对比,发现总有几十米的偏差。后来才搞清楚,WGS84 椭球面和大地水准面之间有个差值,叫「大地水准面差距」。如果你需要真实海拔,得查表修正。

relative_alt —— 相对高度

这个字段我特别喜欢用。它是相对于 起飞点 的高度,单位也是毫米。做航线规划时,我一般用 relative_alt 而不是 alt,因为起飞点高度是已知的,算起来更直观。

举个例子:飞机在 100 米高度巡航,relative_alt 就是 100000。如果 alt 是 500000(海拔 500 米),说明起飞点海拔是 400 米。

vx / vy / vz —— 三轴速度

这三个字段是 NED 坐标系(北东地)下的速度分量。单位是 cm/s。

  • vx:北向速度,正数朝北
  • vy:东向速度,正数朝东
  • vz:垂向速度,正数朝下

这里有个容易搞混的地方:vz 正数代表下降。我第一次做降落数据分析时,看到 vz 一直是正数,还以为飞控坏了。后来才反应过来,NED 坐标系里「地」是正方向,下降就是正速度。

hdg —— 航向角

单位是百分之一度(cdeg)。0° 是正北,顺时针增加。比如 hdg = 9000,代表航向 90°,也就是正东。

注意,这个航向是 地磁航向,不是真北航向。两者之间有个磁偏角,不同地区差异很大。我在新疆做测试时,磁偏角将近 4°,如果不修正,航线会整体偏掉。

字段 类型 单位 范围 常见坑点
lat int32_t 1e7 度 -90° ~ +90° GPS 失效时输出 0
lon int32_t 1e7 度 -180° ~ +180° 同上
alt int32_t mm 取决于海拔 WGS84 椭球高,非海平面
relative_alt int32_t mm 取决于飞行高度 起飞点需校准
vx int16_t cm/s -327.68 ~ +327.67 m/s NED 坐标系,北向
vy int16_t cm/s 同上 NED 坐标系,东向
vz int16_t cm/s 同上 正数朝下!
hdg uint16_t cdeg 0 ~ 35999 地磁航向,需磁偏角修正

3.3 数据采集与解析实战

理论说完了,咱们动手。我习惯用 Python 做原型验证,因为快。下面这段代码,是我在项目里反复用过的模板。

import struct
import serial

# MAVLink GLOBAL_POSITION_INT 消息 ID
MSG_ID_GLOBAL_POSITION_INT = 33

def parse_global_position_int(payload):
    """
    解析 GLOBAL_POSITION_INT 的 payload
    返回字典,单位已换算为常用单位
    """
    # 结构体:IiiihhhH (uint32, int32, int32, int32, int32, int16, int16, int16, uint16)
    fmt = '<IiiihhhH'
    unpacked = struct.unpack(fmt, payload[:22])  # 22 bytes
    
    data = {
        'time_boot_ms': unpacked[0],
        'lat': unpacked[1] / 1e7,
        'lon': unpacked[2] / 1e7,
        'alt': unpacked[3] / 1000.0,
        'relative_alt': unpacked[4] / 1000.0,
        'vx': unpacked[5] / 100.0,
        'vy': unpacked[6] / 100.0,
        'vz': unpacked[7] / 100.0,
        'hdg': unpacked[8] / 100.0
    }
    return data

# 示例:从串口读取并解析
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
while True:
    # 这里简化了 MAVLink 帧同步逻辑
    # 实际项目中需要处理 STX、长度、CRC 等
    raw = ser.read(28)  # GLOBAL_POSITION_INT 完整帧长度
    if raw[0] == 0xFD:  # MAVLink 2 起始字节
        msg_id = raw[5]
        if msg_id == MSG_ID_GLOBAL_POSITION_INT:
            payload_len = raw[1]
            payload = raw[6:6+payload_len]
            pos = parse_global_position_int(payload)
            print(f"位置: {pos['lat']:.6f}, {pos['lon']:.6f}")
            print(f"高度: {pos['relative_alt']:.1f} m")
            print(f"速度: {pos['vx']:.2f}, {pos['vy']:.2f}, {pos['vz']:.2f} m/s")
            print(f"航向: {pos['hdg']:.1f}°")

实战小技巧

我在做数据采集时,一般会同时记录原始 payload 和解析后的数据。万一解析逻辑有 bug,还能回头重算。另外,串口读取建议用缓冲区 + 状态机的方式,别像我早期那样一字节一字节读,效率太低了。

避坑指南

我曾经在解析时忽略了字节序。MAVLink 默认是小端(Little Endian),但有些飞控固件会配置成大端。如果你发现解析出来的经纬度是几亿度,或者速度是几千 m/s,先检查字节序对不对。

3.4 知识体系结构图

下面这张图,是我梳理的 GLOBAL_POSITION_INT 消息解析全流程。从原始字节流到可视化数据,每一步都离不开。

GLOBAL_POSITION_INT 消息解析流程 GLOBAL_POSITION_INT 消息解析全流程 原始字节流 帧同步(STX + 长度 + CRC) 消息ID == 33 ? 否 → 丢弃 Payload 解析(struct.unpack) 单位换算(/1e7, /1000, /100) 数据输出 / 可视化 关键检查点 • 字节序(大端/小端) • GPS 有效性(lat/lon 非 0) • 高度参考系(WGS84 vs 海平面) 常见输出格式 • CSV 日志(原始 + 换算后) • JSON(实时推送) • KML/GPX(轨迹回放)

这张图把整个流程串起来了。从串口拿到原始字节,到最终输出可读的定位数据,每一步都有坑。我个人建议,在写解析代码之前,先用 Wireshark 或者 MAVLink Inspector 抓一包数据,手动验证一下字节序和字段偏移,能省去后面很多调试时间。

好了,GLOBAL_POSITION_INT 的核心内容就这些。记住那几个换算公式,记住 NED 坐标系里 vz 正数朝下,记住检查 GPS 有效性。做到这三点,解析这条消息就不会出大问题。