1.摘要
GPS模块使用串口通信,那么它的的数据处理本质上还是串口通信处理,只是GPS模块的输出的有其特定的格式,需要字符串处理逻辑来解析其含义。如何高效的处理从GPS模块接收到的数据帧,是GPS驱动设计的重点,本文使用状态机的思想来处理GPS输出的串口数据流,相对于定时从串口环形bufer取数据包然后依次解析有更高的实时性并且单片机负荷更低。
2. GPS数据协议简介
常用的GPS模块大多采用NMEA-0183 协议,目前业已成了GPS导航设备统一的RTCM(Radio Technical Commission for Maritime services)标准协议。NMEA-0183 是美国国家海洋电子协会(National Marine Electronics Association)所指定的标准规格,这一标准制订所有航海电子仪器间的通讯标准,其中包含传输资料的格式以及传输资料的通讯协议。
GPS数据格式如下:
帧格式形如:$aaccc,ddd,ddd,„,ddd*hh(CR)(LF)
1、“$”:帧命令起始位
2、aaccc:地址域,前两位为识别符(aa),后三位为语句名(ccc)
3、ddd„ddd:数据
4、“*”:校验和前缀(也可以作为语句数据结束的标志)
5、hh:校验和,$与*之间所有字符ASCII码的校验和(各字节做异或运算,得到
校验和后,再转换16进制格式的ASCII字符)
6、(CR)(LF):帧结束,回车和换行符
可以从串口抓数据帧:
$GPGSV,2,2,08,21,15,076,,23,52,270,,26,50,050,,27,52,179,*7D
$GPRMC,132043.00,V,,,,,,,120116,,,N*7F
$GPGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*30
$GPRMC,133308.00,A,3949.63002,N,11616.48641,E,1.101,,120116,,,A*70
3. GPS状态机接收
一般的应用中我们最关心的数据是GPRMC,即推荐定位信息。我们常见GPS数据接收方法主要是串口中断法,串口中断一直开着,然后定时从中断中取一包数据,解析这包数据,找到定位信息,常见的主要是找到GPRMC帧。
这有几个问题。
1. 一般而言,中断数据很快,而数据处理过程会发生丢接收中断。
2. 为了避免丢数据,可以使用双buffer来处理,读数据时候就把这个buffer锁定,然后再来了中断数据就往另外一个buffer放。
3. 如果取数据时刻刚好是一个有效定位信息,那么切换到第二个buffer后,就导致一条帧分成2份,两个buffer中该数据帧都不完整。
4.代码结构不清晰,应用层收到的数据可能不是完整的一条帧。
基于以上几点,改进方法使用状态机来接收,可以每次完整的给应用层发送一帧数据。
并且不需要关闭串口中断,接收过程一直进行。
接收到一个完整帧后就往上层送一次,上层负责解释数据的含义。
下面是在stm32平台上的接收GPS数据的处理过程。
//gps receive gps_state machine.
#define Start 0// $
#define G 1
#define P 2
#define R 3
#define M 4
#define C 5
#define Data 6
#define Check0 7 // *
#define Check1 8// *
void UART4_IRQHandler(void)
{
static uint8_t len = ;
static uint8_t crc = ;
static GPS_MSG_T GpsMsg;
uint8_t data = ;
uint8_t tmp_flg = ;
//$GPRMC,144601.00,A,3916.72973,N,11706.60267,E,0.719,,180117,,,A*76
if (USART_GetITStatus(GPS_UART, USART_IT_RXNE) != RESET)
{
data = (uint8_t)USART_ReceiveData(GPS_UART); switch(gps_state) // find GPRMC
{
case Start:
if(data == '$') {
gps_state = G;
len = ;
GpsMsg.maxLen = MaxGPSMsgLen;
memset(GpsMsg.buffer, , GpsMsg.maxLen);
GpsMsg.buffer[len++] = data;
crc = ;
}
else gps_state = Start;
break;
case G:
if(data == 'G'){
gps_state = P;
GpsMsg.buffer[len++] = data;
crc ^= data;
}
else gps_state = Start;
break;
case P:
if(data == 'P'){
gps_state = R;
GpsMsg.buffer[len++] = data;
crc ^= data;
}
else gps_state = Start;
break;
case R:
if(data == 'R'){
gps_state = M;
GpsMsg.buffer[len++] = data;
crc ^= data;
}
else gps_state = Start;
break;
case M:
if(data == 'M'){
gps_state = C;
GpsMsg.buffer[len++] = data;
crc ^= data;
}
else gps_state = Start;
break;
case C:
if(data == 'C'){
gps_state = Data;
GpsMsg.buffer[len++] = data;
crc ^= data;
}
else gps_state = Start;
break;
case Data:
if(data == '*'){
gps_state = Check0;
GpsMsg.buffer[len++] = data;
}
else{
gps_state = Data;
GpsMsg.buffer[len++] = data;
crc ^= data;
if(len>GpsMsg.maxLen) gps_state = Start;
}
break; case Check0:
gps_state = Check1;
GpsMsg.buffer[len++] = data;
break;
case Check1: //*hh
gps_state = Start;
GpsMsg.buffer[len++] = data;
if(crc == ((GpsMsg.buffer[len-]-'')* + (GpsMsg.buffer[len-]-'')))
{
GpsMsg.buffer[len++] = '\r';
GpsMsg.buffer[len] = '\n';
GpsMsg.length = len;
//send to gps task
xQueueSendFromISR(GpsQueue, (void *) &GpsMsg, );
}
} USART_ClearITPendingBit(GPS_UART, USART_IT_RXNE);
}
}
这段代码完成4个功能。1)串口接收数据;2)状态机切换;3)数据校验;4)把通过校验的数据发给应用层。
4. GPS数据解析
应用层已经收到数据了,剩下的工作就是字符串解析了。如果只关注GPRMC信息的话,上面已经做了校验,出错的概率极小,那么应用层就可以直接从收到的数据帧里提取经纬度了。
如果希望数据全部都处理,那么在串口接收部分就不能只保留GPRMC信息,应该全部都保留然后发给应用层,应用层解析数据帧。这里给出一个开源的例子,其中使用了多个c标准库字符处理函数,优点是通用性强功能完备,当然在嵌入式中可能比较占内存,如果资源紧张可以自己写该部分处理逻辑。
/*! \file tok.h */ //#include "nmea/tok.h"
#include "tok.h"
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <limits.h>
//#include "config.h" #define NMEA_TOKS_COMPARE (1)
#define NMEA_TOKS_PERCENT (2)
#define NMEA_TOKS_WIDTH (3)
#define NMEA_TOKS_TYPE (4) /**
* \brief Calculate control sum of binary buffer
*/
int nmea_calc_crc(const char *buff, int buff_sz)
{
int chsum = ,
it; for(it = ; it < buff_sz; ++it)
chsum ^= (int)buff[it]; return chsum;
} /**
* \brief Convert string to number
*/
int nmea_atoi(const char *str, int str_sz, int radix)
{
char *tmp_ptr;
char buff[NMEA_CONVSTR_BUF];
int res = ; if(str_sz < NMEA_CONVSTR_BUF)
{
memcpy(&buff[], str, str_sz);
buff[str_sz] = '\0';
res = strtol(&buff[], &tmp_ptr, radix);
} return res;
} /**
* \brief Convert string to fraction number
*/
double nmea_atof(const char *str, int str_sz)
{
char *tmp_ptr;
char buff[NMEA_CONVSTR_BUF];
double res = ; if(str_sz < NMEA_CONVSTR_BUF)
{
memcpy(&buff[], str, str_sz);
buff[str_sz] = '\0';
res = strtod(&buff[], &tmp_ptr);
} return res;
} /**
* \brief Analyse string (specificate for NMEA sentences)
*/
int nmea_scanf(const char *buff, int buff_sz, const char *format, ...)
{
const char *beg_tok;
const char *end_buf = buff + buff_sz; va_list arg_ptr;
int tok_type = NMEA_TOKS_COMPARE;
int width = ;
const char *beg_fmt = ;
int snum = , unum = ; int tok_count = ;
void *parg_target; va_start(arg_ptr, format); for(; *format && buff < end_buf; ++format)
{
switch(tok_type)
{
case NMEA_TOKS_COMPARE:
if('%' == *format)
tok_type = NMEA_TOKS_PERCENT;
else if(*buff++ != *format)
goto fail;
break;
case NMEA_TOKS_PERCENT:
width = ;
beg_fmt = format;
tok_type = NMEA_TOKS_WIDTH;
case NMEA_TOKS_WIDTH:
if(isdigit(*format))
break;
{
tok_type = NMEA_TOKS_TYPE;
if(format > beg_fmt)
width = nmea_atoi(beg_fmt, (int)(format - beg_fmt), );
}
case NMEA_TOKS_TYPE:
beg_tok = buff; if(!width && ('c' == *format || 'C' == *format) && *buff != format[])
width = ; if(width)
{
if(buff + width <= end_buf)
buff += width;
else
goto fail;
}
else
{
if(!format[] || ( == (buff = (char *)memchr(buff, format[], end_buf - buff))))
buff = end_buf;
} if(buff > end_buf)
goto fail; tok_type = NMEA_TOKS_COMPARE;
tok_count++; parg_target = ; width = (int)(buff - beg_tok); switch(*format)
{
case 'c':
case 'C':
parg_target = (void *)va_arg(arg_ptr, char *);
if(width && != (parg_target))
*((char *)parg_target) = *beg_tok;
break;
case 's':
case 'S':
parg_target = (void *)va_arg(arg_ptr, char *);
if(width && != (parg_target))
{
memcpy(parg_target, beg_tok, width);
((char *)parg_target)[width] = '\0';
}
break;
case 'f':
case 'g':
case 'G':
case 'e':
case 'E':
parg_target = (void *)va_arg(arg_ptr, double *);
if(width && != (parg_target))
*((double *)parg_target) = nmea_atof(beg_tok, width);
break;
}; if(parg_target)
break;
if( == (parg_target = (void *)va_arg(arg_ptr, int *)))
break;
if(!width)
break; switch(*format)
{
case 'd':
case 'i':
snum = nmea_atoi(beg_tok, width, );
memcpy(parg_target, &snum, sizeof(int));
break;
case 'u':
unum = nmea_atoi(beg_tok, width, );
memcpy(parg_target, &unum, sizeof(unsigned int));
break;
case 'x':
case 'X':
unum = nmea_atoi(beg_tok, width, );
memcpy(parg_target, &unum, sizeof(unsigned int));
break;
case 'o':
unum = nmea_atoi(beg_tok, width, );
memcpy(parg_target, &unum, sizeof(unsigned int));
break;
default:
goto fail;
}; break;
};
} fail: va_end(arg_ptr); return tok_count;
}
#ifndef __TOK_H__
#define __TOK_H__ //#include "config.h" #ifdef __cplusplus
extern "C" {
#endif #define NMEA_CONVSTR_BUF (256) int nmea_calc_crc(const char *buff, int buff_sz);
int nmea_atoi(const char *str, int str_sz, int radix);
double nmea_atof(const char *str, int str_sz);
int nmea_printf(char *buff, int buff_sz, const char *format, ...);
int nmea_scanf(const char *buff, int buff_sz, const char *format, ...); #ifdef __cplusplus
}
#endif #endif /* __NMEA_TOK_H__ */