文章目录
- 前言
- 1. 中科微电子ATGM336H的使用
- 1.1 ATGM336H引脚说明
- 1.2 数据帧介绍
- 1.3 经纬度介绍
- 1.4 ATGM336H的启动方式
- 2 数据处理前置C语言知识
- 2.1 strstr函数
- 2.2 memset函数
- 2.3 memcpy函数
- 2.4strtod函数
- 3. 开始移植
- 3.1 usart初始化程序
- 3.2 串口中断接收函数
- 3.4 数据帧的解析
- 3.5数据转换
- 4 参考资料
- 5. 总结
前言
GPS定位能够获取自身所在的经纬度信息,进而定位自己的所在位置。GPS模块有很多,这里用到是中科微电子ATGM336H,带陶瓷天线版本。这个在淘宝就可以搜得到。模块具体长下图这样
1. 中科微电子ATGM336H的使用
在使用该模块时,有一下注意事项:
- 该模块必须在无遮挡的空旷地上使用,室内使用不了
- 陶瓷天线上的接收小圆点必须朝上,且在这上面不能有遮挡物
- 不宜在楼距较小的地方使用,楼距小的地方可能导致定位失败
1.1 ATGM336H引脚说明
该GPS模块支持3.3V-5V供电,因此我们即可以将其接到3.3V电源,也可以接到5V电源。数据通过串口的方式,将其发送给主控单元。几个引脚的定义以及接线如下表和下图所示。
管脚 | 定义 |
---|---|
VCC | 电源(3.3V-5V) |
GND | 地 |
TXD | 模块串口发送引脚,可接单片机的RXD |
RXD | 模块串口接收引脚,可接单片机的TXD |
PPS | 时钟脉冲引脚 |
如果单纯做定位,只需要前4个引脚即可,也即PPS引脚用不到,所以我们PPS这个引脚可以不接,空着就行。具体PPS引脚的作用,大家可以自行查阅。
1.2 数据帧介绍
GPS模块通过串口将数据发给主控芯片,ATGM336H会一次性返回多条信息,其中信息头的第一个是消息ID,标示着通过什么定位系统的采集的数据。简单的含义如下,具体含义可以查询使用手册
缩写标识符 | 含义 |
---|---|
BD | BDS,北斗二代卫星系统 |
GP | GPS |
GL | GLONASS |
GA | Galileo |
GN | GNSS,全球卫星导航系统 |
这里我们只需要关注“GNRMC”这条信息就行
$GNRMC,121520.000,A,3438.1766,N,11224.5016,E,0.08,292.36,140816,,,A*77
- GNRMC:为消息ID
- 121520.000:为定位点UTC时间戳,不知道可以自行查阅
- A:定位状态,如果我们要做导航时,此处就应该为V。也就是如果我们做定位,这时此处不为A,就表示此时数据无效
- 3438.1766:纬度,N表示纬度方向(北纬N)
- 11224.5016:经度,E表示纬度方向(东经)
一般这里的经纬度是不可以直接拿来地图上直接搜索定位的,得对其进行数据转换,才能用。下面小节将对经纬度数据格式进行介绍。
1.3 经纬度介绍
经纬度格式分为三种:度、度-分、度-分-秒
标识符 | 含义 |
---|---|
ddd.ddddd ° | “度.度”格式,其中小数部分为5位10进制的数据 |
ddd°mm.mmm’ | “度.分.分”格式,其中小数部分为3位10进制的数据 |
ddd°mm’ss’’ | “度.分.秒”格式 |
格式之间的转换
(1)度分转换
将度分单位数据转换为度单位数据,主要依据的公式为:度=度+分/60
例如:
经度 = 116°20.12’
纬度 = 39°12.34’
经度 = 116 + 20.12 / 60 = 116.33533°
纬度 = 39 + 12.34 / 60 = 39.20567°
(2)度分秒转换:
将度分秒单位数据转换为度单位数据,主要依据的公式为:度 = 度 + 分 / 60 + 秒 / 60 / 60
例如:
经度 = 116°20’43”
纬度 = 39°12’37”
经度 = 116 + 20 / 60 + 43 / 60 / 60 = 116.34528°
纬度 = 39 + 12 / 60 + 37 / 60 / 60 = 39.21028°
而我们的GPS模块输出为**度-分(ddmm.mmmm)的形式,我们一般使用的是度(dd.dddddd(度))**的形式。所以我们要对齐进行转换,转换主要采取以下步骤
- Step1: 将ddmm.mmmm的小数点向前移动两位,得到dd.mmmmmm
- Step2: 将dd.mmmmmm的小数点部分除以60,即mmmmmm/60
- Step3: 所以所得到结果为dd.(mmmmmm/60)即为我们想要的度的格式
例如:
12023.4047 → 120.234047 → 120 + 0.(234047÷60) = 120.390078
1.4 ATGM336H的启动方式
ATGM336H有三种不同的启动模式
启动模式 | 含义 |
---|---|
冷启动 | 适用于初次使用时,电池电耗尽星历信息丢失,或者关机后,移动超过1000公里以上距离,这些都属于冷启动,冷启动时间很长,一般需要几分钟才能定位成功 |
温启动 | 温启动是指在上次关机的地方没有较大的位移变化,且距离上次定位时间超过2h,但不足4h |
热启动 | 热启动是指在上次关机的地方没有较大的位移变化,且距离上次定位时间不足2h |
2 数据处理前置C语言知识
我们收到数据后,需要对齐进行处理,这里分享几个C语言函数,有助于我们后期数据处理。这些函数都包含在<string.h>头文件中,使用这函数必须包含该头文件。
2.1 strstr函数
strstr的原型如下所示。主要功能是在字符串str1中,查找是否有str2子字符串,也就str1字符串长度必须大于等于str2,否则将失去这个函数的意义。如果存在,将返回str2在str1中第一次出现的地址,不存将返回NULL
char *strstr( const char *str1, const char *str2 );
2.2 memset函数
memset的原型如下所示。主要功能是用于初始化,对指定地址的连续指定的字节赋予指定的值。
void *memset(void *s, int c, size_t n);
- s: 为指定的起始地址
- c: 为期望赋予的值
- n 期待被赋予的连续字节数
- 返回类型是一个指向存储区的s的指针
2.3 memcpy函数
strtod的原型如下所示。主要功能是将一块地址指定连续字节数,复制到另一个地址中。
double strtod(const char *nptr, char **endptr);
- dest: 将src的数据复制到dest中
- src: 被复制的源目标
- n 期待被赋予的连续字节数
2.4strtod函数
strtod的原型如下所示。主要功能是将一块地址指定连续字节数,复制到另一个地址中。
double strtod(const char *nptr, char **endptr);
- strtod()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,到出现非数字或字符串结束时(‘\0’)才结束转换,并将结果返回。
- src: 被复制的源目标
- 若endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr传回。参数nptr字符串可包含正负号、小数点或E(e)来表示指数部分。如123.456或123e-2。
例如
char *endptr;
char a[] = "12345.6789";
char b[] = "1234.567qwer";
char c[] = "-232.23e4";
printf( "a=%lf\n", strtod(a,NULL) ); //结果为a=12345.678900
printf( "b=%lf\n", strtod(b,&endptr) ); //结果为b=1234.567000
printf( "endptr=%s\n", endptr ); //结果为endptr=qwer
printf( "c=%lf\n", strtod(c,NULL) ); //结果为c=-2322300.000000
3. 开始移植
首先GSP是使用的串口通信,我们第一步需要先配置我们单片机的串口,并将我们的串口波特率配置为9600。下面是本次测试的代码。
3.1 usart初始化程序
//硬件驱动
#include "usart.h"
//#include "delay.h"
//C库
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
/*
************************************************************
* 函数名称: Usart1_Init
*
* 函数功能: 串口1初始化
*
* 入口参数: baud:设定的波特率
*
* 返回参数: 无
*
* 说明: TX-PA9 RX-PA10
************************************************************
*/
void Usart1_Init(unsigned int baud)
{
GPIO_InitTypeDef gpioInitStruct;
USART_InitTypeDef usartInitStruct;
NVIC_InitTypeDef nvicInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
//PA9 TXD
gpioInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
gpioInitStruct.GPIO_Pin = GPIO_Pin_9;
gpioInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStruct);
//PA10 RXD
gpioInitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
gpioInitStruct.GPIO_Pin = GPIO_Pin_10;
gpioInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStruct);
usartInitStruct.USART_BaudRate = baud;
usartInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件流控
usartInitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //接收和发送
usartInitStruct.USART_Parity = USART_Parity_No; //无校验
usartInitStruct.USART_StopBits = USART_StopBits_1; //1位停止位
usartInitStruct.USART_WordLength = USART_WordLength_8b; //8位数据位
USART_Init(USART1, &usartInitStruct);
USART_Cmd(USART1, ENABLE); //使能串口
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中断
nvicInitStruct.NVIC_IRQChannel = USART1_IRQn;
nvicInitStruct.NVIC_IRQChannelCmd = ENABLE;
nvicInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
nvicInitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&nvicInitStruct);
}
/*
************************************************************
* 函数名称: Usart2_Init
*
* 函数功能: 串口2初始化
*
* 入口参数: baud:设定的波特率
*
* 返回参数: 无
*
* 说明: TX-PA2 RX-PA3
************************************************************
*/
void Usart2_Init(unsigned int baud)
{
GPIO_InitTypeDef gpioInitStruct;
USART_InitTypeDef usartInitStruct;
NVIC_InitTypeDef nvicInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
//PA2 TXD
gpioInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
gpioInitStruct.GPIO_Pin = GPIO_Pin_2;
gpioInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStruct);
//PA3 RXD
gpioInitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
gpioInitStruct.GPIO_Pin = GPIO_Pin_3;
gpioInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStruct);
usartInitStruct.USART_BaudRate = baud;
usartInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件流控
usartInitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //接收和发送
usartInitStruct.USART_Parity = USART_Parity_No; //无校验
usartInitStruct.USART_StopBits = USART_StopBits_1; //1位停止位
usartInitStruct.USART_WordLength = USART_WordLength_8b; //8位数据位
USART_Init(USART2, &usartInitStruct);
USART_Cmd(USART2, ENABLE); //使能串口
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); //使能接收中断
nvicInitStruct.NVIC_IRQChannel = USART2_IRQn;
nvicInitStruct.NVIC_IRQChannelCmd = ENABLE;
nvicInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
nvicInitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&nvicInitStruct);
}
/*
************************************************************
* 函数名称: Usart_SendString
*
* 函数功能: 串口数据发送
*
* 入口参数: USARTx:串口组
* str:要发送的数据
* len:数据长度
*
* 返回参数: 无
*
* 说明:
************************************************************
*/
void Usart_SendString(USART_TypeDef *USARTx, unsigned char *str, unsigned short len)
{
unsigned short count = 0;
for(; count < len; count++)
{
USART_SendData(USARTx, *str++); //发送数据
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); //等待发送完成
}
}
/*
************************************************************
* 函数名称: UsartPrintf
*
* 函数功能: 格式化打印
*
* 入口参数: USARTx:串口组
* fmt:不定长参
*
* 返回参数: 无
*
* 说明:
************************************************************
*/
void UsartPrintf(USART_TypeDef *USARTx, char *fmt,...)
{
unsigned char UsartPrintfBuf[296];
va_list ap;
unsigned char *pStr = UsartPrintfBuf;
va_start(ap, fmt);
vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); //格式化
va_end(ap);
while(*pStr != 0)
{
USART_SendData(USARTx, *pStr++);
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);
}
}
3.2 串口中断接收函数
/*
************************************************************
* 函数名称: USART1_IRQHandler
*
* 函数功能: 串口1收发中断
*
* 入口参数: 无
*
* 返回参数: 无
*
* 说明:在进行数据接收时,我们一帧一帧地接收,直到接收到我们需要的帧
* 后,将接收数据buffer的数据复制到我们提前定义好的接收数据结构体中
************************************************************
*/
void USART1_IRQHandler(void)
{
uint8_t reContent //存储接收内容
// 如果串口接收到内容
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;
}
}
}
3.4 数据帧的解析
存储到需要的帧信息后,下一步就是对它进行解析,解析时我们利用每一个数据间的逗号作为分隔符,利用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 useful!\r\n");
}
else if (receDataFrame.UsefullFlag[0] == 'V')
{
printf ("Data is invalid!\r\n");
}
}
else
{
printf ("Prase Errpr!\r\n"); // 解析错误
}
}
}
}
}
3.5数据转换
数据读取完后,对其进行解析。
/*
*==============================================================================
*函数名称:Data_Transfor
*函数功能:数据转换
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void Data_Transfor (void)
{
float latitude_temp1,latitude_temp2_min,latitude = 0, ; // 存储纬度信息
float longitude_temp1,longitude_temp2_min,longitude = 0; // 存储经度信息
latitude = strtod(receDataFrame.latitude,NULL); // 字符串转换成浮点数
longitude = strtod(receDataFrame.longitude,NULL); // 字符串转换成浮点数
// 纬度信息处理
latitude_temp1 = (uint16_t)latitude/100; //提取度-分中度的不分
latitude_temp2_min = (latitude%100 )/60 //提取度-分中,分的部分并将其转为度
latitude = (float)latitude_temp1 + latitude_temp2_min;
// 经度信息处理
// 五位经度信息
longitude_temp1 = (uint16_t)longitude/100; //提取度-分中度的不分
longitude_temp2_min = (longitude%100 )/60 //提取度-分中,分的部分并将其转为度
latitude = (float)longitude_temp1 + longitude_temp2_min;
}
4 参考资料
[1] 参考博文1: GPS基础知识+模组使用
[2] 参考博文2: 定位模块介绍及使用(GPS、北斗、GLONASS、伽利略、准天顶)
[3] 参考博文3: 嵌入式外设集 – GPS定位模块(ATGM336H)
[4] 参考博文4: 【STM32外设系列】GPS定位模块(ATGM336H)
5. 总结
以上即是本次的内容。如果单片机的功能较多的话,这个串口中断接收函数,可能会一直触发导致无法进行别的函数。推荐如果做功能较多的项目的话,可以用DMA搬运。这也是后续可改进的点。代码大部分来源于参考博文4。