背景
在进行组合导航系统开发时,处理来自卫星导航模块的数据报文是必不可少的步骤之一。特别是对于NMEA格式的信息,这类数据以字符形式传输且长度不固定,其处理过程既可视为直接明了也充满挑战。初步考虑时,我的首选方案是从互联网资源中寻找现成的解析函数,期望这些工具具有较高的通用性,从而减少个人调试所需的时间成本。
然而,在实际应用过程中发现,所选用的几个开源实现虽然代码结构清晰优雅,但在执行阶段却暴露出了诸如内存溢出等严重问题,这些问题极大地增加了调试工作的复杂度。经过对现有解决方案的多次修改尝试后,效果均不尽如人意。鉴于此,我最终决定自主研发一套新的解析算法,采用基于字符识别与转换的基础方法来进行数据处理,避免使用过于复杂的函数调用,以期达到稳定可靠的运行状态。
【说明】开源的NMEA_GPS_parse已经解决了内存溢出问题,现在应当可以正常使用了。
https://gitee.com/boo0ood/NMEA_GPS_parse
思路
无论是采用将接收到的数据暂存于缓冲区中进行逐步处理的方式,还是选择即时处理每一份接收到的数据包,数据解析的基本策略均是首先定位报文的起始与终止标记,随后利用格式化输入方法提取并解析其中的有效内容。
程序思路可以归纳为以下几个步骤:
1. 验证数据格式:
首先,需要检查接收到的数据是否符合NMEA协议的格式。NMEA数据通常以$开头,并以回车换行符\r\n结尾。这是识别NMEA语句的重要标志。
例如,一个有效的NMEA语句可能看起来像这样:$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n。
2. 提取数据字段:
在确认数据格式正确后,下一步是提取语句中的数据字段。NMEA语句中的数据字段由逗号,分隔。
继续以上面的GPGGA语句为例,字段包括时间(123519)、纬度(4807.038)、经度(01131.000)等,每个字段都对应着特定的信息。
3. 解析特定信息:
根据需要,解析出特定类型的信息。不同类型的NMEA语句(如GGA、RMC、VTG等)包含不同的信息集,因此需要根据语句类型来解析相应的数据。
例如,在GPGGA语句中,我们可能关注时间、位置和海拔信息。时间字段通常是UTC时间,位置和海拔则以特定的格式和单位给出。
4. 数据转换和处理:
提取出的数据字段可能需要进一步转换或处理才能使用。例如,纬度和经度可能需要从度分格式转换为十进制度格式,时间可能需要转换为更常用的日期时间格式。
对于海拔等信息,可能还需要根据单位(如米或英尺)进行适当的转换。
5. 错误处理和验证:
在解析过程中,应该实施错误处理机制来应对格式错误、数据缺失或损坏的情况。这可以通过检查字段数量、验证数据范围或格式、以及使用校验和(如NMEA语句末尾的CRC校验)来实现,这里就不作处理了。
代码
#ifndef NMEA_PARSE_H
#define NMEA_PARSE_H
#include "stm32f4xx_hal.h"
#define NMEA_MAX_LENGTH 82
// GGA data
typedef struct {
float time;
double latitude;
char lat_dir;
double longitude;
char lon_dir;
uint8_t fix_quality;
uint8_t num_satellites;
float altitude;
float hdop;
float vdop;
float alterr;
uint8_t basenum;
uint8_t chk;
} GGA_Data;
// VTG data
typedef struct {
float yaw_true;
float yaw_mag;
float v_kn;
float v_km;
uint16_t num1;
uint16_t num2;
} VTG_Data;
typedef enum{
NOGNSS = 0,
VELUDT,
POSUDT
}GNSS_SOL;
/**************** 函数定义 ***************/
uint8_t parseNMEA(GGA_Data* gga_data, VTG_Data* vtg_data, char* str);
#endif
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "NMEAParse.h"
#define GGA_LEN (64)
//#define VTG_LEN (32)
#define VTG_LEN (12)
char str_part[256];
/**
* @brief 处理GNSS字符串并填充相应的数据结构
*
* 根据传入的字符串和类型信息,解析GNSS数据,并填充到相应的数据结构中。
*
* @param str 待解析的GNSS字符串
* @param info 类型信息,1表示GGA数据,2表示VTG数据
* @param gga_data GGA数据结构体指针
* @param vtg_data VTG数据结构体指针
*
* @return 返回GNSS数据类型,POSUDT表示位置数据,VELUDT表示速度数据,NOGNSS表示无效数据
*/
GNSS_SOL GNSSStrProc(char* str, int info, GGA_Data* gga_data, VTG_Data* vtg_data)
{
uint16_t len = strlen(str);
if (info==1 && len>=GGA_LEN) {
sscanf(str, ",%f,%lf,%c,%lf,%c,%hhu,%hhu,%f,%f,M,%f,M,%f,%hhu*%hhu",
&gga_data->time, &gga_data->latitude, &gga_data->lat_dir, &gga_data->longitude, &gga_data->lon_dir,
&gga_data->fix_quality, &gga_data->num_satellites, &gga_data->hdop, &gga_data->altitude, &gga_data->alterr,
&gga_data->vdop, &gga_data->basenum, &gga_data->chk);
return POSUDT;
} else if(info==2 && len>=VTG_LEN) {
sscanf(str, ",%f,T,%f,M,%f,N,%f,K,%hd*%hd",
&vtg_data->yaw_true, &vtg_data->yaw_mag, &vtg_data->v_km, &vtg_data->v_kn, &vtg_data->num1, &vtg_data->num2);
return VELUDT;
}
return NOGNSS;
}
/************************************************
函数名称 : parseNMEA
功 能 : NMEA数据处理
参 数 : GGA_Data* 定位数据结构体指针
VTG_Data* 速度数据结构体指针
char* NMEA数据
返 回 值 :
*************************************************/
uint8_t parseNMEA(GGA_Data* gga_data, VTG_Data* vtg_data, char* str)
{
uint16_t len = strlen(str), item_size = 0;
char *str_p0;
uint16_t item_info = 0;
uint8_t gnss_state = 0;
for (int k=5; k<len; k++) {
if (str[k-5] == '$' && str[k-2]=='G' && str[k-1]=='G' && str[k]=='A') {
str_p0 = str+k+1;
item_size = 0;
item_info = 1; // 位置信息
}
if (str[k-5] == '$' && str[k-2]=='V' && str[k-1]=='T' && str[k]=='G') {
str_p0 = str+k+1;
item_size = 0;
item_info = 2; // 速度信息
}
if (str[k-5] == '$' && str[k-2]=='R' && str[k-1]=='M' && str[k]=='C') {
str_p0 = str+k+1;
item_size = 0;
item_info = 0; // 位置信息
}
if (str[k-5] == '$' && str[k-2]=='G' && str[k-1]=='S' && str[k]=='A') {
str_p0 = str+k+1;
item_size = 0;
item_info = 0; // GSA信息
}
if (str[k-5] == '$' && str[k-2]=='G' && str[k-1]=='S' && str[k]=='V') {
str_p0 = str+k+1;
item_size = 0;
item_info = 0; // GSV信息
}
if (str[k-5] == '$' && str[k-2]=='Z' && str[k-1]=='D' && str[k]=='A') {
str_p0 = str+k+1;
item_size = 0;
item_info = 0; // ZDA信息
}
if (str[k-5] == '$' && str[k-2]=='T' && str[k-1]=='X' && str[k]=='T') {
str_p0 = str+k+1;
item_size = 0;
item_info = 0; // TXT信息
}
if (str[k-5] == '$' && str[k-2]=='G' && str[k-1]=='L' && str[k]=='L') {
str_p0 = str+k+1;
item_size = 0;
item_info = 0; // GLL信息
}
item_size++;
if (str[k-1]=='\r' && str[k]=='\n' && item_info>0) {
memcpy(str_part, str_p0, item_size);
str_part[item_size-3] = '\0';
gnss_state += GNSSStrProc(str_part, item_info, gga_data, vtg_data);
}
}
return gnss_state;
}
测试
这里的测试就是将一组确定的NMEA消息重复100,000次,主要是防止出现内存管理问题。
测试项目:https://gitee.com/tmrnic/inertial-navigation-code/tree/master/NMEAParse_C

参考资料
● https://gitee.com/boo0ood/NMEA_GPS_parse
● https://blog.csdn.net/Akaxi1/article/details/138528551
● https://blog.csdn.net/return_oops/article/details/100976574
● https://blog.csdn.net/qq_43557686/article/details/123893672
● https://blog.csdn.net/fddnihao/article/details/122126375