资讯动态

STM32 GPS定位与OLED显示实战:NMEA解析、DMA接收与避坑指南

发布时间:2026/10/20 17:30:14 来源:尧图企业网站定制
简介这是一套面向嵌入式初学者与物联网开发者的STM32GPS定位系统学习资料围绕STM32微控制器、GPS模块与OLED显示屏的协同工作展开可用于车载导航、户外运动设备等场景的原型开发。资源包共152个文件约2.09MB以C语言源文件与头文件为主体涵盖STM32外设驱动、GPS数据解析等核心代码并包含Keil工程文件、编译中间文件与烧录脚本便于直接打开工程进行调试与二次开发。其中示例程序演示了通过UART接收NMEA数据流、解析经纬度与时间信息并驱动OLED实时刷新的完整流程读者可据此掌握串口通信配置、NMEA语句解析与显示控制等关键环节。目前已有7482人学习下载适合希望快速搭建GPS定位demo、理解嵌入式定位系统开发思路的读者参考借鉴。1. 从一块 GPS 模块开始STM32 定位方案到底能跑出什么效果手里有一块带陶瓷天线的 GPS 模块串口一接、OLED 一亮屏幕上跳出经纬度和 UTC 时间——这是很多人做嵌入式定位的第一个画面。但真到自己动手问题往往不是“能不能亮”而是“多久能定位、数据怎么解析、OLED 怎么刷才不闪、冷启动为什么等了三分钟还没星”。STM32 加 GPS 加 OLED 这套组合核心解决的是把卫星定位数据从串口读进来、解析成经纬度/时间/速度再实时显示到本地屏幕上的完整链路。它适合做车载追踪、户外设备、手持定位终端、物流记录仪这类需要离线定位与本地显示的场景。整套方案不依赖网络上电即工作成本可控是嵌入式定位入门到产品化的经典路径。下面按硬件选型、数据解析、显示刷新、避坑排查、进阶优化五块拆开讲每一步都落到能直接抄的代码和参数上。2. 硬件选型与接线GPS 模块、STM32 串口、OLED 怎么配才不打架2.1 GPS 模块选型NMEA 0183 是通用语言市面上常见的 GPS 模块无论芯片方案怎么变对外输出基本都遵循 NMEA 0183 协议通过 UART 以固定波特率吐出一行行 ASCII 语句。常见默认波特率是 9600也有模块出厂设成 38400 或 115200拿到手第一件事是确认波特率否则读出来全是乱码。模块一般有 VCC、GND、TX、RX 四根线TX 接 STM32 的 RXRX 接 STM32 的 TX。如果只做接收显示模块的 RX 可以不接但建议接上方便后续发配置命令改刷新率或输出语句类型。供电要注意很多 GPS 模块标称 3.3V~5V但峰值电流在搜星阶段能到 40~60mA冷启动瞬间更高。如果直接从 STM32 的 3.3V LDO 取电且板上还有其他外设可能出现电压跌落导致模块反复重启。常见做法是给 GPS 单独一路 LDO或者在 VCC 并一个 100uF 电解加 0.1uF 陶瓷电容。天线方面陶瓷天线适合室内窗边户外建议用有源天线馈线尽量短远离 MCU 晶振和 DC-DC 电感否则灵敏度掉得厉害。2.2 STM32 串口分配别和调试口抢以 STM32F103 为例USART1 默认是 PA9/PA10很多人用来接 CH340 做调试打印。如果 GPS 也挂 USART1调试信息会和 NMEA 数据混在一起解析直接崩。建议 GPS 走 USART2PA2/PA3或 USART3PB10/PB11调试口单独留给 USART1。初始化时波特率、字长、停止位、校验位要和模块一致通常是 8 数据位、1 停止位、无校验。接收方式有两种中断逐字节收或者 DMA 加空闲中断收一整帧。NMEA 语句长度不定从几十字节到上百字节都有用 DMA 空闲中断最省 CPU也不容易丢数据。下面是一段 HAL 库下 USART2 加 DMA 空闲中断的初始化骨架// GPS 串口初始化USART2, 9600, DMA 接收 空闲中断 void GPS_UART_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 9600; // 与模块实际波特率一致 huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; huart2.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart2); // 开启 DMA 接收缓冲区大小建议 256 字节 HAL_UART_Receive_DMA(huart2, gps_rx_buf, GPS_BUF_SIZE); // 使能空闲中断一帧结束后触发 __HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE); }逻辑说明DMA 负责把串口收到的字节自动搬进gps_rx_bufCPU 不用管每个字节。空闲中断在总线静默一个字节时间后触发表示一帧 NMEA 语句收完了。在中断服务函数里计算本次收到的长度置个标志位给主循环处理。参数上GPS_BUF_SIZE取 256 足够容纳最长语句如果模块输出频率高比如 10Hz缓冲区可以加倍防止上一帧没处理完下一帧就覆盖。2.3 OLED 接口I2C 省引脚SPI 刷得快OLED 常见 0.96 寸 128x64驱动芯片 SSD1306 居多。I2C 版本只占两根线SCL/SDA适合引脚紧张的项目SPI 版本刷屏速度快适合需要高频刷新或显示波形的情况。I2C 地址通常是 0x78 或 0x7A取决于模块上的电阻跳线。接线时注意上拉电阻很多模块自带 4.7k 上拉如果没有STM32 这边要补上否则 I2C 通信时好时坏属于典型玄学问题。显示内容建议分四行第一行 UTC 时间第二行纬度第三行经度第四行卫星数和速度。刷新频率不用太高GPS 本身通常 1Hz 更新OLED 跟着 1Hz 刷就够。如果每收到一帧就全屏清一次再写会看到明显闪烁后面章节会讲怎么用局部刷新解决。3. NMEA 语句解析从 $GNRMC 里把经纬度抠出来3.1 先搞清楚模块到底吐了哪些语句上电后把 GPS 模块的 TX 接到 USB 转串口工具用串口助手看原始输出你会看到类似这样的行$GNRMC,083559.00,A,2232.12345,N,11405.67890,E,0.0,0.0,010124,,,A*6C $GNGGA,083559.00,2232.12345,N,11405.67890,E,1,08,1.2,45.6,M,-2.1,M,,*5A $GPGSV,3,1,11,01,05,120,25,02,15,230,30,...其中$GNRMC包含时间、定位状态、经纬度、速度$GNGGA包含定位质量、卫星数、海拔$GPGSV是可见卫星信息。做基础定位显示解析 RMC 和 GGA 就够了。注意开头可能是$GNRMC也可能是$GPRMC取决于模块支持的星座系统解析时判断RMC子串即可不要写死$GNRMC。3.2 解析 RMC状态位不判断漂移数据会骗你RMC 语句的字段用逗号分隔关键字段位置如下字段序号含义示例注意1UTC 时间083559.00时分秒需转北京时间加 8 小时2定位状态A / VA 有效V 无效必须判断3纬度2232.12345度分格式需转十进制度4纬度半球N / SS 为负5经度11405.67890度分格式6经度半球E / WW 为负7地面速度0.0节乘 1.852 转 km/h解析代码可以按逗号切分也可以逐字符扫描。下面是一个轻量解析函数// 解析 RMC 语句提取经纬度、时间、速度 // 返回 1 表示解析成功且定位有效0 表示无效 int GPS_ParseRMC(const char *line, GPS_Data_t *out) { if (strstr(line, RMC) NULL) return 0; char buf[128]; strncpy(buf, line, sizeof(buf) - 1); buf[sizeof(buf) - 1] \0; char *token strtok(buf, ,); int field 0; char status V; double lat 0, lon 0; char ns N, ew E; while (token ! NULL) { switch (field) { case 1: out-utc_time atof(token); break; case 2: status token[0]; break; // A 或 V case 3: lat atof(token); break; case 4: ns token[0]; break; case 5: lon atof(token); break; case 6: ew token[0]; break; case 7: out-speed_knots atof(token); break; } token strtok(NULL, ,); field; } if (status ! A) return 0; // 定位无效直接丢弃 // 度分转十进制度ddmm.mmmmm - dd mm.mmmmm/60 out-latitude (int)(lat / 100) fmod(lat, 100) / 60.0; out-longitude (int)(lon / 100) fmod(lon, 100) / 60.0; if (ns S) out-latitude -out-latitude; if (ew W) out-longitude -out-longitude; return 1; }逻辑说明先用strstr确认是 RMC 语句避免误解析其他帧。strtok按逗号切分字段序号从 0 开始所以时间在 case 1。状态位status必须判断很多新手直接跳过结果模块还没定位就显示经纬度 0.0或者显示上一次的残留值。度分转十进制是固定公式fmod取小数部分。参数上buf大小 128 足够RMC 语句一般不超过 80 字节。3.3 解析 GGA卫星数和定位质量GGA 语句里第 6 字段是定位质量0 无效1 GPS 定位2 差分定位第 7 字段是卫星数第 9 字段是海拔。显示卫星数能让用户判断当前搜星状态比单纯显示经纬度更有参考价值。解析逻辑和 RMC 类似这里不重复贴代码注意字段序号别搞错GGA 的时间在第 1 字段纬度和 RMC 一样在第 2、4 字段。提示不要在主循环里用HAL_UART_Receive阻塞读GPS 数据是持续流阻塞读会拖慢整个系统。用 DMA 加空闲中断主循环只处理完整帧。4. OLED 显示刷新局部更新、缓冲区、字库三个关键点4.1 显存缓冲别每次全屏清SSD1306 内部有 128x64 的 GDDRAM按页组织共 8 页每页 128 字节。如果每次刷新都调用全屏清屏再写I2C 上要传 1024 字节1Hz 刷新下屏幕会明显闪。正确做法是在 MCU 内存里开一个 1024 字节的显存数组所有绘制操作先改数组最后一次性把数组刷到 OLED。这样屏幕内容变化是整体的不会出现清屏瞬间的黑屏。// OLED 显存缓冲128x64 对应 8 页 x 128 列 static uint8_t oled_buf[8][128]; // 把显存整体刷到 SSD1306 void OLED_Refresh(void) { for (uint8_t page 0; page 8; page) { OLED_SetCursor(page, 0); OLED_WriteData(oled_buf[page], 128); // I2C 连续写 128 字节 } }逻辑说明oled_buf是 MCU 侧的影子显存绘制函数只改这个数组。OLED_Refresh按页写入每页 128 字节连续写I2C 速率 400kHz 下刷完 1024 字节约 25ms1Hz 刷新完全够用。参数上如果 I2C 速率只有 100kHz刷屏时间会到 100ms 左右建议把 I2C 时钟配到 400kHz。4.2 局部刷新只更新变化的区域如果不想全屏刷可以记录哪些页的内容变了只刷变化的页。比如时间行在第 0 页经纬度在第 2、4 页卫星数在第 6 页每次只刷这四页I2C 传输量降到 512 字节。实现方式是在绘制函数里对目标页做标记刷新时只遍历标记页。对于 GPS 这种 1Hz 更新的场景局部刷新和全屏刷新的视觉差异不大但 CPU 占用和总线负载更低。4.3 字库与显示格式经纬度别显示成小数后五位OLED 128x64 的物理尺寸有限一行最多显示 16 个 6x8 字符或 8 个 16x16 汉字。经纬度如果直接printf(%.5f)一行放不下。常见做法是纬度显示到小数点后 4 位约 11 米精度经度同样时间显示HH:MM:SS速度显示整数。字库方面如果只用 ASCII取模 6x8 或 8x16 即可如果要显示中文“纬度”“经度”需要 16x16 汉字取模Flash 占用会增加但 F103 的 64KB 以上 Flash 完全放得下。// 格式化显示时间、纬度、经度、卫星数 char line[20]; sprintf(line, UTC %02d:%02d:%02d, (int)(gps.utc_time / 10000), (int)((int)gps.utc_time % 10000) / 100, (int)gps.utc_time % 100); OLED_ShowString(0, 0, line); sprintf(line, LAT %.4f, gps.latitude); OLED_ShowString(0, 2, line); sprintf(line, LON %.4f, gps.longitude); OLED_ShowString(0, 4, line); sprintf(line, SAT %02d, gps.satellites); OLED_ShowString(0, 6, line);逻辑说明sprintf把数值转成字符串OLED_ShowString按页和列写入显存。参数上line长度 20 足够注意sprintf在 Keil 下默认不支持浮点需要在工程设置里勾选 Use MicroLIB 或者改用整数拼接。浮点显示如果出现乱码八成是这个问题。5. 避坑与排查冷启动、乱码、漂移、I2C 挂死这四类问题5.1 冷启动三分钟没定位热启动秒定现象模块上电后长时间没有有效定位OLED 一直显示无效或 0.0。原因GPS 冷启动需要下载星历开阔天空下通常 30 秒到 1 分钟室内或天线环境差时可能几分钟甚至定不上。热启动因为有备份电池供电保存星历几秒就能定位。解决确认模块的 VBAT 备份引脚是否接了纽扣电池或超级电容没有备份电池就是每次冷启动。另外把天线放到窗边或户外陶瓷天线在室内基本残废。判断是否定位看 RMC 的状态位和 GGA 的定位质量不要只看经纬度数值。5.2 串口读出来全是乱码现象串口助手或 MCU 收到的数据是乱码没有$开头。原因波特率不匹配或者 TX/RX 接反或者电平不兼容。解决先用 USB 转串口工具直接接模块逐个波特率试 9600、38400、115200直到出现可读的 NMEA 语句。确认模块波特率后再配 MCU。如果模块是 3.3V 电平MCU 也是 3.3V直连即可如果模块是 5V 电平需要电平转换否则可能损坏 MCU 引脚。5.3 经纬度漂移静止时数值一直跳现象模块静止不动经纬度小数点后几位持续变化。原因这是正常现象民用 GPS 水平精度在 2.5 米到 5 米左右静止时也会有随机误差。如果漂移幅度达到几十米检查天线是否被遮挡、是否有强干扰源。解决软件上可以做滑动平均滤波取最近 5 到 10 个有效定位点做均值显示会更稳。但注意滤波会引入延迟车载场景下转弯时显示会滞后需要权衡。5.4 I2C OLED 偶尔不亮或花屏现象OLED 上电后不显示或者显示内容错乱复位后又正常。原因I2C 上拉电阻不足、总线被拉死、初始化时序不对。解决用示波器看 SCL/SDA 波形如果上升沿很缓说明上拉太弱换成 2.2k 到 4.7k。如果 SDA 一直被拉低可能是 OLED 初始化过程中复位不完整在初始化前加 100ms 延时或者先发一次软复位命令。另外注意 I2C 地址0x78 和 0x7A 写错一个位就是不亮。注意GPS 模块和 OLED 共用电源时GPS 搜星瞬间的电流波动可能让 OLED 复位。如果遇到 OLED 随机闪烁先给 GPS 单独供电试试。6. 进阶技巧用 1PPS 做时间同步与定位数据校验GPS 模块除了串口输出 NMEA通常还有一个 1PPS 引脚每秒输出一个上升沿精度在几十纳秒级。这个信号可以用来做两件事一是校准 MCU 的 RTC二是判断 NMEA 数据的新鲜度。具体做法是把 1PPS 接到 STM32 的外部中断引脚在中断里记录时间戳主循环解析到 RMC 后对比 1PPS 时间戳和 UTC 秒数如果偏差超过 1 秒说明数据帧可能错位或丢失丢弃这一帧。另一个实用技巧是解析 GSV 语句统计可见卫星数和 GGA 里的使用卫星数对比。如果可见卫星很多但使用卫星很少说明信号质量差或者天线方向不对这时候显示的经纬度可信度低。可以在 OLED 上加一个信号质量指示比如用卫星数除以可见卫星数低于 0.5 时提示“信号弱”。// 1PPS 中断记录秒脉冲时间戳 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPS_PPS_Pin) { pps_tick HAL_GetTick(); // 记录毫秒级时间戳 pps_flag 1; // 通知主循环 } } // 主循环中校验RMC 秒数变化时应有 1PPS 伴随 if (pps_flag rmc_updated) { uint32_t delta pps_tick - last_pps_tick; if (delta 1500 || delta 500) { // 1PPS 间隔异常丢弃当前定位数据 gps_valid 0; } pps_flag 0; rmc_updated 0; }逻辑说明1PPS 每秒来一次正常情况下两次中断间隔约 1000ms。如果间隔明显偏离说明模块可能失锁或重启。参数上500和1500是容差范围根据实际模块调整。这个校验能过滤掉大部分异常帧比单纯判断状态位更可靠。从那以后我每次拿到新的 GPS 模块都先单独接串口助手确认波特率和语句输出再上 MCU最后才调 OLED 显示。这个顺序能省掉大量来回排查的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑