$GNGGA,085727.80,3452.56956825,N,11401.14987635,E,1,18,0.9,67.0944,M,-19.0960,M,,*67
1. 语句格式说明
$GNGGA
语句是一种常见的 NMEA(National Marine Electronics Association,美国国家海洋电子协会)标准格式语句,主要用于提供全球定位系统(GPS)的定位相关基本信息,其格式通常为以逗号分隔的多个字段,各字段有特定含义,最后还有校验和部分(以 *
开头后面跟两位十六进制校验和数字)。
2. 各字段解析
$GNGGA
:语句起始标识符,表示这是一条GNGGA
类型的 NMEA 语句,用于区分不同类型的语句(如还有$GNRMC
、$GNVTG
等其他类型语句)。085727.80
:表示定位的协调世界时(UTC)时间,格式为HHMMSS.SS
,也就是小时(HH
)、分钟(MM
)、秒(SS.SS
)。在这个例子中,时间是08
时57
分27.80
秒。以下是提取各时间部分的代码示例:
char time_str[] = "085727.80";
int hours = atoi(time_str) / 10000;
int minutes = (atoi(time_str) % 10000) / 100;
float seconds = (float)(atoi(time_str) % 100);
// 此时,hours = 8,minutes = 57,seconds = 27.80
3452.56956825,N
:表示纬度信息,格式为DDMM.MMMMM
(度分格式,DD
是度,MM.MMMMM
是分及分的小数部分),后面跟着表示北纬(N
)或南纬(S
)的方向标识符。在此例中,纬度是34
度52.56956825
分,且为北纬。以下是进一步提取纬度度、分及相关处理的代码示例:
char lat_str[] = "3452.56956825";
float latitude = atof(lat_str) / 100.0;
int degrees = (int)latitude;
float minutes = (latitude - degrees) * 60.0;
// 此时,latitude经过转换后为度为单位的浮点数(约34.87615947度),degrees = 34,minutes = 52.56956825(分及小数部分)
11401.14987635,E
:表示经度信息,格式同样为DDDMM.MMMMM
(度分格式,DDD
是度,MM.MMMMM
是分及分的小数部分),后面跟着表示东经(E
)或西经(W
)的方向标识符。这里经度是114
度01.14987635
分,且为东经。类似纬度解析,提取经度度、分的代码示例如下:
char lon_str[] = "11401.14987635";
float longitude = atof(lon_str) / 100.0;
int degrees = (int)longitude;
float minutes = (longitude - degrees) * 60.0;
// 经计算,longitude约为114.01916461度,degrees = 114,minutes = 01.14987635(分及小数部分)
1
:定位质量指示符,含义如下:0
:定位无效。1
:定位有效(单点定位,比如 GPS 单独定位)。2
:差分定位(使用了差分基站等辅助定位手段,精度更高)。3
:PPS(Pulse Per Second,每秒脉冲数)定位等。
在此例中,值为1
,表示定位是有效的单点定位。
18
:表示使用的卫星数量,即此次定位使用了18
颗卫星的信号来进行定位计算。0.9
:水平精度因子(HDOP,Horizontal Dilution of Precision),是反映定位精度的一个指标,数值越小通常表示定位精度越高。这里HDOP
值为0.9
,说明当前定位精度相对较好。67.0944,M
:表示海拔高度(单位:米),相对于平均海平面的高度。这里海拔高度为67.0944
米,后面的M
表示单位是米(Meter
)。-19.0960,M
:表示大地水准面高度(单位:米),大地水准面是一个近似于平均海平面的参考面,这个值反映了与该参考面的高度差,这里大地水准面高度为-19.0960
米,同样单位是米(Meter
)。*67
:校验和部分,用于验证数据的完整性和准确性,是通过对$
和*
之间的所有字符按照一定算法(一般是异或运算等)计算得出的两位十六进制数值。接收端可以根据同样的算法重新计算校验和并与接收到的校验和进行对比,如果不一致则可能说明数据在传输过程中出现了错误。
以下是采用中断方式接收 NMEA 语句(以 $GNGGA
语句为例)并进行解析的完整代码示例,基于 STM32 标准库(以 STM32F103 为例,你可根据实际使用的 STM32 型号调整相应的寄存器配置等细节),代码中包含了串口初始化、中断配置、校验和验证以及 $GNGGA
语句的解析功能,同时对校验和错误等情况做了相应处理:
#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 定义接收缓冲区大小,可根据实际情况调整
#define RX_BUFFER_SIZE 256
// 接收数据缓冲区
unsigned char rx_buffer[RX_BUFFER_SIZE];
// 当前接收到的数据在缓冲区中的索引位置
unsigned int rx_index = 0;
// 串口初始化函数
void USART1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 开启USART1和对应GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置USART1的Tx引脚(PA9)为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART1的Rx引脚(PA10)为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART1的参数
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 使能USART1接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 使能USART1
USART_Cmd(USART1, ENABLE);
}
// 计算NMEA语句校验和的函数
unsigned char calculate_checksum(const char *nmea_sentence) {
unsigned char checksum = 0;
int i = 1; // 从第一个字符(跳过'$')开始计算
while (nmea_sentence[i]!= '*') {
checksum ^= nmea_sentence[i];
i++;
}
return checksum;
}
// 验证NMEA语句校验和的函数
int verify_checksum(const char *nmea_sentence) {
int len = strlen(nmea_sentence);
if (len < 3) { // 语句至少应该有'$'、一些数据和'*'加校验和
return 0;
}
char received_checksum_str[3];
strncpy(received_checksum_str, nmea_sentence + len - 2, 2); // 提取接收到的校验和
received_checksum_str[2] = '\0';
unsigned char received_checksum = (unsigned char)strtoul(received_checksum_str, NULL, 16); // 将十六进制字符串转换为无符号字符
unsigned char calculated_checksum = calculate_checksum(nmea_sentence);
return (received_checksum == calculated_checksum);
}
// 解析$GNGGA语句的函数
void parse_GNGGA(const char *nmea_sentence) {
char *ptr;
ptr = strtok((char *)nmea_sentence, ","); // 以逗号为分隔符拆分字符串,首次调用传入缓冲区首地址
ptr = strtok(NULL, ","); // 跳过语句头(即"$GNGGA"),获取时间字段(格式如HHMMSS.SS)
if (ptr!= NULL) {
// 这里可以根据需求进一步处理时间数据,比如将其拆分为小时、分钟、秒等部分
int hours = atoi(ptr) / 10000;
int minutes = (atoi(ptr) % 10000) / 100;
float seconds = (float)(atoi(ptr) % 100);
// 示例中只是简单提取了各部分,可按需存储或使用这些时间数据
}
ptr = strtok(NULL, ","); // 获取纬度数据(格式为DDMM.MMMMM,DD表示度,MM.MMMMM表示分及小数部分)
if (ptr!= NULL) {
float latitude = atof(ptr) / 100.0; // 将纬度字符串转换为浮点数并按NMEA格式要求进行转换(除以100)
int degrees = (int)latitude; // 提取度的整数部分
float minutes = (latitude - degrees) * 60.0; // 提取分及小数部分
// 此时可以选择将度、分单独存储,或者根据应用需求组合成合适的表示形式
}
ptr = strtok(NULL, ","); // 获取纬度方向(N或S),用于确定是北纬还是南纬
if (ptr!= NULL) {
char latitude_direction = ptr[0];
// 根据latitude_direction的值('N'或'S')可以在后续应用中正确处理纬度正负等情况
}
ptr = strtok(NULL, ","); // 获取经度数据(格式同纬度类似,如DDDMM.MMMMM,DDD表示度,MM.MMMMM表示分及小数部分)
if (ptr!= NULL) {
float longitude = atof(ptr) / 100.0; // 经度数据的格式转换,与纬度类似
int degrees = (int)longitude;
float minutes = (longitude - degrees) * 60.0;
// 同样可以按需求处理经度的度、分表示及后续应用相关问题
}
ptr = strtok(NULL, ","); // 获取经度方向(E或W),确定是东经还是西经
if (ptr!= NULL) {
char longitude_direction = ptr[0];
// 根据longitude_direction来正确设置经度的正负等在应用中的表示
}
ptr = strtok(NULL, ","); // 获取定位质量指示符(0:定位无效,1:定位有效等)
if (ptr!= NULL) {
int fix_quality = atoi(ptr);
// 根据定位质量值可以判断当前定位结果是否可靠,进而决定是否使用相关数据等操作
}
ptr = strtok(NULL, ","); // 获取使用的卫星数量
if (ptr!= NULL) {
int satellite_count = atoi(ptr);
// 可以记录卫星数量,例如用于显示或者分析定位精度可能受卫星数量影响等情况
}
ptr = strtok(NULL, ","); // 获取水平精度因子(HDOP,反映定位精度的一个指标)
if (ptr!= NULL) {
float hdop = atof(ptr);
// 根据HDOP值评估定位精度,比如在高精度要求应用中可设置阈值来判断精度是否满足要求
}
ptr = strtok(NULL, ","); // 获取海拔高度(单位:米)
if (ptr!= NULL) {
float altitude = atof(ptr);
// 可以对海拔高度数据进行存储、显示或者与其他传感器数据对比等操作
}
ptr = strtok(NULL, ","); // 获取大地水准面高度(单位:米),用于和海拔高度结合等应用场景
if (ptr!= NULL) {
float geoid_height = atof(ptr);
// 可根据大地水准面高度进一步校正海拔高度等相关操作(在需要高精度高度数据的应用中)
}
}
// 串口1中断服务函数
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) // 判断接收中断标志位
{
rx_buffer[rx_index++] = USART_ReceiveData(USART1); // 将接收到的数据存入缓冲区
if (rx_index >= RX_BUFFER_SIZE) // 如果缓冲区已满,进行相应处理,比如重置索引
{
rx_index = 0;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除接收中断标志位
}
}
// 主函数
int main(void)
{
// 系统初始化相关配置(时钟等)
SystemInit();
// 配置串口1
USART1_Config();
// 配置NVIC,使能USART1中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
while (1)
{
// 查找是否接收到完整的$GNGGA语句(简单判断以'$'开始,以'\r'或'\n'结束,实际可能更复杂)
char *start = strstr((char *)rx_buffer, "$GNGGA");
char *end = NULL;
if (start!= NULL)
{
end = strchr(start, '\r'); // 先尝试查找'\r'作为结束符
if (end == NULL)
{
end = strchr(start, '\n'); // 如果没有'\r',查找'\n'作为结束符
}
if (end!= NULL)
{
int len = end - start + 1;
char nmea_sentence[len];
strncpy(nmea_sentence, start, len);
nmea_sentence[len - 1] = '\0'; // 去除结束符并添加字符串结束标志'\0'
// 验证校验和
if (verify_checksum(nmea_sentence))
{
// 校验和正确,解析$GNGGA语句
parse_GNGGA(nmea_sentence);
}
else
{
// 校验和错误,可进行相应处理,如丢弃数据、记录错误等
printf("Checksum error in $GNGGA statement!\n");
// 此处选择丢弃数据,重置接收缓冲区索引
rx_index = 0;
}
}
}
}
}
以下是对上述代码的详细说明:
1. 串口初始化(USART1_Config
函数)
首先开启了 USART1 和对应 GPIO(PA9 用于发送,PA10 用于接收)的时钟,然后分别配置了这两个引脚的模式(PA9 为复用推挽输出,PA10 为浮空输入),接着设置了 USART1 的通信参数,包括波特率为 9600、8 位数据位、1 位停止位、无校验位以及无硬件流控制,并且设置为接收和发送模式(USART_Mode_Rx | USART_Mode_Tx
)。最后,使能了 USART1 接收中断(USART_ITConfig(USART1, USART_IT_RXNE, ENABLE)
)以及 USART1 本身(USART_Cmd(USART1, ENABLE)
),这样当有数据通过串口接收引脚到达时,就会触发中断
2. 校验和相关函数
calculate_checksum
函数:按照 NMEA 语句校验和的计算规则,从语句的第二个字符(跳过$
符号)开始,对每个字符依次进行异或运算,直到遇到*
字符为止,最后返回计算得到的校验和值,用于后续和接收到的校验和进行对比验证。verify_checksum
函数:先获取接收到的 NMEA 语句的长度,判断其是否足够包含校验和部分(长度至少要有 3 个字符,即$
、数据和*
加校验和)。然后提取语句中*
后面的两位十六进制校验和字符串,并将其转换为无符号字符类型的实际校验和值。接着调用calculate_checksum
函数计算语句本身应有的校验和,对比两者是否相等来判断校验和是否正确,返回 1 表示正确,0 表示错误。
3. $GNGGA
语句解析函数(parse_GNGGA
函数)
- 以逗号为分隔符,使用
strtok
函数逐步拆分$GNGGA
语句字符串,按照其各字段固定的格式规范,依次提取并处理时间(拆分为小时、分钟、秒等)、纬度(转换格式并提取度、分部分以及确定南北纬方向)、经度(类似纬度的处理方式,确定东西经方向)、定位质量指示符、卫星数量、水平精度因子、海拔高度、大地水准面高度等各字段的数据,方便后续在具体应用中根据这些定位相关信息进行进一步操作,比如显示位置、评估定位精度等。
4. 串口中断服务函数(USART1_IRQHandler
函数)
- 当中断触发(即接收到数据,
USART_IT_RXNE
中断标志位被置位)时,将接收到的数据存入预先定义的接收缓冲区rx_buffer
中,并更新rx_index
来记录当前接收到的数据在缓冲区中的位置。如果缓冲区已满(rx_index
达到RX_BUFFER_SIZE
),则将rx_index
重置为 0,避免缓冲区溢出。最后,清除接收中断标志位(USART_ClearITPendingBit(USART1, USART_IT_RXNE)
),以便下次能继续触发中断接收新的数据。
5. 主函数(main
函数)
- 首先进行系统初始化(
SystemInit
),然后调用USART1_Config
函数配置串口 1,接着配置 NVIC(嵌套中断向量控制器)来使能 USART1 的中断,设置其抢占优先级和子优先级等参数。 - 在主循环中,不断查找接收缓冲区中是否存在完整的
$GNGGA
语句(通过简单判断以$
开始,以\r
或\n
结束,实际应用中可能需要更严谨的判断机制),找到完整语句后提取出来存到nmea_sentence
字符串中,调用verify_checksum
函数验证其校验和是否正确。若校验和正确,则调用parse_GNGGA
函数解析该语句提取相关定位信息;若校验和错误,则进行相应的错误处理,这里是打印错误信息并丢弃当前数据,重置接收缓冲区索引,等待接收下一轮数据。
通过这种中断方式接收数据,可以提高 CPU 的利用效率,避免在等待数据接收过程中一直占用 CPU 资源,使得程序可以在后台自动接收数据,同时在接收到完整语句且校验和正确后及时进行解析处理,满足对实时性和准确性有一定要求的应用场景,比如车载导航、船舶定位等系统中对 GPS 定位数据的获取与处理。你可以根据实际需求进一步扩展和完善代码功能,例如添加对其他类型 NMEA 语句的解析、将解析结果通过显示屏展示或者进行更复杂的导航相关运算等。
注意事项
- 中断优先级设置:在实际应用中,要根据整个系统中其他中断的情况合理设置 USART1 中断的优先级,避免出现中断嵌套混乱或者重要中断响应不及时等问题。
- 数据处理速度与缓冲区大小:如果外部发送数据速度过快,而主函数中对数据的处理速度跟不上接收速度,可能会导致缓冲区溢出等问题,需要根据实际情况适当增大缓冲区大小或者优化数据处理逻辑以提高处理效率。
- 接收结束符判断:代码中以
\r
或\n
来判断$GNGGA
语句结束只是一种简单的方式,实际中不同的 GPS 设备或者数据源发送的 NMEA 语句结束符可能有差异,需要根据具体情况进一步完善判断机制,确保能准确提取完整的语句进行解析。