news 2026/9/7 7:26:43

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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是GPS,GL是GLONASS,BD是北斗;后面三位是语句类型,GGA、RMC、GSV这些都算。校验和紧跟在*后面,是两个十六进制字符,内容是$*之间所有字符按位异或的结果。比如上面那条GGA,校验和就是$后面GPGGA,123519,...*之前每个字符异或的结果。

为什么要设计成ASCII文本而不是二进制?因为调试方便,一个串口助手就能直接看到数据,而且模块厂商之间兼容性也好。代价是每条语句有大量冗余字符,但对串口波特率不敏感的应用来说完全不是问题,9600波特率下传几条语句绰绰有余。

1.2 实际项目里最常用的几条语句

实际开发中真正高频用到的是GGA、RMC、GSV这几条,关键字段我整理成了一张表:

语句主要信息典型输出频率
GGA定位状态、经纬度、卫星数、HDOP、海拔1Hz~5Hz
RMC推荐最小数据,含时间、经纬度、速度、航向、日期1Hz
GSV可见卫星编号、仰角、方位角、信噪比随卫星数量变化
GSA参与定位卫星编号、PDOP/HDOP/VDOP、定位模式1Hz

GGA和RMC是解析器最优先支持的。GGA直接给定位质量和坐标,RMC带有速度和航向,做车载导航或者轨迹记录时基本靠这两条。

具体到字段编号,GGA里的位置大概是这样的:

$GPGGA,hhmmss.ss,ddmm.mmmm,N,dddmm.mmmm,W,定位状态,卫星数,HDOP,海拔,M,... 字段1 字段2 字段3 字段4 字段5 字段6 字段7 字段8 字段9

RMC的字段更紧凑,定位状态在第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 字段切分:尽量别用strtok

NMEA语句的字段分隔符是逗号,但有些字段为空,比如GGA里后面几个字段经常是空的。标准库的strtok会把连续分隔符当成一个处理,还会修改原字符串,在多线程环境或者还需要保留原始帧内容的地方容易出问题。

我自己的做法是写一个按分隔符索引的函数:给定帧字符串和字段编号,返回该字段起始指针和长度。拿到指针和长度后,要么复制到临时缓冲区,要么直接结合strtodatoi解析。这样源头清晰,也好调试。如果非要用标准库函数,至少记住用strtok_r而不是strtok,而且解析前先复制一份原字符串,别把缓冲区搞得面目全非。

3. 核心代码实现:一个可直接移植的GPS解析器

到这一步,我们已经能把一条完整的NMEA语句从串口流里拎出来了。接下来就是常规的字符串解析。我习惯把解析结果先收拢到一个结构体里,这样上层逻辑和底层协议彻底解耦。

3.1 先定义好输出结构体

typedef struct { int fix_quality; /* 0=无效, 1=GPS单点, 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.mmmmdddmm.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个字段,只有AV两种取值,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 定位无效时,返回的不是错误而是0

GPS模块刚上电或者在天线遮挡严重的室内,输出的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解析,每次重写的原因不是协议不懂,而是第一版总是把解析和业务逻辑耦合在一起,导致换个模块型号或者加个语句类型就要动一大片代码。现在我的习惯是先做纯解析库,用日志文件做自动化测试,再接到实际设备上,省心很多。如果你也在做类似的东西,建议从状态机成帧和字段索引解析这两块入手,先把这两块写稳,后面的功能基本都是在这个骨架上加肉。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 7:26:25

第5章 智能元素理论

第5章 智能元素理论&#x1f4c5; 2026年09月06日&#x1f464; 东塬一老翁&#x1f4c2; 第二篇 智能的结构基础第5章 智能元素理论5.1 智能元素定义**智能元素&#xff08;Intelligence Element&#xff09;**是构成智能结构、智能能力、智能机制和智能行为的基本结构单元…

作者头像 李华
网站建设 2026/9/7 7:25:38

基于IIO子系统的嵌入式Linux频谱示波器实现与调优

简介&#xff1a;这份资源是一款面向Linux平台的频谱示波器软件&#xff0c;基于C与GTK图形库开发&#xff0c;并依托Linux内核的IIO框架与信号采集设备通信&#xff0c;适用于电子工程、通信技术及信号处理等领域的研发与调试场景。包体约45.43MB&#xff0c;共776个文件&…

作者头像 李华
网站建设 2026/9/7 7:25:20

CANopen设备开发中的EDS文件与edsEditor使用指南

简介&#xff1a;CANopen edsEditor 是一套面向工业自动化与控制系统开发者的 CANopen 设备描述文件&#xff08;EDS&#xff09;编辑工具包。它主要用于创建、编辑和校验 EDS 文件&#xff0c;帮助设备制造商、系统集成商和终端用户以标准化方式描述设备硬件、软件特性、通信参…

作者头像 李华
网站建设 2026/9/7 7:24:57

Qt+QCustomPlot实时数据可视化动态曲线绘制与频谱分析工程详解

简介&#xff1a;这是一份基于Qt框架的动态曲线绘制完整示例工程&#xff0c;面向需要实现实时数据可视化的Qt开发者与学习者&#xff0c;演示如何借助QCustomPlot库在同一图表中绘制并动态更新多条曲线。压缩包体量轻巧&#xff0c;仅242KB&#xff0c;共9个文件&#xff0c;涵…

作者头像 李华
网站建设 2026/9/7 7:23:56

自动驾驶仿真测试场景设计:概念、方法与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华