ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GPS数据解析实战:NMEA 0183协议与C语言解析器实现

GPS数据解析实战:NMEA 0183协议与C语言解析器实现 简介一套完整的GPS数据解析C程序源码包面向嵌入式开发者和单片机爱好者解决GPS模块NMEA报文解析与12864液晶屏实时显示经纬度的问题。资源共23个文件以.C源码、.H头文件、.OBJ目标文件和.LST列表文件为主压缩包约72KB同时包含HEX烧录文件、UV2工程文件及备份文件便于直接编译、烧录与二次开发。程序由郭天祥开发代码规范实用涵盖UART串口接收、NMEA语句如GPGGA/GPGLL解析、逗号分隔字段提取、坐标字符串转浮点以及12864驱动显示等关键步骤并配有LCD和GPS模块的驱动接口。已有2196人学习下载适合希望掌握GPS数据解析、串口通信与嵌入式显示技术的开发者参考可快速迁移到物联网、定位终端、车载导航等实际项目中。1. GPS数据解析到底在解析什么NMEA 0183协议速览先说句实话很多人第一次拿到GPS模块以为串口直接吐一个经纬度数字出来。接上USB转串口一看收到的却是这么一串东西$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47这是NMEA 0183协议GPS行业最常用的一种数据输出格式。C程序要做的事就是把这串文本里逗号分隔的字段拆出来把经纬度、时间、速度、定位质量这些信息变成一个结构体交给上层算法或者界面去用。这个项目本身不大但涉及串口数据流、文本解析、校验算法、边界情况处理特别适合练手C语言基本功也适合要在嵌入式设备上接GPS模块的开发者直接抄作业。1.1 一条NMEA语句是怎么组成的NMEA 0183的语句都以$开头以\r\n结尾中间用逗号分隔。整体可以拆成四块起始符和地址域、数据字段、校验和、结束符。地址域前两位表示系统来源GP是GPSGL是GLONASSBD是北斗后面三位是语句类型GGA、RMC、GSV这些都算。校验和紧跟在*后面是两个十六进制字符内容是$和*之间所有字符按位异或的结果。比如上面那条GGA校验和就是$后面GPGGA,123519,...到*之前每个字符异或的结果。为什么要设计成ASCII文本而不是二进制因为调试方便一个串口助手就能直接看到数据而且模块厂商之间兼容性也好。代价是每条语句有大量冗余字符但对串口波特率不敏感的应用来说完全不是问题9600波特率下传几条语句绰绰有余。1.2 实际项目里最常用的几条语句实际开发中真正高频用到的是GGA、RMC、GSV这几条关键字段我整理成了一张表语句主要信息典型输出频率GGA定位状态、经纬度、卫星数、HDOP、海拔1Hz~5HzRMC推荐最小数据含时间、经纬度、速度、航向、日期1HzGSV可见卫星编号、仰角、方位角、信噪比随卫星数量变化GSA参与定位卫星编号、PDOP/HDOP/VDOP、定位模式1HzGGA和RMC是解析器最优先支持的。GGA直接给定位质量和坐标RMC带有速度和航向做车载导航或者轨迹记录时基本靠这两条。具体到字段编号GGA里的位置大概是这样的$GPGGA,hhmmss.ss,ddmm.mmmm,N,dddmm.mmmm,W,定位状态,卫星数,HDOP,海拔,M,... 字段1 字段2 字段3 字段4 字段5 字段6 字段7 字段8 字段9RMC的字段更紧凑定位状态在第2个字段经纬度在3到6字段速度是第7个字段航向是第8个日期是第9个。解析之前先把这些编号记牢写代码的时候就不用来回翻协议文档了。2. 解析程序的设计思路先把流程拆成三件大事写解析器最容易犯的错误是一上来就while(1)里等数据然后硬啃字符串。我的建议是把整个流程拆成三个阶段成帧、校验、字段提取。三个阶段各干各的事出了问题也好定位。2.1 成帧串口读到的字节流不是天然分好的串口驱动每次read返回的数据长度是随机的。可能一次读到半条语句也可能一次读到好几条黏在一起。所以第一件事不是找逗号而是先把一条完整的NMEA语句从字节流里切出来。我习惯用一个简单的状态机状态迁移是找$、接收数据、等*、读两个十六进制校验字符、等\r\n。代码写出来不复杂typedef enum { IDLE, RECV, SUM, END } frame_state_t; void gps_rx_byte(frame_state_t *st, char c, char *buf, int *len, unsigned char *sum) { switch (*st) { case IDLE: if (c $) { *st RECV; *len 0; *sum 0; buf[(*len)] c; } break; case RECV: if (c *) { *st SUM; } else { *sum ^ (unsigned char)c; if (*len MAX_FRAME_LEN - 1) { buf[(*len)] c; } else { *st IDLE; } } break; case SUM: /* 开始读两个十六进制校验字符这里需要另外保存半字节状态 */ *st END; break; case END: if (c \n) { /* 一帧完整数据可以交给解析模块 */ buf[*len] \0; } *st IDLE; break; } }状态机的好处是天然处理半包一个字节一个字节喂来多少收多少跟底层串口一次给多少字节没有任何关系。这个设计是整套程序里最值得花时间的部分。2.2 校验和必须自己算不能省这个环节看起来多余实际省不得。GPS模块在电磁环境差的时候可能出现误码尤其是串口线附近有电机驱动器在跑干扰一上来数据就花了。校验和算法很简单从$后面第一个字符到*之前逐个字符异或得到一个字节再跟星号后面的两个十六进制字符比对。逻辑上不复杂但有一个细节要注意别用strlen去计算长度。串口缓冲区里可能混入0x00这种不可见字符虽然正常NMEA是ASCII但一旦有脏数据strlen会在中间截断校验和永远对不上。按实际接收长度循环才是最稳妥的。2.3 字段切分尽量别用strtokNMEA语句的字段分隔符是逗号但有些字段为空比如GGA里后面几个字段经常是空的。标准库的strtok会把连续分隔符当成一个处理还会修改原字符串在多线程环境或者还需要保留原始帧内容的地方容易出问题。我自己的做法是写一个按分隔符索引的函数给定帧字符串和字段编号返回该字段起始指针和长度。拿到指针和长度后要么复制到临时缓冲区要么直接结合strtod、atoi解析。这样源头清晰也好调试。如果非要用标准库函数至少记住用strtok_r而不是strtok而且解析前先复制一份原字符串别把缓冲区搞得面目全非。3. 核心代码实现一个可直接移植的GPS解析器到这一步我们已经能把一条完整的NMEA语句从串口流里拎出来了。接下来就是常规的字符串解析。我习惯把解析结果先收拢到一个结构体里这样上层逻辑和底层协议彻底解耦。3.1 先定义好输出结构体typedef struct { int fix_quality; /* 0无效, 1GPS单点, 2差分 */ int satellites; /* 参与定位的卫星数 */ double latitude; /* 十进制度南纬为负 */ double longitude; /* 十进制度西经为负 */ double altitude; /* 海拔单位米 */ double hdop; /* 水平精度因子 */ double speed_knots; /* 速度节 */ double course; /* 对地航向度 */ int hour, minute, second; int day, month, year; /* UTC日期 */ } gps_info_t;有人喜欢把原始度分格式存下来等上层用的时候再换算。我的建议是解析层就把标准单位算好上层不应该关心NMEA格式细节。不然以后换模块、换协议版本上层代码全要跟着改得不偿失。3.2 GGA解析经纬度度分转换是重点GGA的第2、4个字段分别是纬度和经度格式是ddmm.mmmm和dddmm.mmmm。很多新手直接把atof结果当成十进制度数用结果坐标偏出去几十公里。转换成十进制度的公式是整数部分除以100得到度余下的小数部分是分最终度数 度 分 / 60。static double coord_to_decimal(const char *field, char dir) { double raw strtod(field, NULL); int deg (int)(raw / 100.0); double min raw - deg * 100.0; double dec deg min / 60.0; if (dir S || dir W) { dec -dec; } return dec; }南纬和西经取负号北纬东经保持正值。这个符号处理放在解析层上层拿到的就是带正负号的标准十进制度坐标。解析GGA时字段2是纬度数值字段3是纬度方向N/S字段4是经度数值字段5是经度方向E/W字段6是定位状态。核心逻辑大致是这样int gps_parse_gga(const char *frame, gps_info_t *info) { const char *lat get_field(frame, 2); const char *lat_dir get_field(frame, 3); const char *lon get_field(frame, 4); const char *lon_dir get_field(frame, 5); const char *fix get_field(frame, 6); if (!lat || !lat_dir || !lon || !lon_dir || !fix) { return -1; } info-fix_quality atoi(fix); info-latitude coord_to_decimal(lat, lat_dir[0]); info-longitude coord_to_decimal(lon, lon_dir[0]); return 0; }解析的时候尽量用strtod而不是sscanf(%f)因为sscanf遇到空字段的行为在不同平台上有差异而且经纬度字段可能为空字符串strtod会返回0.0配合定位状态标志位就能识别出无效数据。3.3 RMC解析速度和时间RMC语句字段少解析起来比GGA更简单。定位状态在第2个字段只有A和V两种取值A代表有效定位V代表无效。第7个字段是速度单位节第8个字段是航向单位度第9个字段是日期格式ddmmyy。int gps_parse_rmc(const char *frame, gps_info_t *info) { const char *status get_field(frame, 2); const char *speed get_field(frame, 7); const char *course get_field(frame, 8); if (!status || !speed || !course) { return -1; } if (status[0] A) { info-speed_knots strtod(speed, NULL); info-course strtod(course, NULL); } else { info-speed_knots 0.0; info-course 0.0; } return 0; }速度单位是节想转公里每小时就乘1.852想转米每秒就乘0.514444。时间字段是UTC如果设备要显示本地时间得按业务配置时区偏移别把UTC直接当本地时间显示否则车载屏幕上会差好几个小时。4. 实测现场的坑串口粘包、脏数据与定位无效代码写完之后真正开始联调才会碰到一堆文档外的问题。这一节的内容基本是我在不同项目里反复踩过的坑每一行都值得记下来。4.1 半包和粘包同时出现我实际调试时用USB转串口一次read下去缓冲区里塞了八条完整语句最后一条还缺了一半。这种情况非常常见。状态机一个字节一个字节处理就完全不怕如果是传统做法一次性收完再按\n找行遇到一条被拆成两次读的情况就麻烦了。稳妥的做法是每次read得到的字节都喂给状态机帧完整后立刻回调解析函数。另外要特别注意串口助手软件里能正常显示的语句不代表程序里就能完整收到。\r\n这个结尾是协议的一部分很多人在找行尾的时候只找\n结果把\r残留到了下一条数据前面折腾半天才发现。4.2 定位无效时返回的不是错误而是0GPS模块刚上电或者在天线遮挡严重的室内输出的GGA语句定位状态字段是0经纬度字段可能是空或者0.0。这种帧语法完全正常校验和也正确如果解析器不判断状态直接输出坐标上层画轨迹就会出现一堆(0,0)或者漂移点。我的做法是解析成功后先看fix_quality为0就不更新位置信息但可以继续统计卫星数。冷启动找星需要时间有时候要几十秒甚至几分钟程序里最好有对应的超时提示不然用户会以为设备坏了。4.3 常见问题速查表把平时最容易撞上的问题整理成了一张速查表排查方向基本都在里面现象可能原因解决思路校验和频繁失败波特率错误、串口线干扰、数据里混入非ASCII垃圾确认模块波特率用串口助手先看原始数据经纬度偏差几十公里级将ddmm.mmmm当纯角度解析按 度整数/100分小数最终度分/60 转换坐标一直是0,0定位状态为0未定位天线放窗边检查fix_quality等待冷启动完成程序偶发崩溃strtok破坏原字符串、数组越界改用字段索引长度解析前复制原字符串本地时间差好几个小时UTC时间未加时区偏移按业务配置偏移或只存UTC由上层处理速度明显不对节和公里每小时没换算明确内部单位在解析层统一换算特别是经纬度转换那条我在别人代码里见过不止三次同样的错误。这属于典型的看起来对实际差得很远的问题。5. 这个解析器后续怎么扩展从解析到定位应用解析器只解决数据入口问题真正要做车载导航、无人机、物流追踪后面还有一堆活。但解析层做得够不够干净直接决定后面费不费劲。5.1 把解析层做成独立模块建议把解析接口设计成输入完整帧、输出结构体int gps_parse_frame(const char *frame, gps_info_t *out);这样好处很明显不用依赖具体串口实现既能接真实硬件也能用一个文本文件里的NMEA日志回放来测试和复现问题。没有GPS模块时可以用GPS信号模拟器生成固定场景的卫星信号或者直接录一段设备日志反复喂给解析器做回归。把采集和解析分开是工程上很推荐的写法。我还习惯给解析器配上几组典型的测试用例一条完整GGA、一条带空字段的GGA、一条校验和错误的语句、一条只有半截的语句。每次都跑一遍防止改其他功能的时候把解析逻辑改坏。5.2 对接定位算法时的几个思路解析出干净的经纬度只是开始。如果要做轨迹平滑可以接卡尔曼滤波如果要根据多组距离做位置估计常用的是三边测量算法如果需要更高精度可以关注差分数据或者RTK。这些算法需要的数据入口往往就是gps_info_t这种结构体。另外现在很多模块输出多星座数据地址域会变成BD、GL、GA这些语句类型和字段顺序基本一致解析逻辑可以复用只需要把地址域判断改成更通用的形式。这个扩展点在设计结构体的时候就应该留好。最后分享一点个人体会我在好几个项目里都写过GPS解析每次重写的原因不是协议不懂而是第一版总是把解析和业务逻辑耦合在一起导致换个模块型号或者加个语句类型就要动一大片代码。现在我的习惯是先做纯解析库用日志文件做自动化测试再接到实际设备上省心很多。如果你也在做类似的东西建议从状态机成帧和字段索引解析这两块入手先把这两块写稳后面的功能基本都是在这个骨架上加肉。本文还有配套的精品资源点击获取
返回列表