简介:这是一份基于STM32F103ZET6微控制器与STM32CubeMX图形化配置工具开发的GPS定位工程,面向正在学习嵌入式串口通信的开发者,尤其适合初次接触CubeMX+HAL库的中级读者。压缩包内含188个文件,整体大小约8.58MB,既包括.ioc工程配置文件、.uvprojx等Keil工程文件,也包含大量C源码/H头文件、编译产物(.axf/.hex)及说明文档,目录按Drivers、Core、Hardware、MDK-ARM等模块划分,便于直接打开工程并按需修改。目前已有219人学习下载。该例程从CubeMX配置时钟、GPIO、UART和中断开始,生成HAL初始化代码,并在应用层实现GPS模块的NMEA语句与二进制协议解析,覆盖经纬度、速度、定位时间等信息的提取。借助HAL_UART_Transmit、中断接收回调等API,读者可清晰理解STM32与外部模块的交互方式,同时也能学到CubeMX重新生成代码时的文件组织策略,是快速上手STM32嵌入式开发的实用参考。
STM32F103ZET6结合CUBE的使用GPS例程
玩STM32有一阵子的朋友,大概率绕不开GPS模块。不管是做智能小车定位、飞行器导航,还是户外数据记录仪,GPS几乎是入门定位功能时最先接触的方案。STM32F103ZET6这颗芯片资源充沛,配合STM32CubeMX的图形化配置,写一个GPS例程其实比大多数人想象中要简单。我这次就把完整的实现过程梳理一遍,从硬件接线、CubeMX配置、NMEA协议解析,到串口接收和坐标换算,全部展开讲清楚。
这篇内容适合刚接触STM32的新手,也适合之前用寄存器开发、想切到HAL库的朋友。我尽量把每一步背后的原因也一并说明白,不只是丢代码让你抄。毕竟GPS例程玩透了,后面接MPU6050、接LORA、接各种串口传感器,套路都是一样的。
1. 项目出发点与方案选型
1.1 为什么选定这套组合
STM32F103ZET6是F1系列里配置比较顶的型号:512KB Flash、64KB RAM、112个GPIO、5个USART,还带FSMC、CAN、USB等外设。GPS模块虽然接入简单,但如果你后面想把GPS数据同时显示在OLED上、通过ESP8266上传到云平台、或者做SD卡存储,F103ZET6的大封装和丰富外设给了充足的扩展空间。
用CubeMX而不是纯寄存器开发,主要考虑两点。第一,串口初始化的代码极其繁琐——波特率寄存器计算、中断优先级配置、GPIO复用模式设置,这些CubeMX点几下就完成了,而且生成的是HAL库代码,层次清晰。第二,后续如果要把串口2换个引脚、或者增加一个DMA通道,CubeMX重新生成即可,不需要去翻参考手册纠结寄存器映射。
有人会说“用寄存器不是更能理解底层吗”,这话不假,但对GPS例程这种应用型项目,核心价值在协议解析和数据处理,而不是串口寄存器配置。把时间花在NMEA协议的理解上,比纠结USART_BRR寄存器的算式有意义得多。
1.2 GPS模块怎么选,NMEA协议是什么
市面上常见的GPS模块有串口输出的裸模块(如ATGM336H、NEO-6M),也有带显示屏的成品模块。做嵌入式的,推荐直接买串口输出的裸模块,引脚一般是VCC、GND、TX、RX,部分模块还带PPS秒脉冲输出脚。ATGM336H因为体积小、功耗低、兼容NMEA协议,在淘宝上很常见,价格也便宜,我用的就是这颗模块。
GPS模块的默认输出格式是NMEA-0183协议,这是GPS接收机输出数据的事实标准。协议里包含很多种语句,常用的就两类:$GPRMC(推荐最小定位信息)和$GPGGA(定位信息详单)。这两个语句里已经囊括了经纬度、UTC时间、定位状态、卫星数、海拔、地面速度等关键数据。解析NMEA协议,本质上就是按逗号切分字符串,再对应字段取值。
需要留意的是,“GPS模块输出的数据”和“能用在地图上的坐标”之间还差一步转换。NMEA协议里经纬度是“度分”格式的,例如3125.1234代表北纬31度25.1234分,你得换算成“度”为单位的小数形式,才能用于后续计算或地图标注。这个换算过程我后面会详细演示。
2. 硬件连接与准备
2.1 引脚分配与硬件接线
我用的是STM32F103ZET6最小系统板,GPS模块物理接线如下:
| GPS模块引脚 | STM32F103ZET6引脚 | 说明 |
|---|---|---|
| VCC | 3.3V或5V(根据模块要求) | ATGM336H支持3.3V供电 |
| GND | GND | 必须共地 |
| TX | PA10(USART1_RX) | GPS发送,MCU接收 |
| RX | PA9(USART1_TX) | 如果不需要向GPS发命令可不接 |
| PPS | 可悬空 | 秒脉冲输出,用于时间同步 |
串口我选USART1,理由只有一个——它在PA9/PA10上,这是最常用的串口引脚,CubeMX默认配置最方便,而且下载调试时不容易和ST-LINK冲突。如果后续要扩展,USART1可以留着做数据输出,另外开一个串口接GPS模块也行,思路是一样的。
接线时有几个细节要提醒。第一,GPS模块的TX接MCU的RX,RX接MCU的TX,这是交叉连接,新手经常在这里搞反。第二,务必共地,不然电压参考点不一致,串口波形会乱。第三,GPS模块的天线要放在能接收卫星信号的位置,金属外壳会屏蔽信号,室内基本收不到星,这个后面排查无数据问题时还会提到。
2.2 硬件调试前的关键检查点
上电之前,先拿万用表量一下GPS模块的VCC和GND引脚之间电压是否正常。我之前遇到过模块标称3.3V供电,结果板载的稳压芯片是坏的,输出5V把模块烧了——这种低级错误一旦发生,排查起来非常浪费精力。
对于带陶瓷天线的GPS模块,天线位置很敏感。第一次上电调试,最好把天线放到窗边或者直接到室外空旷处。GPS冷启动需要几分钟时间来完成卫星捕获和星历下载,不是你一上电马上就能输出有效定位数据的。期间模块虽然会输出NMEA语句,但里面定位状态是无效的,这个属于正常现象,别着急改代码。
3. CubeMX工程配置
3.1 串口波特率与中断配置
打开CubeMX,新建一个基于STM32F103ZET6的工程。这里有个命名小技巧:工程名和生成的代码会直接关联,别用中文和空格,我用的是GPS_Demo。
在Connectivity下找到USART1,设置为Asynchronous异步模式。波特率选9600,数据位8位、无校验、1位停止位——这是GPS模块出厂默认的串口参数,绝大多数模块都是这个配置。有些模块支持更高的波特率(比如115200),但需要发送命令修改模块配置,默认还是9600稳妥。
关键的一步是开启串口中断。在USART1的NVIC Settings页勾选USART1 global interrupt,并把优先级设置为一个合适的值。示例工程里中断优先级默认即可,但如果你的系统里还有定时器中断、DMA中断,需要统一规划优先级。GPS数据是持续不断的(频率一般是1Hz或5Hz),中断响应不及时就会丢数据。
时钟树的配置也要注意。我设置系统主频为72MHz(F103的最高频率),APB1总线时钟36MHz,APB2总线时钟72MHz。USART1挂在APB2上,所以它的时钟源是72MHz,CubeMX会自动计算波特率分频系数。这里不需要手算,但理解原理有助于排查“波特率不对导致乱码”这类问题。
3.2 代码生成与HAL库基本结构
在Project Manager里设置IDE为MDK-ARM(如果你用Keil)或者STM32CubeIDE,然后点击Generate Code生成初始工程。生成的代码里,MX_USART1_UART_Init()函数已经完成了串口参数配置,stm32f1xx_it.c里已经包含了USART1_IRQHandler的中断入口。
一个需要注意的地方:CubeMX默认生成的中断回调函数是空的,你需要重写HAL_UART_RxCpltCallback。但这里有个坑——HAL库接收中断默认是一字节一字节触发的,要真正接收一串GPS数据,你得自己设计缓存方案。我用的方法是循环地把每次中断收到的单字节丢进一个环形缓冲区,然后在主循环里检测$和换行符,切出完整的NMEA语句,再做解析。这个方法对GPS这种不定长的数据帧非常适用。
如果你非要一次性接收整段数据,可以用HAL_UART_Receive_IT配合定长数组,但GPS语句长度不固定,用定长接收容易把后半条语句截断或者粘连,处理起来很别扭。实测下来,环形缓冲区加上逐字节中断接收,是最省心也最不容易出错的方案。
4. NMEA数据解析:GPS的“母语”
4.1 GPGGA和GPRMC字段拆解
模块上电后一秒左右,串口就会开始输出NMEA语句。用USB转TTL接在模块的TX/RX上,接电脑串口助手,你会看到类似这样的数据:
$GPGGA,121548.000,3125.1234,N,12132.5678,E,1,8,0.9,20.5,M,0.0,M,,*5E $GPRMC,121548.000,A,3125.1234,N,12132.5678,E,0.5,173.8,020721,,,D*74看起来是一长串看不懂的字符,其实拆开来看就很清晰了。以GPGGA语句为例,按逗号切分:
| 字段位置 | 取值 | 含义 |
|---|---|---|
| 0 | $GPGGA | 语句类型 |
| 1 | 121548.000 | UTC时间:12时15分48秒 |
| 2 | 3125.1234 | 纬度:31度25.1234分 |
| 3 | N | 北纬 |
| 4 | 12132.5678 | 经度:121度32.5678分 |
| 5 | E | 东经 |
| 6 | 1 | 定位状态,1表示有效定位 |
| 7 | 8 | 使用卫星数 |
| 8 | 0.9 | 水平精度因子(HDOP) |
| 9 | 20.5 | 海拔高度(米) |
GPRMC则是一个更精简的版本,包含了时间和定位状态,还有地面速度、航向和日期。解析GPS数据时,我一般两个都解析:GPRMC取速度和日期更合适,GPGGA取海拔和卫星数更合适。如果你的项目只需要经纬度,解析其中一个就够了。
需要注意$GPRMC里定位状态字段是位置1(即A表示有效,V表示无效),而$GPGGA里定位状态字段是位置6(1或2表示有效,0表示无效)。这个位置经常有人记混,导致判断定位有效时出错,测试时明明已经定位成功了程序却一直认为没定位。
4.2 坐标转换:度分格式转小数度
拿到NMEA里的经纬度数据,别直接拿去做运算。GPS模块输出的3125.1234是“度分”格式的:31度25.1234分。而地图坐标和高德、百度坐标系需要的是“度”格式的小数,也就是31 + 25.1234 / 60 ≈ 31.4187度。
代码实现很简单:
// nmea度分格式转小数度 // 输入:3125.1234(31度25.1234分) // 输出:31.4187(小数度) double ConvertNmeaToDeg(double nmea_coord) { int degree = (int)(nmea_coord / 100); // 取度 double minute = nmea_coord - degree * 100; // 取分 return degree + minute / 60.0; }这里有个细节:int强转会截断小数,直接(int)(3125.1234 / 100)得到31,minute就是25.1234,这个很简单。但如果输入的是南纬或者西经,注意N/S、E/W字段对应的正负号,南纬西经需要取负数。
三边测量算法在模块内部已经完成,你拿到的经纬度就是最终结果。如果后续想把定位做成更精确的差分GPS,才需要自己去处理伪距和载波相位数据,那种玩法不在基础例程范围内。
5. 核心代码实现与解析
5.1 串口接收:基于环形缓冲区
环形缓冲区是串口接收的常用数据结构,原理是维护一个固定大小的数组,写指针和读指针循环递增。STM32的HAL库中断回调每收到一个字节,就把它写入缓冲区,主循环里再取出来解析。
#define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint16_t rx_write_index = 0; volatile uint16_t rx_read_index = 0; // 串口接收中断回调,每次收到一个字节调用一次 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 把收到的字节写入环形缓冲区 rx_buffer[rx_write_index] = rx_temp; rx_write_index = (rx_write_index + 1) % RX_BUFFER_SIZE; // 继续开启接收中断 HAL_UART_Receive_IT(&huart1, &rx_temp, 1); } }主循环里读取缓冲区数据,检测完整的一行NMEA语句:
uint8_t nmea_line[128]; uint16_t nmea_len = 0; uint8_t in_line = 0; void ProcessRxBuffer(void) { while (rx_read_index != rx_write_index) { uint8_t ch = rx_buffer[rx_read_index]; rx_read_index = (rx_read_index + 1) % RX_BUFFER_SIZE; if (ch == '$') { in_line = 1; nmea_len = 0; } if (in_line) { nmea_line[nmea_len++] = ch; if (ch == '\n' || nmea_len >= sizeof(nmea_line) - 1) { nmea_line[nmea_len] = '\0'; ParseNmea((char *)nmea_line); // 解析一条完整语句 in_line = 0; } } } }主循环里调用ProcessRxBuffer()就行。这个思路很直白:看到$开始记录,遇到换行符认为一条语句接收完毕,交给解析函数处理。
5.2 NMEA语句解析:字符串切割实战
NMEA语句的解析核心是strtok函数,按逗号分割字符串后对应字段取值。
#include <string.h> #include <stdio.h> #include <stdlib.h> GPS_DATA gps_data; // 解析结果存放结构体 typedef struct { uint8_t position_valid; // 定位是否有效 double lat_deg; // 纬度(小数度) double lon_deg; // 经度(小数度) float altitude; // 海拔 float speed_kmh; // 速度 km/h float course; // 航向 uint8_t hour, minute, second; } GPS_DATA; void ParseGPGGA(char *line) { char *token; int field = 0; // 按逗号逐字段切割 token = strtok(line, ","); while (token != NULL) { switch (field) { case 1: // UTC时间 HHMMSS.mmm gps_data.hour = (token[0] - '0') * 10 + (token[1] - '0'); gps_data.minute = (token[2] - '0') * 10 + (token[3] - '0'); gps_data.second = (token[4] - '0') * 10 + (token[5] - '0'); break; case 2: // 纬度(度分格式) gps_data.lat_deg = ConvertNmeaToDeg(atof(token)); break; case 3: // N/S if (token[0] == 'S') gps_data.lat_deg = -gps_data.lat_deg; break; case 4: // 经度(度分格式) gps_data.lon_deg = ConvertNmeaToDeg(atof(token)); break; case 5: // E/W if (token[0] == 'W') gps_data.lon_deg = -gps_data.lon_deg; break; case 6: // 定位状态 gps_data.position_valid = (token[0] == '1') ? 1 : 0; break; } field++; token = strtok(NULL, ","); } }需要注意strtok会修改原字符串,所以在调用之前最好先复制一份原始语句到临时数组,避免影响后续使用。
5.3 浮点数格式化与printf重定向
解析出经纬度后,最想做的事情就是把数据打印出来看。嵌入式系统里printf默认是不能用的,需要重定向fputc函数来输出到串口。
#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100); return ch; }接着在代码里就可以:
printf("GPS Status: %d\r\n", gps_data.position_valid); printf("Lat: %.6f, Lon: %.6f\r\n", gps_data.lat_deg, gps_data.lon_deg); printf("Alt: %.1f m, Speed: %.1f km/h\r\n", gps_data.altitude, gps_data.speed_kmh);如果printf输出乱了,检查工程设置里MicroLIB是否勾选。在Keil的Options -> Target里勾上Use MicroLIB,浮点格式化的printf才正常工作,不然会卡在浮点输出环节。
6. 实战问题排查与避坑记录
6.1 常见问题速查表
做这个例程时,我前后调试了几个小时,踩了不少坑。下面这个表格里的内容,可以说是这类GPS例程最常见的问题清单了。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 串口完全没数据 | GPS模块供电异常或接线错误 | 检查VCC和GND,检查TX/RX是否交叉连接 |
| 有数据但全是乱码 | 波特率不匹配 | 确认模块默认波特率(通常9600) |
| 有数据但定位状态一直是V | 天线位置收不到卫星信号 | 把天线放到窗边或室外空旷处,等待冷启动 |
| 有数据且定位有效,但坐标不对 | 度分格式没有转换 | 检查是否除以60做小数度换算 |
| printf输出卡住 | 浮点格式化未正确支持 | 勾选MicroLIB,检查fputc重定向 |
| 数据每隔一会丢失一段 | 中断优先级冲突 | 检查NVIC优先级设置,必要时开启DMA接收 |
GPS冷启动时间长是新手最容易误判为故障的情况。模块冷启动时要从卫星下载星历数据,这个过程在开阔环境下通常需要1~3分钟,在窗边可能需要更久。如果在室内收不到信号,输出语句里的定位状态一直是V(无效),这并不代表代码写错了。
6.2 关于GPS精度的一点个人体会
GPS定位误差主要来源有几个方面:卫星轨道误差、大气层信号延迟、多路径效应(信号反射)和接收机噪声。民用GPS的标称精度在2.5米左右,实际在开阔地带通常能到3到5米,在城市高楼之间可能漂移十几米。这个精度做路径记录、大致定位是够用的,但如果你的项目要求厘米级定位,民用GPS模块就远远不够了,得考虑RTK方案。
对于基础例程来说,你可以把$GPGGA里的使用卫星数和HDOP值打印出来观察——卫星数越多、HDOP越小,定位精度就越好。我实测在开阔楼顶,卫星数能稳定在10颗以上,HDOP小于1,经纬度抖动在几米以内。在窗边测试时,卫星数会掉到4到6颗,坐标会有明显漂移。
最后分享一个我自己的调试习惯:先不用STM32,直接用USB转TTL接GPS模块,在电脑串口助手上确认模块能正常输出、并能看到定位成功后的$GPRMC里出现A状态,然后再接到单片机上联合调试。这样可以把问题快速隔离在“GPS模块本身”还是“STM32代码”这两者之间,省下大把排查时间。这个流程看起来简单,但很多人就是因为跳过了这一步,在单片机代码里反复找问题,最后才发现是天线位置放错了。
本文还有配套的精品资源,点击获取