在智能设备、自动驾驶、无人机等领域,组合导航定位技术是实现精准位置感知的核心技术之一。它通过融合 GNSS(全球导航卫星系统)、IMU(惯性测量单元)等多传感器数据,实现高精度、高可靠性的定位。从本期开始,我们将通过一系列实战教程,逐步讲解组合导航定位系统的开发过程。本文作为开篇,将聚焦于 GNSS 模块的原始数据读取,特别是 NMEA 协议中GNGGA 语句的解析方法。
一、相关知识
1.1 NMEA 0183 协议简介
NMEA 0183 是全球通用的海洋电子设备数据传输标准,目前广泛应用于 GNSS 接收机、航海仪器等设备。该协议采用 ASCII 码格式,以文本行形式传输数据,每条语句以$开头,以*后跟两位十六进制校验和结尾,结构清晰易解析。
1.2 GNGGA 语句格式解析
GNGGA(Global Navigation Satellite System Fix Data)是 NMEA 协议中最重要的定位数据语句之一,包含了实时定位的关键信息。其标准格式如下:
$GNGGA,<UTC 时间>,<纬度>,<纬度方向(N/S)>,<经度>,<经度方向>,<定位状态>,<参与定位的卫星数>,<8>,<海拔高度>
1.3 IMU 基础概念
IMU(Inertial Measurement Unit)即惯性测量单元,通常包含三轴加速度计和三轴陀螺仪,部分还集成了磁力计。在组合导航系统中,IMU 能够提供高频的相对运动数据,弥补 GNSS 更新率低、动态响应慢的缺点。这里选用 MPU6050 作为 IMU 模块示例,它是一款低成本、高性能的 6 轴传感器,集成了 3 轴加速度计和 3 轴陀螺仪,通过 I2C 接口与控制器通信。
二、基于 Python 的串口数据读取与解析
2.1 GNGGA 解析函数
关键逻辑说明:
- 度分转换公式:十进制度数 = 度 + 分 / 60,例如4807.038N表示 48 度 7.038 分,转换为 48 + 7.038/60 = 48.1173 度(北纬为正)
- 定位状态判断:仅当fix_status=1时(单点定位),数据有效(0 表示无定位,2 表示差分定位,4表示固定解,5表示浮点解)未接入RTK时为单点定位(网络RTK校正后续会讲)
- 异常处理:字段缺失或格式错误时返回None,避免程序崩溃
def parse_gngga(nmea_sentence):
try:
parts = nmea_sentence.split(',')
if len(parts) < 15: # 确保字段完整性
return None
# 时间格式转换:HHMMSS -> HH:MM:SS
utc_time = parts[1][:6] # 截取前6位(忽略毫秒)
time_str = f"{utc_time[:2]}:{utc_time[2:4]}:{utc_time[4:6]}" if utc_time else "N/A"
# 纬度转换:ddmm.mmmm -> 十进制度数(±表示南北)
latitude = parts[2]
lat_dir = parts[3]
lat_deg = float(latitude[:2]) # 提取度部分
lat_min = float(latitude[2:]) # 提取分部分
lat_decimal = lat_deg + lat_min / 60.0 # 度分转换为十进制
lat_decimal = -lat_decimal if lat_dir == 'S' else lat_decimal # 南半球取负值
# 经度转换:原理同纬度,注意经度为三位数(如011表示11度)
longitude = parts[4]
lon_dir = parts[5]
lon_deg = float(longitude[:3])
lon_min = float(longitude[3:])
lon_decimal = lon_deg + lon_min / 60.0
lon_decimal = -lon_decimal if lon_dir == 'W' else lon_decimal # 西半球取负值
return {
'time': time_str,
'latitude': lat_decimal,
'longitude': lon_decimal,
'fix_status': '有效' if parts[6] == '1' else '无效', # 1=GPS fix有效
'satellites': int(parts[7]), # 参与定位的卫星数
'altitude': float(parts[9]) # 海拔高度(单位:米,字段9)
}
except (ValueError, IndexError) as e:
print(f"解析错误: {e}")
return None
2.2 串口通信与数据读取
- 端口设置:Windows 系统端口为COM3等,Linux 为/dev/ttyUSB0,需通过设备管理器 / 系统日志确认
- 波特率匹配:GNSS 模块默认波特率通常为 9600(可通过 AT 指令修改)
- 数据过滤:通过startswith('$GNGGA')确保仅处理目标语句(GNGGA较常用,也可选其他语句),避免解析其他类型数据(如 GPRMC、GNGSA)
import serial
# 串口配置
ser = serial.Serial(
port='/dev/ttyS0', # Linux串口示例(如USB转TTL可能为/dev/ttyUSB0)
baudrate=9600, # 常见波特率:4800/9600/115200
timeout=1 # 读取超时时间(秒)
)
try:
while True:
line = ser.readline().decode('ascii', errors='ignore').strip()
if line.startswith('$GNGGA'): # 仅处理GNGGA语句
print(f"原始数据: {line}")
data = parse_gngga(line)
if data:
# 格式化输出关键信息
print(f"时间:{data['time']}")
print(f"纬度:{data['latitude']:.8f}°") # 保留8位小数
print(f"经度:{data['longitude']:.8f}°")
print(f"定位状态:{data['fix_status']}")
print(f"卫星数:{data['satellites']}")
print(f"海拔:{data['altitude']}米\n")
except KeyboardInterrupt:
print("程序终止(用户中断)")
except Exception as e:
print(f"发生错误: {e}")
finally:
ser.close() # 确保串口关闭
2.3 校验和验证(扩展优化)
NMEA 语句的校验和位于*之后,用于验证数据完整性。可通过以下代码实现校验和验证(可根据需求添加):
def check_checksum(sentence):
# 提取数据部分(去除$和*后的内容)
data_part = sentence[1:sentence.index('*')]
checksum = sentence[sentence.index('*')+1:sentence.index('*')+3]
# 计算异或校验和
xor = 0
for c in data_part:
xor ^= ord(c)
return checksum.upper() == f"{xor:02X}"
# 在解析前调用:
if check_checksum(line):
data = parse_gngga(line)
else:
print("校验和错误,忽略本条数据")
2.4 MPU6050 数据读取与解析
MPU6050 通过 I2C 协议通信,默认地址为 0x68,使用 smbus2 库实现 I2C 读写:
- _write_byte:向指定寄存器写入数据
- _read_bytes:从指定寄存器读取多个字节数据
- read_raw_data ():从数据寄存器 (0x3B) 开始读取 14 字节数据,包含加速度、温度和陀螺仪的原始值
class MPU6050:
def __init__(self, bus=1, address=0x68):
self.bus = SMBus(bus)
self.address = address
# 初始化传感器
self._write_byte(0x6B, 0x00) # 退出睡眠模式
self._write_byte(0x1B, 0x00) # 陀螺仪量程 ±250°/s
self._write_byte(0x1C, 0x00) # 加速度计量程 ±2g
def _write_byte(self, reg, value):
self.bus.write_byte_data(self.address, reg, value)
def _read_bytes(self, reg, length):
read = i2c_msg.read(self.address, length)
self.bus.i2c_rdwr(i2c_msg.write(self.address, [reg]), read)
return list(read)
def read_raw_data(self):
# 一次性读取所有传感器数据(14字节)
raw = self._read_bytes(0x3B, 14)
# 加速度计原始值(16位有符号数)
accel_x = (raw[0] << 8) | raw[1]
accel_y = (raw[2] << 8) | raw[3]
accel_z = (raw[4] << 8) | raw[5]
# 温度传感器原始值
temp = (raw[6] << 8) | raw[7]
# 陀螺仪原始值
gyro_x = (raw[8] << 8) | raw[9]
gyro_y = (raw[10] << 8) | raw[11]
gyro_z = (raw[12] << 8) | raw[13]
return accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z, temp
def get_calibrated_data(self):
# 读取原始数据并转换为物理量
accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z, temp = self.read_raw_data()
# 加速度换算为g(±2g量程时灵敏度为16384 LSB/g)
accel_x_g = accel_x / 16384.0
accel_y_g = accel_y / 16384.0
accel_z_g = accel_z / 16384.0
# 陀螺仪换算为°/s(±250°/s量程时灵敏度为131 LSB/(°/s))
gyro_x_dps = gyro_x / 131.0
gyro_y_dps = gyro_y / 131.0
gyro_z_dps = gyro_z / 131.0
# 温度换算(可以不要)
temp_c = (temp / 340.0) + 36.53
return {
'accel': (accel_x_g, accel_y_g, accel_z_g),
'gyro': (gyro_x_dps, gyro_y_dps, gyro_z_dps),
'temp': temp_c
}