简介:STM32+GPS定位系统是一份基于ARM Cortex-M内核微控制器与NMEA-0183协议的嵌入式定位方案资源包,面向物联网、车载导航、户外运动等领域的嵌入式开发者。资料覆盖STM32固件工程、GPS模块数据解析及OLED显示控制,帮助用户完成从UART串口接收、NMEA语句解析到经纬度、速度、时间等定位信息展示的完整流程。包内共152个文件,以工程源码为主:37个.h头文件与34个.c源文件构成核心固件体系,辅以.s启动文件、.hex烧录文件、.uvprojx工程文件及编译中间产物,便于用户直接打开工程参考编译与链接过程,整包压缩后约2.09MB。目前已有7470人学习浏览,作者将GPS解析后的串口输出与OLED显示分离处理,读者既可对照源码理解NMEA数据帧解析细节,也可参考固件初始化、波特率配置与显示刷新逻辑,快速迁移到自有硬件平台。
1. 项目概述
这几个月一直在折腾STM32的GPS定位项目,从最开始买模块、看协议,到后面把数据解析出来、在屏幕上画出轨迹,中间踩了不少坑。现在把整个项目从方案选型到代码实现到问题排查完整梳理一遍,希望能给正在做类似项目的人一些参考。这个项目说到底就是让STM32通过串口读取GPS模块输出的NMEA协议数据,解析出经纬度、时间、速度、卫星数等关键信息,然后根据需求做显示、存储或者无线上报。整个方案适合做毕业设计的学生、准备做车辆定位或便携设备的开发者,也适合想深入了解GPS数据解析的嵌入式爱好者。先说结论:这个项目难度不大,但细节很多,尤其是协议解析和硬件抗干扰这两个环节,处理不好会浪费大量时间。
市面上主流GPS模块的接口几乎都是串口TTL电平,这就让STM32接GPS变成了一个标准的串口数据接收任务。无论你是用NEO-6M、ATGM336H还是其他模块,核心工作就是接收一段ASCII字符串,然后按照NMEA协议去截取字段。真要做起来,硬件连接十分钟搞定,真正的复杂度在解析逻辑和异常处理上。而且GPS本身有个特性——它输出的数据不是连续的,而是以固定频率(通常1Hz)周期性发送一整条语句,这对串口缓冲区的设计提出了要求。我的方案是用串口空闲中断加DMA接收,这个后面会详细讲,如果你只是想快速跑通,用普通中断接收也能实现,只是稳定性会差一些。
2. 整体设计与方案选型
2.1 硬件选型:为什么用STM32F103C8T6和ATGM336H
项目第一步是选型。STM32这边我选了最常见的F103C8T6,蓝色Pill板子,理由很简单:便宜、资料多、串口资源足够,一个串口接GPS,还能留一个串口做调试输出。如果你用F407或者H743,性能溢出不说,调试反而更麻烦——高主频带来的电源噪声对GPS这种弱信号接收设备并不友好。
GPS模块选择上,我对比过市面上常见的几款,这里直接列个表供参考:
| 模块型号 | 价格区间 | 特点 | 适合场景 |
|---|---|---|---|
| NEO-6M | 15-25元 | u-blox经典款,文档全,但搜星较慢 | 学习入门 |
| ATGM336H | 10-20元 | 国产中科微,功耗低,支持BDS+GPS双模 | 大多数项目首选 |
| NEO-M8N | 40-60元 | u-blox高端款,搜星快,抗干扰好 | 对性能有要求 |
| A2235H | 8-15元 | 超便宜,体积小,性能一般 | 预算敏感项目 |
我最终选了ATGM336H,一个重要原因是它支持BDS+GPS双模,在中国环境下搜星数量和定位速度都明显优于单GPS模块。实测下来,冷启动搜星在开阔地带大概30到40秒能定位,热启动基本在1到2秒内完成。模块上面一般会带一个陶瓷天线,如果设备是放在室内的,建议换一个有源天线,否则信号强度根本不够。
2.2 串口参数与协议基础:GPS数据长什么样
GPS模块通过串口输出的是一串以美元符号开头的ASCII文本,协议叫NMEA 0183。上电以后模块会周期性输出多种语句,常用的有$GNGGA(定位信息)、$GNRMC(推荐最小定位信息)、$GNGSA(卫星精度)和$GNVTG(地面速度)。注意不同模块前缀有区别,老的GPS-only模块输出的是$GPGGA、$GPRMC,双模模块会输出$GNGGA、$GNRMC。
串口参数一般固定为波特率9600、8位数据位、1位停止位、无校验。这里有个小坑——不少国产模块默认确实是9600,但你买到手可能被商家改过,建议上电后用串口助手先看一帧数据,确认波特率再说。我曾经遇到过一块默认38400的模块,直接用9600去接,出来的全是乱码,排查了半天才发现问题。
2.3 为什么选串口中断加缓冲而不是轮询
GPS数据是持续不断流的,每秒会有好几条语句连续发过来,总长度大概几百字节。如果用轮询方式在main循环里getchar,一是浪费CPU,二是容易丢数据。我采用的方案是串口空闲中断加DMA接收,简单说就是:DMA把串口收到的数据持续搬运到缓冲区,当一帧数据发完(总线空闲),触发空闲中断,这时候在中断里置一个标志位,主循环发现标志后就解析缓冲区。
这个方案的好处是CPU几乎零负担,也能保证数据完整性。不过STM32F103的DMA加空闲中断有个老毛病——第一次接收会有芯片勘误表里提到的问题,所以我在初始化时故意把串口寄存器里的接收标志位清掉,触发一次假空闲中断来规避。细节后面代码里会体现。
3. NMEA协议解析:从字符串到经纬度
3.1 核心语句$GNGGA字段详解
$GNGGA语句包含了最核心的定位数据,一条完整的语句长这样(字段间用逗号分隔):
$GNGGA,034523.000,3101.28434,N,12120.67890,E,1,08,0.9,8.5,M,-2.3,M,,*4F按顺序解读各个字段:UTC时间03时45分23.000秒、纬度3101.28434(度分格式,即31度01.28434分)、北纬N、经度12120.67890(即121度20.67890分)、东经E、定位质量指示(0=无效,1=GPS定位,2=差分定位)、使用卫星数08颗、水平精度因子0.9、海拔高度8.5米、大地水准面差距、差分时间、差分站ID、校验和*4F。
解析时最关键的坑是纬度和经度是度分格式,不是常见的十进制格式。以北纬3101.28434为例,实际十进制度数是31 + 01.28434 / 60 = 31.0214057。如果你算出来经纬度是3101度,那肯定不对了,得除以100取整再对余数除以60。不少人在这一步翻车,拿到数据直接在百度地图里搜,出来的位置完全不对。
3.2 二选一:解析GGA还是RMC
GGA和RMC都包含了经纬度信息,区别在于RMC还多了日期、速度和航向角,但没有海拔。如果你的项目需要展示速度或记录日期,用RMC更方便;如果更关心海拔和卫星数,就解析GGA。我的做法是同时解析这两条语句,反正放到结构体里也不多占内存。需要提醒的是,这两条语句里的经纬度字段格式完全一致,解析函数可以复用。
3.3 完整解析代码框架
我用的HAL库加标准C语言,解析部分核心代码如下:
typedef struct { uint8_t hour; uint8_t minute; uint8_t second; double longitude; // 十进制表示 double latitude; // 十进制表示 uint8_t positionStatus; // 0无效 1有效 uint8_t satelliteNum; float altitude; // 海拔 米 } GPS_Info_t; GPS_Info_t gpsInfo; static double convertNMEAtoDecimal(const char* src, char direction) { if (src[0] == '\0' || direction == '\0') return 0.0; double raw = atof(src); int degrees = (int)(raw / 100); double minutes = raw - degrees * 100; double decimal = degrees + minutes / 60.0; if (direction == 'S' || direction == 'W') { decimal = -decimal; } return decimal; } void parseGNGGA(char* buffer) { char* p = buffer; int fieldIndex = 0; char* fieldPtr = NULL; while ((fieldPtr = strsep(&p, ",")) != NULL) { switch (fieldIndex) { case 1: // UTC时间 if (strlen(fieldPtr) >= 6) { gpsInfo.hour = (fieldPtr[0] - '0') * 10 + (fieldPtr[1] - '0'); gpsInfo.minute = (fieldPtr[2] - '0') * 10 + (fieldPtr[3] - '0'); gpsInfo.second = (fieldPtr[4] - '0') * 10 + (fieldPtr[5] - '0'); gpsInfo.hour += 8; // 北京时间 东八区 if (gpsInfo.hour >= 24) gpsInfo.hour -= 24; } break; case 2: // 纬度 gpsInfo.latitude = convertNMEAtoDecimal(fieldPtr, *(p + 1)); break; case 4: // 经度 gpsInfo.longitude = convertNMEAtoDecimal(fieldPtr, *(p + 1)); break; case 6: // 定位状态 gpsInfo.positionStatus = atoi(fieldPtr); break; case 7: // 卫星数 gpsInfo.satelliteNum = atoi(fieldPtr); break; case 9: // 海拔 gpsInfo.altitude = atof(fieldPtr); break; } fieldIndex++; } }注意我这里用了strsep来分割字段,这个函数在分割时会直接修改原字符串,所以传入的buffer必须是可以写的。如果你用的是只读字符串数组,会直接触发硬错误。另外经纬度解析部分没有做完整的边界检查,实际项目里建议对上位机传来的数组长度先做个校验,防止非法数据导致atof读到越界内容。
3.4 校验和验证要不要做
NMEA语句末尾的号后面跟两位十六进制校验和,是对$和之间所有字符的异或结果。有人觉得GPS模块出来的数据不会出错,省掉这一步。我的建议是:调试阶段可以做,正式项目最好也做,因为电磁干扰可能在传输过程中改变某个字符,尤其是导线较长或者设备附近有电机、继电器这类干扰源时。实现起来也不复杂,十几行代码的事。但要注意,我第一次写校验函数时直接把*后面的两个字符按ASCII码相加了,结果怎么都不对,花了两小时才想起要把它们先转成十六进制数字再算异或。
4. 实操过程与核心环节实现
4.1 硬件连接与先期验证
硬件接线其实很简单,ATGM336H模块六个引脚,实际只用四个:VCC接3.3V、GND接GND、TXD接STM32的PA10(USART1_RX)、RXD接PA9(USART1_TX)。模块还有一个PPS引脚,是秒脉冲输出,可以用来做时间同步,不是必须接。注意模块TXD和STM32的RX本来就该交叉连接,有人在这里接错过,导致收不到任何数据。
在把模块接到STM32之前,强烈建议先做一个独立验证:模块用USB转TTL接电脑,打开串口助手,看模块是否能正常输出数据。这一步能直接确认模块本身没问题,避免后面STM32调不通时还要怀疑模块坏了。我遇到过模块一到室外就能定位,在室内靠窗能搜到星但定不了位,这种其实都算正常——GPS信号穿墙能力很差,室内定位需要把天线贴近玻璃才行。
连接完成后,可以先跑一个最简单的回环测试:STM32把收到的原始串口数据原样转发到调试串口,在电脑串口助手上看有没有$GNGGA这样的语句输出。这一步过了,说明硬件链路是通的,再往下做解析才有意义。
4.2 串口DMA空闲中断接收实现
STM32F103的USART1挂载在APB2总线上,时钟频率72MHz。我们配置串口波特率9600、8位数据位、1位停止位、无校验,并使能空闲中断和DMA接收:
void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 9600; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(&huart1, uartDmaBuffer, DMA_BUFFER_SIZE); } void USART1_IRQHandler(void) { if (RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); HAL_UART_DMAStop(&huart1); uint16_t len = DMA_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&huart1); if (len > 0 && len < DMA_BUFFER_SIZE) { processGPSData(uartDmaBuffer, len); // 这里会把数据复制到一个可写的缓冲区再做解析 } HAL_UART_Receive_DMA(&huart1, uartDmaBuffer, DMA_BUFFER_SIZE); } }这个方案的效率非常高,整个接收过程中CPU几乎不参与,DMA模块直接把串口数据搬运到内存里。每收到完整的一帧数据后,空闲中断触发一次,我们做一次解析。注意DMA缓冲区大小要设置得足够大,GPS语句最长的一条可能超过100字节,缓冲区太小会导致一帧数据放不下。
4.3 从原始数据流中提取完整NMEA语句
这里有个关键点:GPS模块输出的不是单条语句,而是多条语句连续输出。如果直接处理DMA缓冲区,一个缓冲区里可能包含半条、一条半、甚至多条语句。我的处理方式是逐字节扫描,找到$作为语句起始,然后继续扫描到换行符\r\n作为语句结束,把中间的内容提取到一个临时数组里再交给解析函数。伪代码如下:
static void processGPSData(uint8_t* data, uint16_t len) { static uint8_t lineBuffer[128]; static uint8_t lineIndex = 0; static uint8_t inLine = 0; for (uint16_t i = 0; i < len; i++) { uint8_t ch = data[i]; if (ch == '$') { inLine = 1; lineIndex = 0; } if (inLine) { lineBuffer[lineIndex++] = ch; if (lineIndex >= sizeof(lineBuffer) - 1) { inLine = 0; // 防止缓冲区溢出 } } if (ch == '\n' && inLine) { lineBuffer[lineIndex] = '\0'; if (strstr((char*)lineBuffer, "$GNGGA") != NULL) { parseGNGGA((char*)lineBuffer); } inLine = 0; } } }这样处理以后,无论一帧DMA数据里包含几条完整还是不完整的语句,我们都能正确地提取出以美元符起始、以换行符结尾的完整NMEA语句。这个逻辑虽然不复杂,但理清楚以后就再也不会出现一条语句被切碎导致解析失败的情况了。
4.4 定位算法的理解与三边测量
既然项目是GPS定位,很多做毕设的朋友会对定位原理感兴趣。GPS接收机定位原理其实就是一个三维空间的三边测量——每颗卫星知道自己精确的位置,也能算出自己和接收机之间的距离,然后以卫星为球心、距离为半径画球面。我们如果同时收到4颗以上的卫星信号,就能解出接收机位置的三个坐标(经度、纬度、高度)加一个时间误差,一共四个未知数。
在STM32上我们可以做个演示版的三边测量算法,比如在已知三个参考点坐标和距离的情况下,计算目标位置:
- 给定三圆交点,对两圆联立方程求解得到两个交点,再根据第三圆的误差选出一个真实位置
- 实际GPS是伪距定位,每颗卫星的距离都包含接收机时钟误差,所以需要至少4颗卫星迭代求解
- 如果项目里想用WIFI定位或蓝牙信标做定位,这类距离传感器报出来的距离不准,直接用三边测量误差很大,需要配合最小二乘法做多点拟合
我建议在博文里把我们做的三边测量演示也总结一下,因为很多人在做室内定位的毕设时用到了类似算法。简单说一下实现思路:已知三个基站的坐标(A(x1,y1)、B(x2,y2)、C(x3,y3)),以及目标到这三个基站的距离r1、r2、r3,可以用最小二乘法把超定方程组写出来,再通过矩阵运算解出目标坐标。在STM32上我把这个写成了解矩阵的代码,因为要处理浮点运算,所以用了C标准库的数学函数,注意使能硬件浮点或者保证编译链接了浮点支持。代码大约三十行,核心就是用两个式子分别算出各自可能的点,再通过误差比较选取更优的那个。
4.5 KEIL5与开发环境里遇到的一个小坑
这个项目开发环境用的是KEIL5,配合STM32F103的芯片包。有一次我在KEIL5里编译完烧录后,调试器突然提示找不到目标设备,关键是一开始还能连上,烧过一次程序之后就再也连不上了。排查了很久最后发现是ST-LINK的连接线松了,加上开发板供电不稳导致芯片锁死了。解锁办法是按住复位键、点击下载瞬间再松开,成功几率很大。
另外有一点,如果你同时装了C51和STM32的KEIL,注意安装顺序有讲究,最好先装C51再装MDK,否则两个版本的Pack管理可能互相干扰,提示找不到设备。这个问题虽然和GPS本身无关,但做这个项目的过程中我确实被折腾了一次,顺带记录下来。
5. 常见问题与排查技巧实录
5.1 搜不到星 / 一直无定位数据
这个是最常见的问题。模块上电后,如果LED灯在闪但始终没有定位,最常见的原因是天线没面向天空。GPS信号强度很弱,穿过玻璃、墙壁都会有明显衰减,所以模块需要放在窗边或室外。另外一个原因是模块刚从仓库发出,处于冷启动状态,第一次搜星可能需要1到2分钟,耐心等一下。还有一种可能是模块的锂电备份电池没有电,导致每次上电都是冷启动——这种情况可以通过外接一个3V的纽扣电池解决。
排查顺序建议:先看模块有无数据输出(用串口助手直接接模块),再看语句里的定位状态字段是不是1,再看模块附近有没有遮挡物,最后再检查天线焊接和馈线。注意,不要在电脑屏幕旁边测试定位——显示器的电磁辐射和金属外壳会遮蔽GPS信号。
5.2 串口收到乱码 / 字符串不完整
乱码的先查波特率,GPS模块默认9600,但有的改装板默认是38400或115200。再查电平——STM32的串口是3.3V电平,如果GPS模块是5V供电但信号引脚是5V的,直接接STM32会损伤引脚。用示波器或者万用表量一下TXD引脚的空闲电平是不是3.3V左右。如果模块输出的是5V电平,中间需要加电平转换芯片,或者用分压电阻。
字符串不完整的情况,大概率是DMA缓冲区太小,把长语句截断了。检查DMA缓冲区设置,建议至少256字节。
5.3 坐标偏移到了海里
解析出来的经纬度如果看起来完全不对,比如本来是31度变成3101度,那就是没做度分转十进制。另外还有一个可能是字段位置取错了——GGA语句和RMC语句的字段索引不一样,我见过有人拿解析GGA的索引去解析RMC,结果把高度当成经度用了。
另一个偏移问题是坐标系不对。GPS输出的是WGS-84坐标系,而国内很多地图用的是GCJ-02加密坐标,直接投到百度地图上会偏移几百米。如果你要做地图显示,必须做坐标转换,这个网上有现成算法,搜“WGS84转GCJ02”就行。
5.4 STM32目标找不到 / 下载失败
做这个项目时如果用的ST-LINK,偶尔会遇到“Error: No STM32 target found! If your product embeds debug authentication”之类的报错。这个提示在新版固件里比较常见,重点检查SWDIO和SWCLK两根线是不是接对了,以及目标板有没有独立供电。如果你用了ST-LINK Utility去读Flash,注意它和KEIL5不能同时占用调试口,否则会互相冲突。
有些新出的STM32芯片带调试认证保护,正常从ST官网下载的固件不会锁死,但如果你用J-Flash之类工具去强行读写,反而可能触发保护导致连不上。稳妥的办法是先用标准库写个点灯程序,确认下载链路完全正常后再搞GPS。
5.5 测试时如何模拟GPS信号
如果你在室内做开发,又想快速验证解析逻辑,可以考虑用模拟GPS信号的测试设备。之前提到的Partapack H2这类信号模拟器,能生成模拟的GPS卫星信号射给接收机,让模块在没有真实卫星信号的环境下也能输出定位数据。这类设备以前是军方或专业厂商用的,动辄上万,但现在网上有便宜的简易版本,几百块就能买到,对产线测试和开发调试很有帮助。
用模拟器测出来的数据特征是经纬度固定、时间可设置,卫星数一般固定为某个值。如果你暂时买不起模拟器,也可以用串口助手直接把预先录制好的NMEA语句循环发送给STM32,效果类似,只是没法测试模块的射频性能。
6. 进阶扩展方向
项目跑通以后,加功能的方向很多。最实用的是通过无线模块把GPS数据远程上报,比如配上ESP8266或者SIM模块,把解析好的经纬度通过HTTP或者MQTT发到服务器,在网页上看位置轨迹。这也解释了为什么热词里有“STM32 8266 宿舍控制灯开发实战”——本质是STM32加无线模块的组合玩法,GPS项目同样适用。
另一个方向是数据存储。可以加个SD卡模块,把定位数据按CSV格式记录到文件里,形成轨迹回放。用FATFS文件系统在STM32上做SD卡读写,配合GPS数据做日志记录,很成熟。我当时做了个简化版——串口把数据发给电脑,电脑上写了个简单的上位机,实时显示经纬度并在地图上标点。
如果有余力,还能做电子围栏或者超速提醒。电子围栏就是设定一个中心点和半径,判断当前经纬度是否在圆内——用两点的球面距离公式就行。超速提醒更简单,RMC语句里有速度字段,直接用公里每小时为单位输出,超过阈值就驱动蜂鸣器。这次做的是把解析结果显示在OLED屏上,在户外实际走了一圈验证定位轨迹,跑起来效果挺好。
我在实际使用中发现,GPS项目最磨人的往往不是协议本身,而是对异常数据的处理。实际环境里模块输出的数据远没有教程里那么干净,可能夹杂着无效帧、校验错误甚至整条乱码,解析代码的鲁棒性决定了一个GPS项目能不能真正落地。写代码的时候按照“宁缺毋滥”的原则——解析失败就丢弃,不拿脏数据去计算,这样才能保证后续显示的经纬度始终可用。最后分享一个小技巧:模块的PPS秒脉冲引脚在户外能看到一秒一次的精准翻转,你可以用示波器看这个引脚来确认模块确实在工作,比傻等屏幕上的定位标志更快更准。
本文还有配套的精品资源,点击获取