gaonaiweng 发表于 2022-6-29 23:22

NMEA(xxGGA)报文解析(FPGA实现)

最近接触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>


gaonaiweng 发表于 2022-6-29 23:33

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

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’表示以 米 为单位

gaonaiweng 发表于 2022-6-29 23:35

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

gaonaiweng 发表于 2022-6-29 23:39

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

gaonaiweng 发表于 2022-6-29 23:40

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

xxGGA报文解析模块:

gaonaiweng 发表于 2022-6-29 23:41

/******************************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                        rddat_toUart,
       
        output        reg                 rx_done,                //接收指令结束,上升沿对齐'\n'字符出现时刻,下降沿对齐$xxGGA后面的','的出现时刻的下一时刻
       
        output        reg                hh,                                //UTC时间,整数,时0~24
        output        reg                mm,                                //UTC时间,整数,分0~59
        output        reg                ss,                                //UTC时间,整数,秒0~59
        output        reg                ss2,                        //UTC时间,小数,秒2位小数,0~99
       
        output        reg                lat,                        //纬度整数部分,度0~90
        output        reg                lat2,                        //纬度整数部分,分0~59
        output        reg                lat3,                        //纬度小数部分,分5位小数,0~99999
        output        reg                        NS,                                //区分南北纬,北纬标为1,南纬标为0
       
        output        reg                lon,                        //经度整数部分,度0~180
        output        reg                lon2,                        //经度整数部分,分0~59
        output        reg                lon3,                        //经度小数部分,分5位小数,0~99999
        output        reg                        EW,                                //区分东西经,东经标为1,西经标为0
       
        output        reg         alt,                        //海拔整数部分,m
        output        reg         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                        cntField;                //当前读取第几个域,以','分隔
reg                 cntChar;                //当前读取域中的第几个字符

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

wire                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***************************************************

gaonaiweng 发表于 2022-6-29 23:42

字符-数字转换模块:
/******************************FILE HEAD**********************************
* file_name         : Char2Num.v
* function          : 若Char为字符0~9,将之转化为数字0~9
* author            : 今朝无言
* version & date    : 2021/10/14 & v1.0
*************************************************************************/
module Char2Num(
        input                         Char,
        output                        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***************************************************

gaonaiweng 发表于 2022-6-29 23:42

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        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                        rddat_toUart;

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

wire                hh;                                //UTC时间,整数,时0~24
wire                mm;                                //UTC时间,整数,分0~59
wire                ss;                                //UTC时间,整数,秒0~59
wire                ss2;                        //UTC时间,小数,秒2位小数,0~99

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

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

wire                alt;                        //海拔整数部分,m
wire                alt2;                        //海拔小数部分,一位小数0~9

reg                        i;
initial begin
        rx_done_toUart        <= 0;
        #50;
       
        for(i=0;i<=74*8;i=i+8)begin
                rddat_toUart        <= {data,data,data,data,
                                                        data,data,data,data};
                #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***************************************************

gaonaiweng 发表于 2022-6-29 23:43

ModelSim仿真结果:

gaonaiweng 发表于 2022-6-29 23:45

页: [1]
查看完整版本: NMEA(xxGGA)报文解析(FPGA实现)