恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32的GPS例程:CubeMX与DMA解析NMEA
首页
资讯中心
/
STM32的GPS例程:CubeMX与DMA解析NMEA
STM32的GPS例程:CubeMX与DMA解析NMEA
发布时间:2026/10/15 13:21:10
简介STM32F103ZET6结合STM32CubeMX使用GPS模块的完整例程面向嵌入式初学者与开发者演示如何通过图形化配置工具快速生成初始化代码并完成UART通信、中断处理与NMEA协议解析。资源共188个文件压缩包8.58MB涵盖大量h与c源码、HAL库驱动、Keil工程文件uvprojx、axf、hex及ioc配置目录结构清晰便于对照学习。目前已有220人浏览下载适合需要掌握STM32CubeMX配置流程、理解GPS串口数据解析或正在搭建类似定位应用的开发者。例程配套说明细致覆盖时钟设置、GPIO/UART初始化、接收中断及NMEA报文拆分等关键环节并包含MDK-ARM编译中间文件与硬件相关文档可作为从零上手STM32与外部模块交互的实践参考。1. 从串口乱码到解析出经纬度STM32F103ZET6 与 CubeMX 的 GPS 例程很多人把 GPS 例程想成「串口收到什么就打印什么」等真把模块接上去才发现收到的是一堆以 $ 开头、逗号分隔的 NMEA 句子波特率不对是乱码、天线没放窗边是 NaN、解析函数写错是秒崩。这个基于 STM32F103ZET6 的 CubeMX GPS 例程核心价值不在那几句串口收发代码而是把「CubeMX 初始化配置 → DMA 不定长接收 → NMEA 语句解析 → 经纬度/时间/速度结构化输出」这条链路完整打通。适合两类人一是刚把标准库玩熟、想转 HAL 库的入门者二是在做飞行器、小车或便携设备定位、不想在串口协议解析上再花三天的人。你需要准备的只有一块板子、一个 3.3V TTL 电平的 GPS 模块和一根 USB-TTL剩下的看这篇就够。2. 硬件连接与 CubeMX 初始化先让数据流进芯片2.1 GPS 模块选型参数为什么必须 TTL 电平GPS 模块的接口电平是个容易被忽略的坑。市面常见的模块分两种一种是 3.3V TTL 电平UART TX/RX 直接能连 STM32 的 PA9/PA10另一种是 RS-232 电平输出是正负 12V直接进芯片引脚会烧毁 IO。选模块时别只看「串口输出」先确认模块原理图上的电平转换芯片。若手头只有 RS-232 输出的模块中间必须加 MAX3232 电平转换芯片数据路径变成 GPS_TX → MAX3232 → STM32_RX。除电平外还要确认模块的默认波特率。大部分模块出厂是 9600但有些支持 115200 的型号需要先用上位机发指令切换。例程里所有串口配置都以 9600 为基准拿到模块第一件事是查默认参数不是直接改代码。模块的 VCC 供电则要看清是 3.3V 还是 5VSTM32F103ZET6 的板载 3.3V LDO 电流余量通常够一个 GPS 模块但如果同时驱动屏幕和其他外设建议单独从 USB 5V 取电避免模块启动瞬间拉低主控电压导致复位。2.2 CubeMX 工程配置USART1 与 DMA 的搭配打开 CubeMX 新建工程芯片选 STM32F103ZET6。RCC 时钟配置里HSE 选择 Crystal/Ceramic Resonator时钟树把 SYSCLK 拉到 72MHz。注意 APB2 总线的 USART1 时钟默认也是 72MHz这里没问题。USART1 的配置是重头戏。模式选 Asynchronous波特率设 9600数据位 8无校验停止位 1。这一步多数人不会错容易错的是 DMA 设置。在 DMA Settings 标签页给 USART1_RX 添加 DMA 通道方向 Peripheral To Memory模式 Normal。这里强调一下网上很多例程用 Circular 模式配合空闲中断做不定长接收但在这个工程里我用 Normal 模式加手动重开逻辑更直白也方便后续判断「一帧数据是否完整接收完」。引脚分配上PA9 是 USART1_TXPA10 是 USART1_RX。如果你的开发板板载了 GPS 模块接口留意板子原理图上是否默认把 PA9/PA10 引到了别的外设比如 RS-485 芯片这会导致信号被转成差分电平直接测不到数据。拿到板子先看原理图确认引脚归属。CubeMX 最后一步是生成代码。Project Manager 里 Toolchain 选 MDK-ARMMinimal Stack Size 保持默认。生成后用 Keil 打开先编译一次确认基础工程没问题再往下加代码。2.3 串口参数速查表参数项配置值说明MCUSTM32F103ZET664 引脚以上 LQFP 封装FLASH 512KB时钟HSE 8MHzPLL 倍频到 72MHzAPB2 总线 72MHzUSART1 挂在上面USART1波特率 96008N1与 GPS 模块默认参数对应DMAUSART1_RXNormal 模式数据长度设置为缓冲数组大小GPS 模块电平3.3V TTL5V 模块需加电平转换若后续发现模块输出的是 115200 波特率只需把 CubeMX 中 USART1 的 Baud Rate 改成 115200重新生成代码即可DMA 配置不受影响。改完记得先烧录后接模块别在模块通电状态下热插拔杜邦线瞬间浪涌容易把串口引脚打坏。3. 串口接收方案DMA 空闲中断与环形缓冲的实现3.1 为什么不用轮询和普通中断GPS 数据是持续不断输出的每秒钟大约 5 到 10 条 NMEA 句子每条长度不定从 30 字节到 80 字节都有。用轮询方式在 while(1) 里不断查 RXNE 标志位CPU 会被串口数据占掉一大块尤其当你还要同时跑 OLED 刷新、电机控制或卡尔曼滤波时轮询就是灾难。普通串口中断的痛点则是频繁进出中断。波特率 9600 时每个字节大约 1ms 中断一次单字节中断本身开销不大但如果你做的是「接收到换行符就解析一帧」的逻辑还得有一个字节一个字节地拼帧判断逻辑繁琐不说万一处理函数里写了延时下一个字节到了进不了中断数据就丢了。DMA 空闲中断方案解决的是「批量搬运 帧边界识别」。DMA 控制器把串口接收寄存器里的数据按字节搬到内存数组不占 CPU。当串口总线空闲超过一个字节时间即一帧 NMEA 句子发完后的停顿硬件会产生 IDLE 中断。程序在 IDLE 中断里读一下当前 DMA 还剩下多少没搬用缓冲总长度减掉剩余量就知道这一帧有多少字节。整个过程 CPU 只在帧边界做事其余时间 DMA 全包。3.2 初始化代码DMA 与串口的绑定细节CubeMX 生成的代码里DMA 已经绑定到 USART1_RX但你需要自己在用户代码区开启 DMA 接收并编写空闲中断回调。核心初始化代码如下#define GPS_RX_BUF_SIZE 256 uint8_t gps_rx_buf[GPS_RX_BUF_SIZE]; // DMA 搬运目标缓冲 volatile uint16_t gps_frame_len 0; // 当前帧长度 volatile uint8_t gps_frame_ready 0; // 帧接收完成标志 // 开启串口 DMA 接收数据全部搬进 gps_rx_buf HAL_UARTEx_ReceiveToIdle_DMA(huart1, gps_rx_buf, GPS_RX_BUF_SIZE); // 空闲中断回调函数由 HAL 库在检测到 IDLE 时调用 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { gps_frame_len Size; gps_frame_ready 1; } }这里用的是HAL_UARTEx_ReceiveToIdle_DMA注意函数名里带了 UARTEx这是 HAL 库升级后专门支持空闲中断的接口旧版的HAL_UART_Receive_DMA不会产生空闲中断。Size 参数是 HAL 自动计算出的当前接收字节数不需要手动去读 DMA 的 NDTR 寄存器。收到一帧后数据已经在 gps_rx_buf 里帧长度存在 gps_frame_len。这个方案的边界在于DMA 缓冲大小设了多少单帧最多就能收到多少字节。GPS 最长的一帧是 GSV 星历语句可能超过 80 字节所以缓冲设 256 稳够。如果你用 Circular 模式一帧数据可能被 DMA 分成两段存在缓冲的尾部与头部还需要额外处理环形拼接而 Normal 模式则天然避开了这个问题。3.3 帧处理主循环解析前的数据搬运主循环里拿到 gps_frame_ready 标志后不能直接在回调里做解析。GPS 帧解析里有大量逗号查找和字符串转数字的操作耗时不定放在中断里会把其他中断的响应时间拉长。正确做法是先把帧数据拷贝到独立的解析缓冲区清掉标志位再回主循环慢慢解析char gps_parse_buf[128]; uint16_t parse_len 0; while (1) { if (gps_frame_ready) { // 关闭中断防止在拷贝过程中帧被覆盖 __disable_irq(); parse_len gps_frame_len; memcpy(gps_parse_buf, gps_rx_buf, parse_len); gps_frame_len 0; gps_frame_ready 0; __enable_irq(); // 解析 gps_parse_buf 中的 NMEA 语句 nmea_parse(gps_parse_buf, parse_len); } }临时关闭中断的方式在拷贝数据时防止新帧写入 gps_rx_buf 造成数据错位。这里有个细节关闭中断的时间不能长memcpy 128 字节在 72MHz 下是微秒级操作安全。如果解析串口数据量大、处理逻辑重建议换用双缓冲乒乓操作一个缓冲在拷贝、另一个在接收但从这个例程的使用场景看单缓冲加临界区保护已经足够稳。注意代码里的gps_frame_ready和gps_frame_len都是 volatile原因是它们被中断函数和主循环两个执行流访问不加 volatile编译器可能优化成只从寄存器取值导致主循环永远读不到中断里更新的值这是嵌入式里极其隐蔽的坑。4. NMEA 报文解析把 $GPRMC 变成结构体变量4.1 GPS 数据流里最常用到的四条语句GPS 模块每秒输出多条语句常见的有 GGA、RMC、GSV、GSA。做定位解析其实只需要关注两条RMC 提供经纬度、速度、日期、时间和定位状态GGA 提供海拔和卫星数。其余 GSV 是卫星分布信息用于观察搜星情况不需要在单片机里解析。例程以 RMC 为主解析目标结构化输出到 GPS 结构体里typedef struct { uint8_t valid; // 0x01 定位有效0x00 无效 float longitude; // 经度单位度东正西负 float latitude; // 纬度单位度北正南负 float speed_knots; // 地面速度单位节 float course; // 航向角单位度 uint8_t hour, minute, second; // UTC 时间注意是 UTC 不是北京时间 uint8_t day, month; // 日期 uint16_t year; // 年份 } gps_data_t;RMC 语句的典型格式是$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A字段分别是 UTC 时间、定位状态A 有效、V 无效、纬度、北纬/南纬、经度、东经/西经、速度节、航向角、UTC 日期、磁偏角、磁偏角方向、校验和。NMEA 里纬度输出格式是 ddmm.mmmm需要换算成十进制度数才能用于地图和导航计算换算公式是「度 分/60」。4.2 解析函数的逐段实现与坑点解析函数的目标是从一帧字符串里提取出 RMC 语句的各个字段。先做帧头匹配和语句筛选int nmea_parse(char *buf, uint16_t len) { // 先做基础校验帧尾的异或校验 if (!nmea_checksum_ok(buf, len)) return -1; // 判断语句类型 if (strncmp(buf 1, GPRMC, 5) 0) { return parse_rmc(buf, len); } return 0; }校验和算法很简单从 $ 之后第一个字符开始到 * 之前的所有字符做异或结果与 * 后的两位十六进制比较。实际踩坑点在于单片机里的字符串比较和指针偏移。strncmp 第一个参数传入 buf 1是因为 buf[0] 是 $真正的语句类型从索引 1 开始。解析 RMC 字段时用 strtok 或手写逗号查找。STM32 的 C 库如果开了 Microlibstrtok 是可用的但要注意它会在原字符串上写 \0 分割符会把 gps_parse_buf 的内容破坏。如果后续还要调试看原始帧先拷贝一份再解析。字段截取与经纬度转换代码static int parse_rmc(char *buf, uint16_t len) { char *p buf; int field 0; char lat_str[12], lon_str[12]; char *lat_p lat_str, *lon_p lon_str; while (p *p ! \0 field 10) { char *comma strchr(p, ,); if (!comma) break; int field_len comma - p; switch (field) { case 1: // 定位状态A 或 V gps.valid (p[0] A) ? 1 : 0; break; case 2: // 纬度字段格式 ddmm.mmmm memcpy(lat_str, p, field_len); lat_str[field_len] \0; break; case 4: // 经度字段 memcpy(lon_str, p, field_len); lon_str[field_len] \0; break; case 6: // 地面速度节 gps.speed_knots atof(p); break; } // 跳转到下一个逗号之后 field; p comma 1; } // 经纬度转换成十进制度 gps.latitude convert_nmea_to_dec(lat_str); gps.longitude convert_nmea_to_dec(lon_str); return 0; }这段代码里有个容易疏忽的地方case 里直接memcpy(lat_str, p, field_len)但 NMEA 的经纬度字段里没有小数点和字母直接用atof(lat_str)得到的是 ddmm.mmmm 原始值必须先用 convert 函数处理成度为单位。转换函数里要处理前导零情况例如纬度 4807.038 毫秒级精度下正确结果是 48 07.038/60 48.1173 度。4.3 校验函数与时间转换的附加处理校验和函数如下static int nmea_checksum_ok(char *buf, uint16_t len) { uint8_t calc 0; uint16_t i; // 从第 1 个字符开始异或遇到 * 停止 for (i 1; i len buf[i] ! *; i) { calc ^ buf[i]; } // 字符串转十六进制与计算值比较 char sum_str[3]; sum_str[0] buf[i 1]; sum_str[1] buf[i 2]; sum_str[2] \0; uint8_t sum (uint8_t)strtol(sum_str, NULL, 16); return (sum calc); }注意strtol(sum_str, NULL, 16)转换时NMEA 校验和是小写字母strtol 都能处理。但若你手写十六进制转换一定要先判断字符在 0-9、A-F、a-f 哪个区间别漏掉小写。UTC 时间转北京时间很简单加 8 小时小时数超过 24 就减去 24日期相应加一天。如果产品只在国内用这个转换必须在解析后马上做因为日期跨天会直接影响 RTC 校准逻辑。例程里封装了utc_to_beijing()函数处理这个边界时还要判断月份天数二月份与闰年也不能漏不然用户 2 月 28 日晚上 10 点看到的日期就是 3 月 1 日了。5. 避坑与常见问题排查五次真实调试记录先直接放结论GPS 例程无外乎「数据进不来」「数据进来了是乱的」「数据对但是位置不对」三类问题每一类都有固定排查路径。以下是五个我实际调试中碰过的场景都按「现象 → 原因 → 解决」的链条记录下来。现象一串口助手能看到 GPS 原始数据但 STM32 这边 gps_frame_ready 永远是 0。原因排查后发现是 DMA 通道被 CubeMX 分配到 USART1_RX 后引脚复用功能没确认。STM32F103 的 USART1_RX 默认映射在 PA10 的复用功能上CubeMX 生成代码时确实会配置 GPIO但如果我在引脚配置面板里手动把 PA10 拖成了 GPIO_Input生成的初始化代码会把复用功能覆盖掉。解决方法是回到 CubeMX 的 Pinout 视图右键 PA10 选择 USART1_RX确认引脚颜色变成绿色表示已分配给外设重新生成代码。类似的PA9 选 USART1_TX两个引脚不能搞反。现象二数据能收到但 gps_parse_buf 里出现两帧拼接在一起的数据解析结果时对时错。原因是 Single Buffer 方案下一帧数据刚拷贝完下一帧就开始往同一缓冲写入。GPS 模块每秒连续输出多条语句两帧之间的间隔小于主循环处理时间时会发生覆盖。解决方法是缓冲大小从 256 降到 128同时把主循环的解析逻辑尽量精简更彻底的做法是双缓冲接收与处理轮换使用两块内存区代价是内存占用增加 128 字节。对于 F103ZET6 这种 512KB Flash、64KB RAM 的芯片内存完全不是压力。现象三经纬度打印出来是 4807.038 这种原始格式不是 48.1173。原因很明显解析代码里直接对 NMEA 原始字符串调 atof没做 ddmm.mmmm 到十进制度的换算。解决是增加换算函数核心逻辑是float convert_nmea_to_dec(char *nmea_str) { // 找小数点的位置把度与分分离 char *dot strchr(nmea_str, .); if (!dot) return 0.0f; // 小数点前至少 4 位前 2 位是度后面是分 int deg_len (dot - nmea_str) - 2; char deg_str[3] {0}; strncpy(deg_str, nmea_str, deg_len); char min_str[12] {0}; strcpy(min_str, nmea_str deg_len); float deg atof(deg_str); float min atof(min_str); return deg min / 60.0f; }写这段函数时有两个边界纬度固定 ddmm.mmmm 即小数点前四位但经度是 dddmm.mmmm 小数点前五位南纬和西经要加强制负号处理NMEA 里的 S 和 W 字母要保留到解析状态里。现象四模块放在桌面能收到数据但经纬度一直显示 0.000000。原因是模块还没完成冷启动搜星。刚上电的 GPS 模块需要 30 秒到几分钟不等的时间来下载星历室内或窗口朝北时定位极慢。valid 标志为 0 时RMC 语句已经输出了字段只是定位状态是 V无效。解决方法是先看 GGA 语句里的卫星数量字段卫星数小于 4 时数据不可用。程序侧建议只在 valid 1 时才更新坐标避免 NaN 或无效值污染后续算法。代码里加一个提示valid 为 0 时不更新 GPS 结构体的经纬度只更新状态。现象五程序运行一段时间后串口接收完全停摆DMA 好像死了。原因在 Normal 模式下DMA 搬运完设定长度后不会自动重装载触发。常规做法是在 DMA 传输完成中断里手动重新启动 DMA但 HAL_UARTEx_ReceiveToIdle_DMA 在收到一帧数据后不会自动重开接收。解决方法是每次解析完都重新调用一次HAL_UARTEx_ReceiveToIdle_DMA(huart1, gps_rx_buf, GPS_RX_BUF_SIZE)把它放在gps_frame_ready 0之后。代码里可以在HAL_UARTEx_RxEventCallback里直接重新开启这样主循环完全不用管 DMA 状态彻底避免丢帧停摆问题。6. 验证定位精度的两种实测方案与一个工程习惯测试 GPS 例程不能用「能打印坐标就完事」的态度坐标的跳动、时间的更新频率、冷启动时间都是要验证的指标。推荐两个实测方案都不需要专业仪器。第一个方案固定位置静态测漂移。把开发板和模块放在窗口或天台保持天线位置完全不动运行程序每秒钟记录一次经纬度连续采集 5 分钟。用串口助手把数据导出到 Excel计算最大偏移和平均偏移。模块本身的定位精度在无差分的情况下是 2 到 5 米那么坐标会在真实位置周围画圈。如果发现坐标单向漂移且误差越来越大基本都是模块的天线馈线太长或受到遮挡和代码无关。第二个方案移动位置测响应。带着模块步行绕一个 50 米见方的街区走一圈回到起点对比起点和终点的经纬度差值。GPS 模块存在多路径效应城市峡谷环境飘个 20 米是常见的事所以终点和起点有一定偏差正常。这套验证的核心是确认解析出的速度字段RMC 的 field 7在行走时能稳定在每小时 3 到 5 公里并且方向角field 8随转向平滑变化。这两个字段最容易暴露解析函数的字段编号错位问题。工程习惯方面我会在每次烧录新固件前做一遍原始帧跟踪。用一个 USB-TTL 直接在 PC 上监听模块的 TX 输出确认「模块本身输出正常」再把模块的 TX 断开用 USB-TTL 去监听 STM32 的 PA9 TX 引脚确认「单片机解析后输出正常」。这个习惯帮我定位过无数次问题包括模块波特率被人为改掉、连接线断芯之类的隐性故障。从那以后我每次写完 GPS 驱动都会强制走一遍双通道监听流程先信数据源、再信代码。这个流程听着笨但在现场调试里比任何仿真器都好用。希望帮到你。本文还有配套的精品资源点击获取