最近接触GPS,需要使用FPGA进行NMEA报文的解析,以获得经纬度和时间信息,我选用的报文是xxGGA,包含GPGGA(GPS系统的)、GBGGA(北斗系统的)、GLGGA(GLONASS系统的)、GAGGA(伽利略系统的),GNGGA(任意GNSS系统组合)。他们的格式完全相同,不同之处仅在于报文头,xxGGA报文格式如下

$xxGGA,time,lat,NS,lon,EW,quality,numSV,HDOP,alt,altUnit,sep,sepUnit,diffAge,diffStation*cs<CR><LF>

我们需要关注的数据域如下

time:UTC时间,hhmmss.ss格式,如132253.27表示UTC时间 13时22分53.27秒,需要注意的是.ss表示秒的小数域(2位小数),而非毫秒

lat:纬度,ddmm.mmmm格式,如3124.73251表示 31度24.73251分,1度=60分

NS:指示南北半球,北半球为‘N’,南半球为‘S’

lon:经度,dddmm.mmmmm格式,如13424.73251表示 134度24.73251分

EW:指示东西半球,东半球为‘E’,西半球为‘W’

alt:海拔,-9999.9~9999.9

altUnit:海拔单位,‘M’表示以 米 为单位

例如:

$GPGGA,092725.00,4717.11399,N,00833.91590,E,1,08,1.01,499.6,M,48.0,M,,*5B

表示 UTC时间 09 时 27 分 25.00 秒,北纬 47 度 17.11399 分,东经 8 度 33.91590 分,海拔 499.6 米

------------------------------------------------------------------------------分割线---------------------------------------------------------------------------------------------

本xxGGA解析模块使用UART串口数据,以UART模块发出的rx_done信号驱动。

xxGGA报文解析模块:

/******************************FILE HEAD**********************************
 * file_name         : parseGGA.v
 * function          : 解析xxGGA报文,获取UTC时间、经纬度、海拔
 * author            : 今朝无言
 * version & date    : 2021/10/14 & v1.0
 *************************************************************************/
module parseGGA(
	input				rx_done_toUart,	//整个模块由rx_done_toUart驱动
	input		[7:0]	rddat_toUart,
	
	output	reg 		rx_done,		//接收指令结束,上升沿对齐'\n'字符出现时刻,下降沿对齐$xxGGA后面的','的出现时刻的下一时刻
	
	output	reg	[4:0]	hh,				//UTC时间,整数,时  0~24
	output	reg	[5:0]	mm,				//UTC时间,整数,分  0~59
	output	reg	[5:0]	ss,				//UTC时间,整数,秒  0~59
	output	reg	[6:0]	ss2,			//UTC时间,小数,秒  2位小数,0~99
	
	output	reg	[6:0]	lat,			//纬度  整数部分,度  0~90
	output	reg	[5:0]	lat2,			//纬度  整数部分,分  0~59
	output	reg	[16:0]	lat3,			//纬度  小数部分,分  5位小数,0~99999
	output	reg			NS,				//区分南北纬,北纬标为1,南纬标为0
	
	output	reg	[7:0]	lon,			//经度  整数部分,度  0~180
	output	reg	[5:0]	lon2,			//经度  整数部分,分  0~59
	output	reg	[16:0]	lon3,			//经度  小数部分,分  5位小数,0~99999
	output	reg			EW,				//区分东西经,东经标为1,西经标为0
	
	output	reg [13:0]	alt,			//海拔  整数部分,m
	output	reg [3:0]	alt2			//海拔  小数部分,一位小数  0~9
);
//xxGGA格式: $xxGGA,time,lat,NS,lon,EW,quality,numSV,HDOP,alt,altUnit,sep,sepUnit,diffAge,diffStation*cs<CR><LF>
//time格式: hhmmss.ss
//lat格式: ddmm.mmmmm
//lon格式: dddmm.mmmmm
//alt格式: numeric,一位小数

reg		[4:0]	cntField;		//当前读取第几个域,以','分隔
reg 	[3:0]	cntChar;		//当前读取域中的第几个字符

reg				start	= 1'b0;	//NMEA报文的接收标志,以$开始,到\n结束
reg		[7:0]	charBuffer;
reg		[1:0]	corrNum	= 2'd0;	//比对是否为xxGGA,当corrNum=3时,表示"GGA"字符通过测试,该条报文即xxGGA

wire	[3:0]	num;			//若charBuffer为字符0~9,则将之转换为数字0~9
wire			isnum;

reg				afterDot;		//判断是否是"."后面的数字,在解析海拔时用到

//将字符0~9转换为数字0~9
Char2Num Char2Num_inst(
	.Char(charBuffer),
	.Num(num),
	.isNum(isnum)
);

always @(posedge rx_done_toUart) begin
	charBuffer	<= rddat_toUart;

	//-----------------------接收NMEA报文数据-----------------------------
	if(rddat_toUart == "$") begin			//接收到$,标志着NMEA数据的起始
		start		<= 1'b1;
		cntField	<= 5'd0;
		cntChar		<= 4'd0;
		corrNum		<= 2'd0;
	end
	else if(start) begin
		if(rddat_toUart == "\n") begin		//收到\n,标志NMEA报文结束
			start		<= 1'b0;
			rx_done		<= 1'b1;
		end
		else if(rddat_toUart == "," || 
				rddat_toUart == "*") begin	//收到','或'*',为域的分隔符
			cntField	<= cntField + 1'b1;
			cntChar		<= 4'd0;
		end
		else begin							//收到其他字符
			cntChar		<= cntChar + 1'b1;
		end
	end
	else begin
		start		<= 1'b0;
		cntField	<= 5'd0;
		cntChar		<= 4'd0;
		corrNum		<= 2'd0;
	end
	
	//------------------------判断是否为xxGGA----------------------------
	if(cntField == 5'd0) begin
		if(cntChar == 4'd3 && charBuffer == "G") begin
			corrNum <= corrNum + 1'b1;
		end
		else if(cntChar == 4'd4 && charBuffer == "G") begin
			corrNum <= corrNum + 1'b1;
		end
		else if(cntChar == 4'd5 && charBuffer == "A") begin
			corrNum <= corrNum + 1'b1;
		end
	end
	if(corrNum == 2'd3) begin	//检测到是"xxGGA",开启解析
		rx_done <= 1'b0;
		corrNum	<= 2'b0;
	end
	
	//---------------------------解析xxGGA------------------------------
	if(rx_done == 1'b0) begin
		//解析UTC时间
		if(cntField == 5'd1) begin
			if(cntChar == 4'd1) begin		//UTC-hh
				hh	<= num*4'd10;
			end
			else if(cntChar == 4'd2) begin
				hh	<= hh + num;
			end
			else if(cntChar == 4'd3) begin	//UTC-mm
				mm	<= num*4'd10;
			end
			else if(cntChar == 4'd4) begin
				mm	<= mm + num;
			end
			else if(cntChar == 4'd5) begin	//UTC-ss
				ss	<= num*4'd10;
			end
			else if(cntChar == 4'd6) begin
				ss	<= ss + num;
			end
			else if(cntChar == 4'd8) begin	//UTC-.ss
				ss2	<= num*4'd10;
			end
			else if(cntChar == 4'd9) begin
				ss2	<= ss2 + num;
			end
		end
		
		//解析纬度
		if(cntField == 5'd2) begin
			if(cntChar == 4'd1) begin		//lat-dd
				lat		<= num*4'd10;
			end
			else if(cntChar == 4'd2) begin
				lat		<= lat + num;
			end
			else if(cntChar == 4'd3) begin	//lat-mm
				lat2	<= num*4'd10;
			end
			else if(cntChar == 4'd4) begin
				lat2	<= lat2 + num;
			end
			else if(cntChar == 4'd6) begin	//lat-.mmmmm
				lat3	<= num;
			end
			else if(cntChar == 4'd7 || cntChar == 4'd8 ||
					cntChar == 4'd9 || cntChar == 4'd10) begin
				lat3	<= lat3*4'd10 + num;
			end
		end
		if(cntField == 5'd3 && cntChar == 4'd1) begin	//NS
			if(charBuffer == "N") begin
				NS	<= 1'b1;
			end
			else begin
				NS	<= 1'b0;
			end
		end
		
		//解析经度
		if(cntField == 5'd4) begin
			if(cntChar == 4'd1) begin		//lon-ddd
				lon		<= num;
			end
			else if(cntChar == 4'd2 || cntChar == 4'd3) begin
				lon		<= lon*4'd10 + num;
			end
			else if(cntChar == 4'd4) begin	//lon-mm
				lon2	<= num*4'd10;
			end
			else if(cntChar == 4'd5) begin
				lon2	<= lon2 + num;
			end
			else if(cntChar == 4'd7) begin	//lon-.mmmmm
				lon3	<= num;
			end
			else if(cntChar == 4'd8 || cntChar == 4'd9 ||
					cntChar == 4'd10 || cntChar == 4'd11) begin
				lon3	<= lon3*4'd10 + num;
			end
		end
		if(cntField == 5'd5 && cntChar == 4'd1) begin	//EW
			if(charBuffer == "E") begin
				EW	<= 1'b1;
			end
			else begin
				EW	<= 1'b0;
			end
		end
		
		//解析海拔
		if(cntField == 5'd9) begin
			if(cntChar == 4'd1) begin
				alt			<= num;
				afterDot	<= 1'b0;
			end
			else if(charBuffer==".") begin
				afterDot	<= 1'b1;
				alt2		<= 4'd0;
			end
			else begin
				if(~afterDot) begin
					alt		<= alt*4'd10 + num;		//alt-MMM
				end
				else begin
					alt2	<= alt2*4'd10 +num;		//alt-.M
				end
			end
		end
	end
end

endmodule
//END OF parseGGA.v FILE***************************************************

字符-数字转换模块:

/******************************FILE HEAD**********************************
 * file_name         : Char2Num.v
 * function          : 若Char为字符0~9,将之转化为数字0~9
 * author            : 今朝无言
 * version & date    : 2021/10/14 & v1.0
 *************************************************************************/
module Char2Num(
	input 		[7:0]	Char,
	output		[3:0]	Num,
	output	reg			isNum
);

always@(*)begin
	case(Char)
		"0": isNum <= 1;
		"1": isNum <= 1;
		"2": isNum <= 1;
		"3": isNum <= 1;
		"4": isNum <= 1;
		"5": isNum <= 1;
		"6": isNum <= 1;
		"7": isNum <= 1;
		"8": isNum <= 1;
		"9": isNum <= 1;
		default: isNum <= 0;
	endcase
end

assign Num = isNum? Char - "0" : 4'hff;

endmodule
//END OF Char2Num.v FILE***************************************************

testbench:

/******************************FILE HEAD**********************************
 * file_name         : parseGGA_tb.v
 * function          : 解析xxGGA报文,获取UTC时间、经纬度、海拔
 * author            : 今朝无言
 * version & date    : 2021/10/14 & v1.0
 *************************************************************************/
`default_nettype none
`timescale 1ns/1ps

module parseGGA_tb;

reg	[0:75*8-1]data = {"$GNGGA,092725.00,4717.11399,N,00833.91590,E,1,08,1.01,499.6,M,48.0,M,,*5B",8'd13,8'd10}; //\r\n, \r=13,\n=10

reg				rx_done_toUart;	//整个模块由rx_done_toUart驱动
reg		[7:0]	rddat_toUart;

wire 			rx_done;		//接收指令结束,上升沿对齐'\n'字符出现时刻,下降沿对齐$xxGGA后面的','的出现时刻

wire	[4:0]	hh;				//UTC时间,整数,时  0~24
wire	[5:0]	mm;				//UTC时间,整数,分  0~59
wire	[5:0]	ss;				//UTC时间,整数,秒  0~59
wire	[6:0]	ss2;			//UTC时间,小数,秒  2位小数,0~99

wire	[6:0]	lat;			//纬度  整数部分,度  0~90
wire	[5:0]	lat2;			//纬度  整数部分,分  0~59
wire	[16:0]	lat3;			//纬度  小数部分,分  5位小数,0~99999
wire			NS;				//区分南北纬,北纬标为1,南纬标为0

wire	[7:0]	lon;			//经度  整数部分,度  0~180
wire	[5:0]	lon2;			//经度  整数部分,分  0~59
wire	[16:0]	lon3;			//经度  小数部分,分  5位小数,0~99999
wire			EW;				//区分东西经,东经标为1,西经标为0

wire	[13:0]	alt;			//海拔  整数部分,m
wire	[3:0]	alt2;			//海拔  小数部分,一位小数  0~9

reg		[9:0]	i;
initial begin
	rx_done_toUart	<= 0;
	#50;
	
	for(i=0;i<=74*8;i=i+8)begin
		rddat_toUart	<= {data[i],data[i+1],data[i+2],data[i+3],
							data[i+4],data[i+5],data[i+6],data[i+7]};
		#5;
		rx_done_toUart	<= 1;
		#50;
		rx_done_toUart	<=0;
		#50;
	end
	
	#200;
	$stop;
end

//解析xxGGA报文
parseGGA parseGGA_inst(
	.rx_done_toUart	(rx_done_toUart),
	.rddat_toUart	(rddat_toUart),
	.rx_done		(rx_done),
	.hh				(hh),
	.mm				(mm),
	.ss				(ss),
	.ss2			(ss2),
	.lat			(lat),
	.lat2			(lat2),
	.lat3			(lat3),
	.NS				(NS),
	.lon			(lon),
	.lon2			(lon2),
	.lon3			(lon3),
	.EW				(EW),
	.alt			(alt),
	.alt2			(alt2)
);

endmodule
//END OF parseGGA_tb.v FILE***************************************************

ModelSim仿真结果:

N3 gtp echo报文 gga报文格式_N3 gtp echo报文