ATGM336H定位模块支持GPS系统,BDS(北斗)系统,GLONASS(俄罗斯)系统,伽利略卫星导航系统(欧盟)。这个模块要拿到室外才能接收到信号,且初次初始化或者隔太久时间没有启用会导致获取定位信息的时间很长。
可以使用中科微电子提供的集成软件设置模块,可以设置串口输出的参数,波特率等等参数。
本文介绍用STM32串口接收定位模块的数据,并将数据进行解析,解析后得到经纬度的原始数据,把经纬度原始数据转换成精确的经纬度信息后存放到数组里,可打印或继续串口传输到其他设备,后面会介绍传输到ESP32,并保存到SPIFFS中。
上图是模块通过串口向上位机输出的数据帧样例。
通过GNSS工具可以看到模块能够获取的数据帧有如下格式。
其中RMC数据是最简定位信息,得到这串数据后,可以将GNRMC开始的前六项数据提取,分别是UTC时间,数据有效标志位,纬度,纬度方向,经度,经度方向。此时的经度和纬度只是原始数据,需要再转换为精确数据。
$GNRMC,123211.000,A,2295.33602,N,11326.27041,E,3.21,217.19,100722,,,A*7A
数据转换:
2322.74250 格式:ddmm.mmmmm
11326.27041 格式:ddmm.mmmmm
转换成北纬:23 + 22.74250 / 60 = 23.37904
转换成东经:113 + 26.27041 / 60 = 113.43784
比较不准。。串口输出的数据就是这样的,估计芯片哪里出了问题。。
读取数据可以先用一个缓存数组接收所有的数据,先判断是不是$GNRMC开头的数据帧,如果是的话则逐个字符接收,因为数据帧是以换行符为结尾的,当读取到’\n’则是数据帧的结尾。
定义几个字符数组,用于分别存放UTC时间,经度和纬度的原始数组,经度和纬度的方向。
以逗号为分隔符切割字符串,这里我们需要用到strstr这个函数。
返回指向str1中第一次出现str2的位置的指针,当str1中没有str2则返回空指针。我们可以定义一个子串指针,用这个函数在数据帧中找逗号,再用memcpy函数把数据分割后存在相应的数组。
#include "ATGM336H.h"
#include "string.h"
#define GPS_Buffer_Length 80
#define UTCTime_Length 11
#define latitude_Length 11
#define N_S_Length 2
#define longitude_Length 12
#define E_W_Length 2
char USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
typedef struct SaveData
{
char GPS_Buffer[GPS_Buffer_Length];
char isGetData; //数据获取完成标志位
char isParseData; //解析完成标志位
char UTCTime[UTCTime_Length]; //UTC时间
char latitude[latitude_Length]; //纬度
char N_S[N_S_Length]; //N/S
char longitude[longitude_Length]; //经度
char E_W[E_W_Length]; //E/W
char isUsefull; //信息有效标志位
}GNRMC;
首先定义一个结构体存放几个数组,并宏定义数组的长度。
void ATGM_StructInit()
{
GNRMC_Info.isGetData = false;
GNRMC_Info.isParseData = false;
GNRMC_Info.isUsefull = false;
memset(GNRMC_Info.GPS_Buffer, 0, GPS_Buffer_Length);
memset(GNRMC_Info.UTCTime, 0, UTCTime_Length);
memset(GNRMC_Info.latitude, 0, latitude_Length);
memset(GNRMC_Info.N_S, 0, N_S_Length);
memset(GNRMC_Info.longitude, 0, longitude_Length);
memset(GNRMC_Info.E_W, 0, E_W_Length);
}
首先初始化结构体,先把结构体成员内容全部置零。
void ATGM336H_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
USART_InitTypeDef USART_InitStructure;
ATGM_StructInit();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//USART1_TX PA.9
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 PA.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置中断通道
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3 ;//优先级最低
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
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);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);
ClrBuf(); //初始化
}
之后配置引脚,配置串口1中断,配置串口。引脚TX我选择PA9,RX选择PA10。TX要选择复用推挽输出,RX选择浮空输入。之后开通NVIC中断通道,配置串口波特率为9600,8位数据位,1位停止位,吴娇艳,无硬件流控制,模式为收发模式,因为我既要接收定位数据,也要打印或发送数据。最后别忘记使能中断和串口。
unsigned int DataIndex = 0;
void USART1_IRQHandler()
{
unsigned char receive;
if(USART_GetITStatus(USART1, USART_IT_RXNE) == 1)
{
receive = USART_ReceiveData(USART1); //读取接收到的数据
if(receive == '$')
DataIndex = 0;
USART_RX_BUF[DataIndex++] = receive;
//GNRMC\GPRMC
if(USART_RX_BUF[0] == '$' && USART_RX_BUF[4] == 'M' && USART_RX_BUF[5] == 'C')
{
if(receive == '\n')
{
memset(GNRMC_Info.GPS_Buffer, 0, GPS_Buffer_Length); //清空
memcpy(GNRMC_Info.GPS_Buffer, USART_RX_BUF, DataIndex); //保存数据
GNRMC_Info.isGetData = true;
DataIndex = 0;
memset(USART_RX_BUF, 0, USART_REC_LEN); //清空
}
}
}
}
先是定义一个DataIndex作为数组的下标,当读到了$标志表示数据帧开始,将下标置零,之后判断是不是我们要读取的这种格式,如果是则读到’\n’,即把数据转而存储到GPS_Buffer中,并把数据读取标志位置为true。
void ParseGps()
{
char *subString;
char *subStringNext;
char i = 0;
if (GNRMC_Info.isGetData)
{
GNRMC_Info.isGetData = false;
// printf("\r\n");
// printf(GNRMC_Info.GPS_Buffer);
//截取数据帧前六部分 |对地航速 对地航向 日期
//$GNRMC,112536.000,A,2322.75023,N,11326.28605,E,| 0.00, 0.00, 100722,,,A*78
for (i = 0 ; i <= 6 ; i++)
{
if (i == 0)
{
if ((subString = strstr(GNRMC_Info.GPS_Buffer, ",")) == NULL)//如果没有找到逗号
{
return;
//ERROR
}
}
else
{
subString++;
if ((subStringNext = strstr(subString, ",")) != NULL)
{
char usefulBuffer[2];
switch(i)
{
case 1:memcpy(GNRMC_Info.UTCTime, subString, subStringNext - subString);break;
case 2:
{
memcpy(usefulBuffer, subString, subStringNext - subString);//有效标志位
if(usefulBuffer[0] == 'A')
GNRMC_Info.isUsefull = true;
else if(usefulBuffer[0] == 'V')
GNRMC_Info.isUsefull = false;
break;
}
case 3:memcpy(GNRMC_Info.latitude, subString, subStringNext - subString);break;
case 4:memcpy(GNRMC_Info.N_S, subString, subStringNext - subString);break;
case 5:memcpy(GNRMC_Info.longitude, subString, subStringNext - subString);break;
case 6:memcpy(GNRMC_Info.E_W, subString, subStringNext - subString);break;
default:break;
}
subString = subStringNext;
}
}
}
GNRMC_Info.isParseData = true;
}
}
这里进行六次循环,把数据帧分成六部分,用strstr函数找到从子串next指针开始的下一个逗号的位置,并把这个这个位置到子串指针之间的数据分别存到各自的数组中去。六次循环之后便结束,因为后面的数据是对地航速,对地航向和日期,我没有这个需求,有需要的同学可以自己修改。
extern float Lat;
extern float Lon;
extern char dest[23];
void printGpsBuffer()
{
//$GNRMC,123211.000,A,2322.74250,N,11326.27041,E,3.21,217.19,100722,,,A*7A
if (GNRMC_Info.isParseData)
{
int i = 0;
GNRMC_Info.isParseData = false;
if(GNRMC_Info.isUsefull)
{
float tmp = 0; int j = 0;
GNRMC_Info.isUsefull = false;
for (i = 0; GNRMC_Info.latitude[i] != '\0'; i++)
{
if (GNRMC_Info.latitude[i] == '.')
{
continue;
}
if (i <= 1)
{
Lat = (GNRMC_Info.latitude[0] - 48) * 10 + (GNRMC_Info.latitude[1] - 48);
//取出个位和十位
}
else
{
tmp += (GNRMC_Info.latitude[i] - 48);
tmp *= 10;
}
}
for (j = 0; j <= 5; j++)
{
tmp /= 10;
}
Lat += tmp / 60;
//23 22.74250
//23.xxxxx
int iLat = 0;
iLat = (int)Lat;
GNRMC_Info.latitude[0] = iLat / 10 + '0';
GNRMC_Info.latitude[1] = iLat % 10 + '0';
GNRMC_Info.latitude[2] = '.';
Lat -= iLat;
for (j = 3; j < 10; j++)
{
Lat *= 10;
iLat = (int)Lat;
GNRMC_Info.latitude[j] = iLat + '0';
Lat -= iLat;
}
tmp = 0;
//113.27041
for (i = 0; GNRMC_Info.longitude[i] != '\0'; i++)
{
if (GNRMC_Info.longitude[i] == '.')
{
continue;
}
if (i <= 2)
{
Lon = (((GNRMC_Info.longitude[0] - 48) * 10 + (GNRMC_Info.longitude[1] - 48)) * 10) + (GNRMC_Info.longitude[2] - 48);
//取出个位和十位和百位
}
else
{
tmp += (GNRMC_Info.longitude[i] - 48);
tmp *= 10;
}
}
for (j = 0; j <= 5; j++)
{
tmp /= 10;
}
int iLon = 0;
//113.43784
Lon += tmp / 60;
iLon = (int)Lon;
GNRMC_Info.longitude[0] = iLon / 100 + '0';
GNRMC_Info.longitude[1] = (iLon % 100) / 10 + '0';
GNRMC_Info.longitude[2] = iLon % 10 + '0';
GNRMC_Info.longitude[3] = '.';
Lon -= iLon;
for (j = 4; j < 11; j++)
{
Lon *= 10;
iLon = (int)Lon;
GNRMC_Info.longitude[j] = iLon + '0';
Lon -= iLon;
}
dest[8] = dest[10] = dest[20] = ',';
dest[9] = 'N'; dest[21] = 'E'; dest[22] = '\0';
for(i = 0; i < 22; i++)
{
if(i <= 7)
dest[i] = GNRMC_Info.latitude[i];
if(i >= 11 && i <= 19)
dest[i] = GNRMC_Info.longitude[i - 11];
}
//printf("\r\ndest = ");
printf(dest);
//printf("\r\n");
}
else
{
printf("GPS DATA Is Not Useful!");
}
}
}
最后就是数据的转换,要把经纬度数组中的字符提取出来组合成数字。对于纬度,先提取数组前两位的数据作为个位和十位,从第三位开始后面的数据提取出来组成一个浮点数,之后把这个浮点数除以60,最后加上个位和十位的整数,便得到了转换后的纬度。经度同理,区别在于经度有百位。
这里只是简单的没有啥依据的不合理的提取。
提取得到两个浮点数后,把经纬度方向一起存储在dest数组中,组合成完整的包括经纬度方向的数据。
数据解析的步骤很繁琐,且模块输出的原始数据不是很准确。后面再试试。