简介STM32F103ZET6结合STM32CubeMX的GPS例程面向嵌入式入门开发者演示如何基于HAL库快速配置UART、GPIO与中断实现GPS模块数据接收与NMEA协议解析。资源共188个文件压缩包8.58MB包含.ioc工程配置、Keil MDK项目文件、HAL驱动源码.c/.h、编译中间文件.o/.crf等目录结构划分了Core、Drivers、Hardware等模块便于对照学习。已有220人下载学习。通过本资源读者可直接用CubeMX打开配置文件查看时钟、串口及中断的完整初始化流程并参考UART收发函数和NMEA报文解析代码理解从硬件初始化到应用层数据处理的完整链路适合希望掌握STM32外设配置与GPS定位应用的开发者。1. 把 GPS 模块接到 STM32F103ZET6 上这套 CubeMX 例程到底帮你解决了什么如果你手里有一块 STM32F103ZET6 开发板、一个 GPS 模块常见的 NEO-6M、ATK-S1216 这一类想看到经纬度、速度、时间这些信息但不想从零去翻寄存器写 USART 驱动那这就是你需要的例程。它解决的问题很具体用 STM32CubeMX 完成串口初始化、引脚分配和时钟配置再配合 HAL 库把 NMEA 0183 协议里的 $GPRMC 语句解析成可读的定位数据整个过程 30 分钟内能跑通。这套例程适合两类人一类是正在做课设或项目需要快速让 GPS 数据出现在串口调试助手里的新手另一类是已经能点灯、会配 GPIO但第一次碰 UART 接收和字符串解析的开发者。它不玩花活直接给你一条从“硬件接线”到“串口打印经纬度”的完整链路。你不需要先搞懂中断优先级和 DMA 映射的每个细节但我会把关键参数和容易翻车的地方讲清楚免得你对着一个黑匣子调试半天还不知道问题在哪。2. GPS 例程的硬件与协议基础串口接线、电平匹配和 NMEA 语句结构2.1 先看硬件GPS 模块和 STM32F103ZET6 怎么接线GPS 模块和单片机之间走的是 UART 串口模块自己把接收到的卫星数据打包成 ASCII 字符串发出来。绝大多数 GPS 模块默认波特率是 9600少数是 115200这点后面会反复提到因为它是新手最容易踩的坑。接线其实就四根线GPS 模块的 TX 接 STM32F103ZET6 的 RXGPS 模块的 RX 接 STM32 的 TX再共地电源接 3.3V 或 5V。STM32F103ZET6 的串口引脚都引到了板子排针上我一般选择 USART1也就是 PA9TX和 PA10RX。用 USART1 的好处是板载的 USB 转串口芯片通常直接连在它上面你调试时打开串口助手就能同时看到 GPS 数据和 printf 输出不用再接第二根 USB 线。注意电平老式 GPS 模块输出的是 5V TTL 电平STM32F103 的 GPIO 最大耐压是 5V但为了稳妥最好确认模块是 3.3V 供电版本或带电平转换。如果模块标的是 5V 供电但你用 3.3V 给它供电模块也能工作只是天线部分灵敏度可能受影响反过来你把 5V 信号直接灌进 PA10虽然 F103 耐压够但我还是建议先量一下模块 TX 引脚的空闲电平确认是 3.3V 再直连——这是血泪经验烧坏过引脚的人不在少数。2.2 NMEA 0183 协议$GPRMC 里藏着你要的一切GPS 模块上电后输出的并不是“经度 116.39”这种直白的话而是一行行 $GPRMC、$GPGGA、$GPGSV 开头的 ASCII 字符串它们统称为 NMEA 0183 语句。对应用开发来说最重要的就是 $GPRMC它包含了推荐最小定位信息UTC 时间、定位状态、纬度、经度、速度、航向和日期。一条典型的 $GPRMC 长这样$GPRMC,073652.00,A,3105.52312,N,12122.16290,E,0.04,83.19,160623,,,D*76按逗号分隔字段编号从 0 开始字段内容含义0$GPRMC语句标识符1073652.00UTC 时间时分秒格式2A定位状态A 表示有效V 表示无效33105.52312纬度度分格式31 度 05.52312 分4N北纬/南纬512122.16290经度度分格式121 度 22.16290 分6E东经/西经70.04以节为单位的地面速度883.19航向角度9160623UTC 日期日/月/年解析这块的根本逻辑不是去背协议文档而是先把整行按逗号切割再取固定索引。这里的“度分格式”是个真正的坑3105.52312 不是 31.0552312而是 31 度 05.52312 分要转成十进制度得拿“分”部分除以 60 再加到“度”上。很多人在此翻车算出来的坐标偏离了几公里还以为模块精度差。2.3 为什么例程用中断接收而不是轮询GPS 模块的串口是个持续数据流每秒输出一到多帧如果主循环里用阻塞方式等数据整个程序就被拖死了——你没法同时处理按键、显示屏幕或者写入 SD 卡。所以成熟例程的处理方式是串口接收中断把每个字节按顺序丢进缓冲区主循环再扫描缓冲区里的完整 NMEA 行来做解析。这里有个概念区分CubeMX 里 USART 有“中断接收”和“DMA 接收”两种方式。GPS 数据量不大9600 波特率下每秒约 1KB用最基础的HAL_UART_Receive_IT逐字节接收完全够用DMA 在这里是锦上添花不是必须。我的建议是先用中断版本跑通遇到丢字节再换 DMA 或者加环形缓冲区不必一步到位。3. 在 CubeMX 里完成工程初始化时钟、串口和中断配置的具体步骤3.1 时钟树配置外部晶振与 72MHz 主频打开 STM32CubeMX选择芯片型号 STM32F103ZET6。首先是时钟配置开发板上一般有 8MHz 外部晶振HSECubeMX 默认可能用的是 HSI内部 8MHz RC这时候即使工程生成成功串口波特率误差也会大得离谱。操作路径RCC → HSE → 选择 Crystal/Ceramic Resonator然后在 Clock Configuration 页面里把输入频率填 8PLL 倍频系数设置成 9系统时钟就变成 72MHz。APB1 总线时钟保持默认的 36MHzUSART1 挂在 APB2 上时钟是 72MHz后面算波特率分频时会用到这个数字。生成工程前还有个容易被忽略的设置在 SYS 里把 Debug 选为 Serial Wire。如果这一步漏了程序烧进去后你接 ST-Link 第二次下载可能会报找不到设备——那感觉跟板子烧了一样实际上只是 SWD 引脚被重新分配成了普通 GPIO复位键按一下能救回来。另外 PA9/PA10 默认功能是 USART1不需要额外配置 Alternate FunctionCubeMX 会自动设好。3.2 配置 USART1异步模式、波特率和中断在 Connectivity 里打开 USART1Mode 选 Asynchronous硬件流控制那些保持 Disable。波特率这里先设 9600因为多数 GPS 模块出厂固件默认就是 9600-8-N-1字长 8 Bits奇偶校验 None停止位 1。参数清单如下配置项值说明ModeAsynchronous仅用 TX/RX关闭流控Baud Rate9600与 GPS 模块波特率一致Word Length8 Bits标准 NMEA 格式ParityNone无校验Stop Bits1单停止位NVICUSART1 global interrupt必须勾选 Enable勾选 USART1 全局中断这步很容易漏。有些人配置完没勾 NVIC生成代码后HAL_UART_Receive_IT调用一次就再也不进回调了因为中断根本没开启。在 CubeMX 的 NVIC Settings 页面里确认 USART1 global interrupt 的 Enable 框勾上同时把抢占优先级设个不太高的值即可。3.3 生成工程后的代码骨架main 函数与串口接收衔接生成工程使用的是 MDK-ARM 工具链的话main.c 里会自动生成 uart 初始化函数 MX_USART1_UART_Init。你需要做的是在 main 函数的用户代码区USER CODE BEGIN 和 USER CODE END 之间加几行启动接收的代码/* USER CODE BEGIN 2 */ uint8_t gps_byte; HAL_UART_Receive_IT(huart1, gps_byte, 1); /* USER CODE END 2 */HAL_UART_Receive_IT的第三个参数是 1意思是一次接收一个字节收完触发中断回调后需要重新调用它在回调里重新「续杯」。这里不能把它理解成“启动接收一批数据”它是单次接收的触发开关必须每次回调后重新开启。对应的回调函数写在 stm32f1xx_it.c 或者 main.c 的 USER CODE 区都行void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 处理 gps_byte存入缓冲区 HAL_UART_Receive_IT(huart1, gps_byte, 1); } }这段代码的逻辑是系统在收到一个字节后进入HAL_UART_RxCpltCallback你在里面把字节存到自己定义的数组里同时立刻调用HAL_UART_Receive_IT注册下一次接收。如果忘记第二次调用串口就收不到后续任何数据这是新手最容易遇到的“为什么只收到一个字节”的玄学问题。4. 解析 GPS 数据并输出定位结果从裸字节到可读的经纬度4.1 用环形缓冲区暂存串口字节流直接在主循环里判断缓冲区字符会导致一个麻烦一个完整的 NMEA 语句可能在你解析到一半时还没来全或者前一条语句的后半段和后一条的前半段混在一起。我一般会在串口回调里把所有字节先存进环形缓冲区主循环再按行取出来处理。环形缓冲区的实现不复杂用一个定长数组加读、写两个索引即可。写索引只在串口中断里动读索引只会在主循环里动这样单生产者单消费者的模型不会出现数据竞争#define GPS_BUFFER_SIZE 512 uint8_t gps_buffer[GPS_BUFFER_SIZE]; volatile uint16_t gps_write_index 0; volatile uint16_t gps_read_index 0; uint8_t gps_byte; void GPS_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { uint16_t next (gps_write_index 1) % GPS_BUFFER_SIZE; if (next ! gps_read_index) { gps_buffer[gps_write_index] gps_byte; gps_write_index next; } /* 继续接收下一字节 */ HAL_UART_Receive_IT(huart1, gps_byte, 1); } } uint8_t GPS_PopByte(uint8_t *out) { if (gps_read_index gps_write_index) return 0; *out gps_buffer[gps_read_index]; gps_read_index (gps_read_index 1) % GPS_BUFFER_SIZE; return 1; }逻辑说明GPS_PopByte返回 1 表示取出一个字节返回 0 表示缓冲区空。主循环可以先不断取字节拼成完整的一行后再交给解析函数。缓冲区大小 512 是够用的一条 $GPRMC 最长不到 100 字节512 足够放下好几条完整语句不会出现“存不下”的情况。4.2 解析 $GPRMC定位状态、度分转换和时间提取完整的解析函数分两步先按换行符切出一帧再对帧内做逗号分割。这里有个容易犯的错数据帧可能以\r\n结尾也可能是只以\n结尾直接按\r切会丢内容。我一般先把帧头整理成 NULL 结尾的字符串再用strstr定位$GPRMC最后用strtok按逗号切分。#include string.h #include stdio.h typedef struct { uint8_t valid; /* 1 定位有效0 无效 */ float latitude; /* 十进制度 */ float longitude; /* 十进制度 */ float speed_knots; /* 速度节 */ float course_deg; /* 航向度 */ uint8_t hour, minute, second; } GpsData; void GPS_ParseLine(char *line, GpsData *data) { char fields[14][16]; uint8_t field_count 0; char *token strtok(line, ,); while (token ! NULL field_count 14) { strncpy(fields[field_count], token, 15); fields[field_count][15] \0; field_count; token strtok(NULL, ,); } if (field_count 10) return; /* 字段不够丢弃 */ if (strncmp(fields[0], $GPRMC, 6) ! 0) return; if (fields[2][0] ! A) { >#include stdio.h int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 100); return ch; }在 MDK-ARM 工程里还需要在 Target Options 的 MicroLIB 选项前打勾。原因很简单标准 C 库的 printf 实现在无操作系统环境下会尝试调用一些底层系统函数MicroLIB 是个精简版能直接支持 retarget 到串口省去大量文件系统相关的胶水代码。这个勾不打编译能过但运行时会死在 printf 第一次调用——有人怀疑是堆栈溢出其实只是标准库没被 retarget 到 UART。主循环里的调用方式while (1) { if (GPS_ParseFrame(gps_data)) { /* 取一帧并解析 */ if (gps_data.valid) { printf(LAT: %.6f, LON: %.6f, SPD: %.1f kn\r\n, gps_data.latitude, gps_data.longitude, gps_data.speed_knots); } else { printf(NO FIX\r\n); } } }到这里一套完整的 GPS 数据链路已经通了串口中断收字节 → 环形缓冲暂存 → 主循环按行解析 → printf 打印。把这个例程的中间环节拆开之后你会发现真正需要你动手写的代码并不多关键是把每个环节的数据流接口理清楚。5. 避坑记录GPS 例程移植中的 5 个典型问题5.1 现象串口发配置命令有回显但收不到 GPS 数据原因GPS 模块在室内或者天线没有对准天空时完全没有卫星信号输出是空的。更常见的其实是波特率不对——模块默认 9600工程里配成了 115200。我用过的某款模块上电后先输出几条 9600 波特率的 boot 信息如果波特率不匹配串口助手看到的就是乱码或者什么都不显示。解决先用 USB 转 TTL 模块直接接 GPS在电脑串口助手上确认模块输出内容和波特率再回头查工程配置。另外检查供电是否足 3.3VGPS 模块的搜星电流经常到 50mA 以上开发板的 3.3V LDO 通常够用但劣质杜邦线压降大导致模块电压低于 3.0V 后不工作。5.2 现象数据能收到但是乱码偶尔丢字符原因串口引脚虽然连对了但 GPS 模块 TX 的电平可能是 5VSTM32 的 RX 引脚虽然有 5V 容忍但不代表推荐长期工作在 5V 下。还有可能是接线太长、接触不良或者 GND 只接了一根线模块和单片机之间电位不共地信号参考电压漂移。解决用万用表量 GPS 模块 TX 引脚对 GND 的空闲电平如果接近 5V加一个电平转换模块确认模块 GND、单片机 GND、USB 转串口 GND 三者是接通的。杜邦线换成短一点的排线串口数据线的长度控制在 20cm 以内。5.3 现象定位成功了但经纬度计算出来离实际位置几十公里原因这是度分转换公式写错了。有人直接把原始字段 3105.52312 当成 31.0552312 用还有人把lat_min算成了lat_raw - (int)lat_raw漏掉了“分是两位整数”这个前提。GPS 模块输出的度分格式里度是前两位经度是前三位分是剩余部分转换公式是度 分/60不是简单的除以 100。解决打印中间变量把lat_deg、lat_min分别打出来对照手机地图上的真实坐标逐项核对。还有一点要注意NMEA 里的纬度和经度字段可能带小数点后不同位数strtof不会出错但你要确保字符串没有被其他逻辑截断。5.4 现象CubeMX 重新生成代码后自己写的 GPS 解析函数全没了原因CubeMX 在生成代码时会重写 main.c 和 stm32f1xx_hal_msp.c 等文件。凡是写在 USER CODE 区域之外的自定义函数下次生成工程时会被覆盖。这个“后悔药”其实是有准备的CubeMX 把所有可以安全保留的代码段都用/* USER CODE BEGIN x */和/* USER CODE END x */包裹起来。解决把所有自定义函数放在 USER CODE 区域内。新建解析模块文件是更稳妥的做法比如gps_parser.c和gps_parser.h它们不在 CubeMX 的管理范围内生成代码不会碰这些文件。main.c 里只保留中断回调和循环调用。5.5 现象UTC 时间跟本地时间差 8 个小时原因GPS 模块输出的时间是世界协调时UTC北京时间是东八区要加 8 小时。很多人拿到hour字段直接显示发现下午两点变成早上六点怀疑模块坏了。解决在打印时间前对小时做(hour 8) % 24操作。同时注意日期可能跨天如果 UTC 时间是 23 点北京时间就是第二天早上 7 点日期也要相应加一天。这个逻辑看起来简单但日期进位容易遗漏我用一个day_adjusted标志位来处理跨天。6. 进阶技巧用 Haversine 公式计算距离并用假数据离线验证解析代码6.1 Haversine 公式算两点间的直线距离拿到两组经纬度后最常见的需求是计算当前点和目标点的距离。地球不是平的经纬度直接做勾股定理误差很大工程上常用 Haversine 公式它基于球面三角学在几十公里范围内精度能到米级对应用场景足够。#include math.h #define PI 3.14159265358979f #define EARTH_RADIUS_KM 6371.0f static double DegToRad(double deg) { return deg * PI / 180.0; } double GPS_DistanceKm(double lat1, double lon1, double lat2, double lon2) { double dlat DegToRad(lat2 - lat1); double dlon DegToRad(lon2 - lon1); double a sin(dlat / 2.0) * sin(dlat / 2.0) cos(DegToRad(lat1)) * cos(DegToRad(lat2)) * sin(dlon / 2.0) * sin(dlon / 2.0); double c 2.0 * atan2(sqrt(a), sqrt(1.0 - a)); return EARTH_RADIUS_KM * c; }EARTH_RADIUS_KM取 6371 公里是常用近似值需要更高精度可以调整到 6371.0088但对 GPS 定位的误差来说意义不大。注意atan2比asin(sqrt(a))更适合这里能避免 a 超过 1 时产生 NaN——实际运算中浮点误差可能导致 a 略大于 1用 asin 的话结果直接变成非数排查起来相当隐蔽。6.2 离线验证没有卫星信号也能调试解析器GPS 模块在室内基本搜不到星但你不能等天气好了跑到室外去调代码。正确做法是准备一条固定的 NMEA 语句用串口调试助手手动发到 STM32 的 RX 引脚或者直接用模拟函数把这条语句塞进解析器里验证输出。我用过的调试字符串是char test_line[] $GPRMC,073652.00,A,3105.52312,N,12122.16290,E,0.04,83.19,160623,,,D*76\r\n;把这条字符串按字节模拟串口中断喂进环形缓冲区然后看主循环解析出来的经纬度是否等于 31.092052、121.369382。这个方法的好处是把“硬件链路”和“软件解析”彻底分离如果解析输出正确问题只可能在接线和配置如果解析输出不对直接在那个函数上调试就行不用抱着示波器去插天线。从那以后我每次移植 GPS 相关代码都强制走一遍这三步先用模拟数据验证解析器输出再接到模块看原始数据流最后才写进业务代码。这次例程我帮你把这三步对应的代码都理出来了你照着顺一遍能从 CubxMX 生成到串口打出经纬度避开大多数坑。希望帮到你。本文还有配套的精品资源点击获取