简介:面向GPS原始数据采集与解析入门者的完整工程包,涵盖硬件采集、协议解析与简单分析,适合嵌入式、物联网及测绘相关专业学生用作课程设计或自学实践。资源共32个文件,以C源文件、头文件、Keil工程文件(.uvproj)、编译中间文件、hex固件以及GPS原始数据文本为主,压缩包整体约91KB,目录结构清晰,便于定位代码与数据。工程包含GPS.c、main.c及LCD12864/LCD1602驱动,可学习NMEA语句解析、经纬度与速度提取、LCD显示等关键环节,并附有从通电不完整到稳定后的GPS原始数据记录,便于对照调试。已有665人学习下载,适合需要快速上手GPS数据处理、完成单片机显示项目或入门定位技术的学习者。
1. GPS原始数据捕获:从串口乱码到看得懂的定位结果
GPS原始数据捕获这件事,第一个坑往往不是硬件,而是你根本不知道手里的模块吐出来的东西对不对。很多刚接触GPS开发的人,把模块接到单片机串口上,打开串口助手看到满屏$GPRMC开头、逗号连成一片的ASCII字符串,第一反应是“这什么东西”,第二反应是拿百度翻译逐词查。这套资源解决的就是这个问题:它不只是一份源码,而是一整套“从GPS模块通电、到串口输出NMEA语句、再到单片机解析并在LCD上显示经纬度和速度”的最小可复现工程。适合三类人:正在做单片机定位课设的学生、想在嵌入式设备里加定位功能的开发者、以及被“GPS数据怎么解析”卡住的入门工程师。整份工程基于Keil C51编写,同时驱动LCD1602和LCD12864两种屏幕,还附带两份通电前后的原始日志,能直接对照着看数据从残缺到完整的全过程。下面按我从拿到压缩包到跑通整个流程的顺序,把这套工程拆开讲透。
2. NMEA协议拆解:读懂GPGGA与GPRMC里的每个字段
2.1 GPS模块输出什么:NMEA-0183协议,而不是经纬度
绝大多数GPS模块,上电后输出的都是NMEA-0183协议的ASCII语句。有经验的工程师拿到模块的第一件事不是看屏幕,而是先弄清楚模块输出的是哪种NMEA语句、频率是多少、类型有哪些。这套资源里抓到的原始数据以$GPGGA、$GPRMC、$GPGLL为主,其中GPRMC是最“值钱”的一条——它一次包含了定位状态、经纬度、速度、航向和UTC时间,做简单分析时一条就够用。
常见做法是:模块默认9600波特率、每秒输出一次,每次输出一组多行NMEA帧。不要试图把每一帧都存下来分析,那对单片机来说负担重而且没必要。需要的只有两条:GPGGA用来评估定位质量(可以看到卫星数和海拔),GPRMC用来拿坐标和速度。你要做的解析工作,本质上就是从一长串以$开头、\r\n结尾的文本行里,按逗号切出你想要的位置。
2.2 GPGGA帧逐字段解析:定位质量与卫星数的来源
先看一条典型的GPGGA语句,这是从资源附带的txt日志里截出来的:
$GPGGA,092204.000,3102.4567,N,12112.3456,E,1,08,1.2,12.3,M,4.5,M,,*5E按逗号拆分后,各字段的含义如下表。这条语句一直在做的其实是“把你的三维位置和精度打包成文本发出来”。
| 字段位置 | 示例值 | 含义 | 说明 |
|---|---|---|---|
| 0 | $GPGGA | 语句头 | GGA = 全球定位系统固定数据 |
| 1 | 092204.000 | UTC时间 | 时:分:秒.毫秒,注意是UTC不是北京时 |
| 2 | 3102.4567 | 纬度 | 格式是ddmm.mmmm,31°02.4567′ |
| 3 | N | 北纬/南纬 | N或S |
| 4 | 12112.3456 | 经度 | 格式是dddmm.mmmm,121°12.3456′ |
| 5 | E | 东经/西经 | E或W |
| 6 | 1 | 定位状态 | 0=不可用 1=非差分定位 2=差分定位 |
| 7 | 08 | 可见卫星数 | 越多定位越稳 |
| 8 | 1.2 | HDOP | 水平精度因子,越小越好 |
| 9 | 12.3 | 海拔 | 单位米 |
| 10 | M | 海拔单位 | 固定为M |
这里最容易做错的一件事是直接把字符串里的纬度“3102.4567”当成31.024567度去计算速度。不是的,NMEA的纬度是“度分”混合格式,逗号前的整数部分是“度分合并值”,必须拆开换算成十进制度才能用于距离或速度计算,后面第4章会专门讲。
2.3 GPRMC帧:速度方向加日期,一张表说清
GPRMC被称为“推荐最小定位信息”,对大多数应用场景来说它就是全部。示例:
$GPRMC,092204.000,A,3102.4567,N,12112.3456,E,000.5,240.3,180123,,,A*6A按逗号拆分,核心字段比GPGGA多了速度、航向和日期:
| 字段位置 | 示例值 | 含义 |
|---|---|---|
| 0 | $GPRMC | 语句头 |
| 1 | 092204.000 | UTC时间 |
| 2 | A | 定位状态,A=有效,V=无效 |
| 3 | 3102.4567 | 纬度(度分格式) |
| 4 | N | 北纬 |
| 5 | 12112.3456 | 经度(度分格式) |
| 6 | E | 东经 |
| 7 | 000.5 | 速度,单位是节(海里/小时) |
| 8 | 240.3 | 航向,单位是度,正北为零顺时针 |
| 9 | 180123 | UTC日期,18年01月23日 |
注意第七个字段是“节”,不是公里每小时。换算公式是:km/h = 节 × 1.852。这算是这个资源里最不起眼但最常见的换算坑——后面计算速度的时候,如果直接拿这个值当km/h用,得到的速度会偏小近一半。
3. 单片机串口捕获:从硬件接线到工程跑通
3.1 硬件接线的四个关键点:电平、共地、电源、天线
这套资源的核心代码是GPS.c和main.c,运行在51内核单片机上。硬件层面的接线不复杂,但新手常在四个地方翻车。
第一是电平匹配。GPS模块的串口输出是TTL电平(0~3.3V或0~5V),单片机IO也是TTL,两者可以直接连。但如果你用USB转TTL工具直接连电脑看数据,和用单片机连模块,是两条完全不同的路径,不要混着接。第二是必须共地。GPS模块的GND和单片机的GND要接在一起,否则串口波形参考电平不一致,表现就是乱码或者彻底无数据。第三是电源要干净。GPS模块对电源纹波敏感,最好单独给它供电,不要和电机、继电器共用一组电源。第四是天线位置。模块的天线必须朝上且靠近窗口,室内实测经常出现定位状态一直为V的情况。
资源里main.c的串口配置代码如下:
// 串口1初始化:9600波特率,8位数据,1位停止位 void UART_Init(void) { SCON = 0x50; // 串口模式1:10位异步收发(8位数据+1起始+1停止) TMOD &= 0x0F; // 定时器1模式置0,不影响定时器0 TMOD |= 0x20; // 定时器1工作在方式2:8位自动重装 TH1 = 0xFD; // 9600波特率重装值,晶振11.0592MHz TL1 = 0xFD; TR1 = 1; // 启动定时器1 ES = 1; // 打开串口中断 EA = 1; // 打开总中断 }这段代码的逻辑核心是:51单片机的串口波特率由定时器1的溢出率决定,方式2下TL1自动重装,所以只要把TH1和TL1同时设为0xFD,在11.0592MHz晶振下就能得到稳定的9600波特率。这里有个参数你要格外注意:0xFD这个值只对11.0592MHz晶振成立,如果板子上是12MHz晶振,波特率会偏到约10417bps,接收端就会出现错位乱码——这个问题后面避坑章还会重点说。
3.2 中断接收:为什么不能用主循环轮询接收
很多初学GPS接收时喜欢这样写:在主循环里不断查RI标志位,接收到一个字节就存一个。这在低速、数据量小的场景下勉强能跑,但在GPS每秒输出一串连续多行文本的场景下,主循环里如果同时还在做LCD刷新或按键扫描,很容易丢字节。GPS每帧几十个字符,9600波特率下每秒约960字节,而一个字符的接收间隔只有约1ms,主循环任何一个分支耗时超过这个间隔,就会漏掉中间字节。
这个工程用的是串口中断接收,每个字节到达时硬件自动跳进中断服务函数,把数据压进缓冲区。这样做的好处是CPU不用一直等串口,可以把时间花在LCD刷新和解析上。常见做法是在中断里只做“存”这件事,解析放到主循环里做,避免在中断里做复杂计算导致后面的字节丢失。
// 串口中断服务函数:只负责把数据放入环形缓冲区 unsigned char rx_buffer[256]; // 接收缓冲区 unsigned char rx_head = 0, rx_tail = 0; // 环形缓冲区读写指针 void UART_ISR(void) interrupt 4 { if (RI) // 接收中断标志位 { RI = 0; // 必须软件清零,否则会反复进中断 rx_buffer[rx_head] = SBUF; // 从串口数据寄存器读取一个字节 rx_head = (rx_head + 1) % 256; // 环形指针前移 if (rx_head == rx_tail) // 缓冲区满说明主循环消费太慢 { rx_tail = (rx_tail + 1) % 256; // 丢弃最旧的数据,别丢新的 } } }逻辑说明:环形缓冲区的写指针rx_head只由中断函数修改,读指针rx_tail只由主循环修改,两者独立。当写指针追上读指针时,说明主循环处理速度跟不上串口接收速度,这时丢最旧的数据、保留最新的,保证拿到的是最新GPS状态而不是积压的旧数据。
参数说明:缓冲区大小256字节,约等于GPS完整一帧(最长约80字节)的三倍容量。如果你用的GPS模块输出频率是5Hz以上,缓冲区建议扩到512。如果主循环里解析逻辑很重,也可以开双缓冲——一边存一边解,交替使用,但这对51平台来说通常没必要。
3.3 帧完整性判断:为什么“通电时不完整”到“过一会完整”
资源里有个文件叫“GPS原始数据捕获(从一开始通电时不完整到过一会后完整).txt”,这个名字起得很实在,它记录了一个真实的工程现象:模块刚上电时会输出残缺帧、空行、甚至只有半行的数据,过几秒到几十秒后才稳定输出完整NMEA帧。
原因有两层。第一层:GPS模块内部的协议引擎需要初始化,上电瞬间输出的数据可能来自不完整的内部状态;第二层:定位解算需要接收机完成卫星捕获和跟踪,冷启动时没有星历,需要下载星历或搜星,这个阶段输出的GPRMC定位状态字段是V(无效),但语句结构依然可能是完整的。
所以解析程序不能假设每一条输入都是干净的。正确的做法是只处理以$GPRMC开头、以\r\n结尾、且校验和正确的完整行。工程里处理帧完整性的策略是这样的:主循环里不断从环形缓冲区往外取字节,每取到一个$就把它当作新帧起始,然后持续接收直到遇到\n,一帧收完后再做校验。
// 从环形缓冲区提取一帧完整NMEA数据,返回帧长度;不完整返回0 unsigned char NMEA_GetFrame(unsigned char *frame) { unsigned char len = 0; while (rx_tail != rx_head) // 缓冲区非空 { unsigned char ch = rx_buffer[rx_tail]; rx_tail = (rx_tail + 1) % 256; if (ch == '$') // 遇到新帧起始,重新计数 { len = 0; } if (len < 90) // NMEA最长不超过90字符 { frame[len++] = ch; } if (ch == '\n') // 遇到换行符,帧结束 { frame[len] = '\0'; return len; } } return 0; // 还没收到完整换行 }这段代码的关键逻辑是$重新计帧:如果上一帧因为串口干扰缺了尾巴,下一个$到来时len会归零重新开始,不会被半截数据污染。这也是为什么GPS解析程序几乎都用“帧起始符+结束符”双条件判断,而不是只依赖固定长度。
4. 数据解析与LCD显示:坐标、速度、方向的计算与坑
4.1 解析主流程:状态机与字符串切割
拿到完整帧后,下一步是解析。工程里GPS.c的核心函数做的事情可以拆成三步:
- 判断帧类型:比较帧的前6个字符是
$GPRMC还是$GPGGA; - 按逗号切割,提取目标字段;
- 把ASCII字符串转换成数值,存入全局结构体。
这里有个工程细节:NMEA帧里字段数量是固定的,但某些字段可能为空(比如海拔在某些模块里不输出),所以切割不能简单用strstr定位,必须按序号逐个逗号找。工程里用的方法是定义一个指向帧头的指针,然后循环找逗号、把逗号替换成\0,这样每次strtok风格的分段就出来了。
// 解析GPRMC帧,提取定位状态/纬度/经度/速度/航向 void GPS_ParseRMC(unsigned char *buf) { unsigned char *p = buf; unsigned char field_cnt = 0; // buf格式:$GPRMC,092204.000,A,3102.4567,N,... while (*p != '\0') { if (*p == ',') { *p = '\0'; // 用结束符替换逗号,让每段变成一个独立字符串 field_cnt++; } p++; } // 此时buf里的逗号全部变成了\0,各字段可用buf+偏移访问 // field 0: $GPRMC // field 1: UTC时间 // field 2: 定位状态 A/V // field 5: 经度值 }逻辑说明:这种解析比逐个字符比较更直观,代价是破坏了原始字符串——把逗号替换成\0后,整个帧就不能再被二次使用了。所以如果你还要用原始帧做别的分析,需要先复制一份再解析,或者定义一个大数组存原始数据。这属于工程里的取舍,没有对错,但你要知道有这个问题。
一个更稳妥的做法是用索引数组:扫描一遍,把所有逗号的位置记到数组里,然后用索引直接取字段起始和结束位置,不破坏原字符串。这个方案多占几十字节RAM,但在资源充足的平台上更安全。这套工程在51上跑,RAM有限,用替换法没毛病。
4.2 经纬度度分转换:最容易算错的一步
NMEA输出的经纬度是“度分”格式,必须转成十进制度,才能用于距离和速度计算。转换公式:
十进制度 = 度 + 分 / 60以3102.4567,N为例:整数部分31是度,小数部分的两位是分,所以是31度02.4567分。换算后:
31 + 2.4567 / 60 = 31.040945度注意这里有个非常容易翻车的细节:经度和纬度的整数位长度不同。纬度可能是两位整数(如31度),也可能是三位(如12度赤道附近),经度还可能是三位(如121度)。如果按固定位置切分度分,就会把12112.3456切成“12度112.3456分”,完全错乱。
正确做法是先找小数点的位置:小数点前面是度分合并值,其中小数点前两位(对纬度和小于100的经度而言)是度,但更通用的做法是把小数点前面的数字除以100取整得到度,余数乘100再除以60。下面给出通用转换函数:
// NMEA度分格式转十进制:输入3102.4567,返回31.040945 float GPS_DM2DD(float dm) { int degree = (int)(dm / 100); // 3102.4567 / 100 = 31,整型截断得到度 float minute = dm - degree * 100; // 3102.4567 - 3100 = 2.4567,得到分 return degree + minute / 60.0; // 31 + 2.4567/60 = 31.040945 }这个函数的逻辑不复杂,但精度要注意:dm / 100必须用浮点除法,否则31.024就变成31了;另外degree是整型,如果传入的是负数(南纬西经),要先取绝对值再换算,最后再按方向字段恢复正负号。工程里如果方向字段是S或W,最终结果要取负值,这个符号处理漏掉的话,你会在南半球或西经地区得到完全错误的坐标。
避免用浮点:51单片机没有硬件浮点单元,float运算会引入大量软件库代码,占用ROM并且慢。精打细算的工程师会把度分转换写成整数运算,用“分乘以1000再除以60”的方式保留三位小数精度。具体做法是把minute乘以1000取整,除以60得到毫度。这在显示场景下完全够用,而且计算速度快一个量级。
4.3 LCD1602与LCD12864的双屏驱动差异
这套工程同时包含了LCD1602和LCD12864两套驱动,这在资源里是加分项——因为很多课设要求“用LCD显示定位结果”,但屏幕型号不同,底层驱动完全不同。
先说LCD1602,它是16x2字符屏,驱动核心是写命令和写数据的时序控制,需要RS、RW、E三个控制引脚。这个工程里LCD1602.c里的初始化关键在于延时:LCD1602上电后需要等待一段时间才能执行第一次命令,如果单片机复位后立刻初始化,会因液晶内部尚未就绪而失败。
再说LCD12864,它是128x64点阵屏,支持中文字库(ST7920控制器),可以显示汉字。但它的初始化时序比1602长得多,且写数据时要区分“写命令”和“写数据”两个通道。这个工程里最值得看的地方在于:作者把GPS解析结果拼成字符串数组,然后分别用一个函数输出到LCD1602的字符位和LCD12864的绘图区。地址映射不一样,1602是行列直接对应,12864要把显示内容按页写入。
| 对比项 | LCD1602 | LCD12864 |
|---|---|---|
| 控制引脚 | RS/RW/E 三线 | CS1/CS2/RESET或PSB切换 |
| 显示内容 | ASCII字符 | ASCII+中文字库 |
| 初始化延时 | 约15ms | 约40ms以上 |
| 数据位宽 | 4位/8位可选 | 8位 |
| 典型应用 | 显示经纬度数字 | 显示“定位成功”等汉字状态 |
把GPS数据放到LCD上,格式化是一个细节:经纬度也好、速度也好,在NMEA里都是字符串,直接显示没问题。但如果你想要“N 31.040945 E 121.123456”这种格式,需要在单片机里用sprintf或手动拼接。51平台上sprintf占ROM较大(约1KB以上),很多精简工程用手动拼接。资源里的main.c用的是手动拼接,把十进制的整数部分和小数部分分别转成字符串再拷贝,这样ROM占用小得多。
5. 避坑:GPS数据捕获的五个常见踩坑现场
5.1 现象:串口输出全乱码,偶尔蹦出可识别的字段
原因:波特率不匹配。最常见的场景是模块默认9600波特率,但单片机初始化配置了4800;或者晶振不是11.0592MHz,导致定时器重装值算错,实际波特率偏差超过2%就会开始错位。
解决:示波器或逻辑分析仪量一下模块TX引脚的实际波特率;没有仪器的话,先把模块直接接USB转TTL,用电脑串口助手以9600打开,看到正常输出就锁定波特率。然后核对单片机晶振频率,如果是12MHz,要用0xF7附近的重装值并容忍约0.6%的误差,或者干脆换11.0592MHz晶振。换晶振是治本,改波特率是治标。
5.2 现象:定位状态一直是V,但卫星数有值
原因:模块在室内或天线正上方有金属遮挡,能收到卫星信号但信号强度不足以完成定位解算。GPS模块显示卫星数不等于能定位,卫星数只代表“听到了几颗卫星的载波”,要定位需要至少4颗卫星的信号质量达到门限。
解决:把天线移到窗边或室外,保持上方无遮挡。等待时间上,冷启动可能需要30秒到几分钟,资源里那份“通电不完整到完整”的日志就是这个过程的真实记录。如果你发现五分钟以上还是V,检查天线馈线是否松动,或者模块是否处于“飞行模式”——有些模块需要拉高某个配置引脚才能启用定位。
5.3 现象:程序上电后LCD白屏或显示乱字符
原因:LCD初始化时序不对,或LCD与单片机之间电平不兼容。尤其LCD12864,很多模块是5V电平,如果你的单片机是3.3V供电,IO高电平可能达不到LCD识别门限,命令根本没写进去。
解决:先查供电电压,再查初始化延时。LCD12864上电到首次初始化至少等40ms,很多51工程里这个延时不够,改成50ms后正常。还有一种情况是LCD对比度脚没接电位器,默认电压导致显示太淡,看上去像没内容。处理方式是接一个10K电位器,调中间位置。
5.4 现象:数据解析出来了,但经纬度数值不对,偏了几度
原因:度分格式没有正确转换,或者把南纬西经的正负号丢了。很多人在计算机上测试时直接用北京的坐标,N和E都是正值,没暴露这个问题;一到南半球设备就彻底错位。还有人是直接把“3102.4567”当31.024567用了,差了约两度。
解决:用资源里那份原始日志,找一条已知定位状态的GPRMC,手工把度分格式换算成十进制度,再和解析结果比对。写一个自测用例:输入3102.4567,期望输出31.040945,跑不通就回去检查GPS_DM2DD函数的整数截断和浮点除法。
5.5 现象:LCD上速度显示恒为0,但模块明明在动
原因:GPRMC里的速度字段单位是“节”,需要乘以1.852才是km/h,乘以0.514才是m/s。如果解析时直接把字符串转float后填到显示缓冲区,室内无定位时这个字段为空或0,出门后也一直显示0,往往是因为定位状态没先判断——定位状态是V时速度字段本身就是失效值。
解决:解析GPRMC时先读字段2(定位状态),是A才更新速度、方向等动态数据,是V就保留上次有效值或显示“无定位”。否则模块在搜星阶段输出的速度0会被当成真实值,用户看到的就是永远0。这个顺序问题在资源里GPS.c的处理逻辑中是先判状态再解析字段,你接手代码时别把这两步顺序调反了。
6. 进阶验证:用离线日志复盘定位质量与航迹走向
当单片机端的解析和显示都正常之后,还有一个值得做的验证:把串口抓下来的GPS原始日志离线导入PC端分析,确认模块输出的数据在长时间尺度上是否稳定、坐标是否平滑。这套工程自带的“GPS原始数据及处理后的数据.txt”刚好可以用来做这个事。我一般会用Python写一个快速分析脚本,把每一行的GPRMC解析出来,算相邻两点间的距离,验证速度字段和实际位移是否吻合。
一个实用的验证脚本思路是这样:读取txt里每一行,筛出$GPRMC且定位状态为A的帧,解析出UTC时间和经纬度,然后用半正矢公式算相邻两点间距离,除以时间差得到实测速度,再与GPRMC自带的速度字段比对。如果两者偏差在合理范围内,说明模块输出和解析逻辑都没问题;如果偏差很大,优先怀疑度分转换精度和节转km/h是否漏了。
整份资源里最值得反复看的是那两段串口日志:一段是通电瞬间的输出,一段是稳定后的输出,它们把冷启动和稳定定位这两种状态展示得非常直观,对理解GPS模块行为和调试自己的解析代码都非常有用。我把这个资源跑通前前后后花了两个晚上,翻车最多的地方集中在波特率匹配、度分转换和定位状态判断这三个环节,尤其是度分转换——当时直接用浮点除法保留全部小数,结果在51上慢得屏幕刷新都卡了,后来改成整数毫度运算才顺过来。从那以后我每次拿到GPS模块,第一件事就是先用USB转TTL抓一份原始日志,确认格式后再写单片机那边的解析代码,不做这个前置确认就去调程序,大概率会把时间浪费在猜数据里面。如果你也在做GPS相关的课设或嵌入式项目,建议你也按这个顺序走一遍,希望帮到你。
本文还有配套的精品资源,点击获取