🎀 文章作者:二土电子
🌸 关注公众号获取更多资料!
🐸 期待大家一起学习交流!
文章目录
一、GPS模块简介
我们在做一些项目时有时会需要进行GPS定位,获取自身的经纬度信息,这里使用的是中科微电子的GPS模块ATGM336H,带陶瓷天线。
该模块可以实现GPS定位,返回定位点的UTC时间和经纬度信息。
UTC时间是全世界公用的时间,我们用的北京时间比UTC时间快8个小时。
在使用时必须注意以下几点
- 必须在室外空旷地带才能进行定位;
- 定位是陶瓷天线上的小圆点必须朝上,上方不要有遮挡物;
- 楼间距也可能会影响定位,楼间距较小的地方可能定位失败;
二、使用方法
2.1 引脚介绍
ATGM336H使用3.3V或者5V供电,利用串口将GPS信息发送出来,我们简单地介绍一下它的几个引脚
引脚 | 功能 |
---|---|
VCC | 电源正极(3.3V或5V) |
GND | 电源负极 |
TXD | 串口发送 |
RXD | 串口接收 |
PPS引脚这里没有用到,就不再做介绍了,有兴趣的小伙伴可以自行搜索一下它的用途。
2.2 数据帧介绍
ATGM336H利用串口发送定位信息给主控芯片,串口波特率为9600,我们最开始可以先用USB-TTL连接ATGM336H,到空旷地带观察一下它的输出信息,定位成功时接收到的信息如下
我们可以看到,ATGM336H一次会返回许多信息,其实我们只需要关注其中的“GNRMC”这条信息即可,我们简单看一下这条信息。
$GNRMC,015135.000,A,4159.65553,N,12136.79345,E,0.52,0.00,191123,,,A*7F
- 消息ID —— $GNRMC
- 定位点的UTC时间 —— 015135.000
- 定位状态 —— A:定位;V:导航(我们进行定位时,如果该位为A表示数据有效,该位为V表示数据无效)
- 纬度 —— 4159.65553
- 纬度方向 —— N
- 经度 —— 12136.79345
- 经度方向 —— E
需要注意的是,这里的经纬度并不能直接拿来在地图上搜索定位,而是需要进行数据转换之后才可以,关于数据抓换,后续的程序设计会详细介绍。
2.3 关于不同的启动方式
ATGM336H有三种不同的启动模式,不同模式定位成功所需的时间是不同的,我们这里来简单描述一下这几种启动模式
- 冷启动
冷启动是指在一个陌生的环境下启动 GPS 直到 GPS 和周围卫星联系并且计算出坐标的启动过程。比如我们初次使用,电池耗尽导致星历信息丢失,或者关机状态下将接收机移动 1000 公里以上距离再启动都属于冷启动,冷启动大概需要等待1~2分钟才能定位成功。 - 温启动
温启动是指距离上次定位时间超过 2 个小时的启动,搜星定位时间介于冷启动和热启动之间。 - 热启动
热启动是指在上次关机的地方没有过多移动启动 GPS,但距离上次定位时间必须小于 2 个小时。
三、前置知识
我们在介绍程序设计之前先介绍一下一些必备的前置知识,关于STM32串口的配置这里就不再详细介绍了,具体可以到博主的STM32俗称笔记专栏串口篇查看。这里着重介绍几个C语言中的函数。
3.1 strstr函数
strstr函数原型为
char *strstr( const char *str1, const char *str2 );
该函数是在字符串str1中查找是否含有字符串str2,如果存在,返回str2在str1中第一次出现的地址(指针);否则返回NULL。使用strstr函数时需要包含头文件<string.h>。
值得注意的是,实际输入变量都是指针,如果我们稍加设计,能得到循环查找分隔符的效果,具体可以看后面在解析接收帧信息时的程序设计,这里只介绍一下它的基本用法。
3.2 memset函数
memset函数的函数原型为
void *memset(void *s, int c, size_t n);
memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。
- s指向要填充的内存块。
- c是要被设置的值。
- n是要被设置该值的字符数。
- 返回类型是一个指向存储区s的指针。
3.3 memcpy函数
memcpy函数的函数原型为
void *memcpy(void*dest, const void *src, size_t n);
该函数的功能是将由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。
3.4 strtod函数
strtod函数的函数原型为
double strtod(const char *nptr, char **endptr);
strtod函数会检查输入的nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,到出现非数字或字符串结束时(‘\0’)才结束转换,并将结果返回,返回值是一个double型数值。
若endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr传回。参数nptr字符串可包含正负号、小数点或E(e)来表示指数部分。
使用该函数时需要包含头文件。
四、程序设计
下面我们来进行程序设计,我们用串口1来接收ATGM336H发送来的信息,用串口2将我们解析处理后的GPS信息发送给上位机。
4.1 串口初始化程序
这里用到了两个串口,我们封装一个串口初始化函数用来初始化串口
/*
*==============================================================================
*函数名称:uart_init
*函数功能:初始化USART
*输入参数:UARTx:串口几;bound:波特率
*返回值:无
*备 注:可以修改成输入初始化哪个USART
*==============================================================================
*/
void uart_init(UART_TypeDef UARTx,u32 bound)
{
// 相关结构体定义
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
switch (UARTx)
{
case 0:
// 使能USART1,GPIOA时钟
RCC_APB2PeriphClockCmd (RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA.9
// USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA.10
// Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ; // 抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // IRQ通道使能
NVIC_Init(&NVIC_InitStructure); // 根据指定的参数初始化VIC寄存器
// USART 初始化设置
USART_InitStructure.USART_BaudRate = bound; // 串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为8位数据格式
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); // 初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 开启串口接收中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能空闲中断
USART_Cmd(USART1, ENABLE); // 使能串口1
break;
case 1:
// 使能USART2,GPIOA时钟
RCC_APB1PeriphClockCmd (RCC_APB1Periph_USART2 | RCC_APB2Periph_GPIOA, ENABLE);
// USART2_TX GPIOA.2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; // PA.2
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA.2
// USART2_RX GPIOA.3初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; // PA3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA.3
// Usart2 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ; // 抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // IRQ通道使能
NVIC_Init(&NVIC_InitStructure); // 根据指定的参数初始化VIC寄存器
// USART2 初始化设置
USART_InitStructure.USART_BaudRate = bound; // 串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为8位数据格式
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(USART2, &USART_InitStructure); // 初始化串口2
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); // 开启串口接收中断
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); // 使能空闲中断
USART_Cmd(USART2, ENABLE); // 使能串口2
break;
default:
break;
}
}
4.2 串口1接收中断服务函数
在进行数据接收时,我们一帧一帧地接收,直到接收到我们需要的帧之后将接接收缓冲区的数据复制到我们定义好的接收数据结构体中。
/*
*==============================================================================
*函数名称:USART1_IRQHandler
*函数功能:串口1中断服务函数
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void USART1_IRQHandler (void)
{
u8 recContent; // 存储接收内容
// 如果串口接收到内容
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
recContent = USART_ReceiveData(USART1); // 存储接收内容
// 如果接收到的是$($是一帧信息的开始)
// 保证每接收到新的一帧信息就从缓冲区起始位置开始存储
if(recContent == '$')
{
gReceCunt = 0; // 清零帧信息计数变量
}
// 存储接收到的帧信息
gUart1RcecBuf[gReceCunt ++] = recContent;
// 确定是否收到"GPRMC/GNRMC"这一帧数据
if(gUart1RcecBuf[0] == '$' && gUart1RcecBuf[4] == 'M' && gUart1RcecBuf[5] == 'C')
{
// 接收到换行(接收完了一帧信息)
if(recContent == '\n')
{
memset(receDataFrame.Frame_Buffer, 0, Frame_Buffer_Length); // 初始化接收帧信息数组
memcpy(receDataFrame.Frame_Buffer, gUart1RcecBuf, gReceCunt); // 保存GPRMC/GNRMC这帧的数据
receDataFrame.isGetData = TRUE; // 接收成功
gReceCunt = 0; // 清零接收帧信息接收计数变量
memset(gUart1RcecBuf, 0, UART1RX_MAX_LENGTH); // 清空串口1接收Buf
}
}
// 如果接收内容超出最大长度,不再继续接收
if(gReceCunt >= UART1RX_MAX_LENGTH)
{
gReceCunt = UART1RX_MAX_LENGTH;
}
}
}
4.3 帧信息解析
存储到需要的帧信息后,下一步就是对它进行解析,解析时我们利用每一个数据间的逗号作为分隔符,利用strstr函数寻找逗号对应的地址,将两个逗号之间的信息存储到我们提前定义好的解析信息存储数组中。仔细分析一下“$GNRMC”帧会发现,我们只需要找到七个逗号的地址,提取他们两个相邻逗号中间的字符串就可以得到GPS信息,思路介绍完了,我们来看一下程序设计。
/*
*==============================================================================
*函数名称:Uart_Rece_Pares
*函数功能:解析串口接收内容
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void Uart_Rece_Pares(void) // 串口接收内容解析函数
{
// 注意变量类型
char *point = 0; // 逗号的地址指针
char *nextPoint = 0; // 下一个逗号的地址指针
u8 tempVar = 0; // 临时循环变量
// 如果数据接收成功
if (receDataFrame.isGetData)
{
receDataFrame.isGetData = 0; // 清除接收成功标志位
// for循环解析接收帧
// 总共需要找到7个逗号
for (tempVar = 0;tempVar < 7;tempVar ++)
{
// 第一次循环
if (tempVar == 0)
{
// 寻找第一个逗号
if ((point = strstr(receDataFrame.Frame_Buffer,",")) == NULL)
{
printf ("Prase Errpr!\r\n"); // 解析错误
}
}
else
{
point ++; // 防止重复找到同一个逗号
// 寻找下一个逗号
// 注意strstr函数的输入变量,是从上一个逗号之后开始找下一个逗号
if ((nextPoint = strstr(point,",")) != NULL)
{
// 存储信息
switch (tempVar)
{
case 1: // UTC时间
memcpy(receDataFrame.UTCTime,point,nextPoint - point);
break;
case 2: // 数据有效标识
memcpy(receDataFrame.UsefullFlag,point,nextPoint - point);
break;
case 3: // 纬度
memcpy(receDataFrame.latitude,point,nextPoint - point);
break;
case 4: // 纬度方向
memcpy(receDataFrame.N_S,point,nextPoint - point);
break;
case 5: // 经度
memcpy(receDataFrame.longitude,point,nextPoint - point);
break;
case 6: // 经度方向
memcpy(receDataFrame.E_W,point,nextPoint - point);
break;
}
point = nextPoint; // 更新上一个逗号地址指针
receDataFrame.isParseData = TRUE; // 数据解析完成
// 数据有效
if (receDataFrame.UsefullFlag[0] == 'A')
{
printf ("Data is usefull!\r\n");
}
else if (receDataFrame.UsefullFlag[0] == 'V')
{
printf ("Data is invalid!\r\n");
}
}
else
{
printf ("Prase Errpr!\r\n"); // 解析错误
}
}
}
}
}
4.4 经纬度数据解析转换
解析出经纬度信息后,我们需要将它转换成我们需要的格式,可以直接在地图中输入定位,拿上面的例子来介绍一下转换方法
上面接收到地经纬度信息为
4159.65553,N,12136.79345,E
换算方法和结果为
41 + 59.65553 / 60 = 41.994
121 + 36.79345 / 60 = 121.613
下面我们来展示一下程序实现
/*
*==============================================================================
*函数名称:Data_Transfor
*函数功能:数据转换
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void Data_Transfor (void)
{
float latitude = 0; // 存储纬度信息
u16 temp1 = 0; // 临时变量1,存储整数
float longitude = 0; // 存储经度信息
u16 temp2 = 0; // 临时变量2,存储整数
latitude = strtod(receDataFrame.latitude,NULL); // 字符串转换成浮点数
longitude = strtod(receDataFrame.longitude,NULL); // 字符串转换成浮点数
// 纬度信息处理
// 五位纬度信息
if ((latitude - 10000.0) >= 0)
{
// 前三位需要单独拿出来组成一个数
temp1 = (((u16)latitude / 10000) % 10) * 100 + (((u16)latitude / 1000) % 10) * 10 + ((u16)latitude / 100) % 10;
latitude = latitude - (float)temp1 * 100;
latitude = (float)temp1 + latitude / 60;
printf ("latitude:%.3f\r\n",latitude);
}
else // 四位纬度信息
{
// 前两位需要单独拿出来组成一个数
temp1 = (((u16)latitude / 1000) % 10) * 10 + ((u16)latitude / 100) % 10;
latitude = latitude - (float)temp1 * 100;
latitude = (float)temp1 + latitude / 60;
printf ("latitude:%.3f\r\n",latitude);
}
// 经度信息处理
// 五位经度信息
if ((longitude - 10000.0) >= 0)
{
// 前三位需要单独拿出来组成一个数
temp2 = (((u16)longitude / 10000) % 10) * 100 + (((u16)longitude / 1000) % 10) * 10 + ((u16)longitude / 100) % 10;
longitude = longitude - (float)temp2 * 100;
longitude = (float)temp2 + longitude / 60;
printf ("longitude:%.3f\r\n",longitude);
}
else // 四位经度信息
{
// 前两位需要单独拿出来组成一个数
temp2 = (((u16)longitude / 1000) % 10) * 10 + ((u16)longitude / 100) % 10;
longitude = longitude - (float)temp2 * 100;
longitude = (float)temp2 + longitude / 60;
printf ("longitude:%.3f\r\n",longitude);
}
}