STM32读取GPS模块:手写NMEA解析,获取经纬度与速度
2026/9/7 17:59:42 网站建设 项目流程

很多朋友第一次把GPS 模块接到STM32上,上电后看到串口助手吐出一串$GPRMC$GPGGA开头的字符串,人是懵的。这些看起来很乱的数据就是NMEA 0183 协议,也是绝大多数民用定位模块和单片机之间沟通的标准语言。这次要聊的项目很明确:用STM32读取VK2828U7G5模块的GPS 定位数据,自己写代码把NMEA 语句解析成可用的经纬度、速度、时间和日期,而不是靠卖家给的现成上位机。

这个项目很适合三类人:正在做电子设计、课程设计或毕业设计的学生,想给自己的物联网设备加定位功能但不想用开发板现成固件的嵌入式工程师,以及想彻底搞懂 GPS 数据底层格式的爱好者。读完这篇你不仅能把模块跑起来,还会明白定位数据为什么这么算、串口数据为什么经常丢、定位误差到底从哪来。

1. VK2828U7G5 模块到底是个什么东西

1.1 扒开模块看内部:主控芯片与小板的真实关系

VK2828U7G5 是一块非常典型的国产 GPS/BDS 双模定位模块,很多淘宝卖家也叫它“AT6558 模块”,因为板子核心就是中科微电子的 AT6558 主控。这颗芯片支持 GPS、北斗以及 GLONASS 等多个星座的信号接收,默认通过串口输出标准 NMEA 语句,波特率出厂一般是9600bps,8 位数据位、无校验、1 位停止位,也就是常说的 8N1。模块上通常还带一个 LNA 低噪声放大器和 SAW 滤波器,所以裸板加根天线就能工作。

很多同学会误以为 VK2828U7G5 是一颗芯片型号,其实它更像是“模组/开发板”的商品命名,丝印上你大概率看到的是 AT6558 或者板厂自己的编号。这恰恰说明一个问题:这类模块本质上是同一个主控的各种封装,采购时只要认准 AT6558 方案,基本都能用同一套代码驱动,兼容性非常强。对初学者来说,这意味着你不需要把精力花在纠结“这个模块的寄存器怎么配”上,因为 AT6558 的定位核心已经帮我们把信号处理做完了,你只需要关心串口拿到的 NMEA 数据。

1.2 选型对比:为什么大家都爱用这种小模块

刚接触定位功能的同学常问:是不是买个 GPS 模块带屏的更好?或者直接用手机 GPS?这里我先做个简单的方案对比,你就明白为什么 VK2828U7G5 这类模块在嵌入式项目里这么受欢迎。

方案类型典型代表优点缺点适合场景
双模小模块VK2828U7G5 / AT6558便宜、功耗低、NMEA 通用需要自己配天线、自己写解析STM32 等项目集成
高端串口模块u-blox NEO-M8N / MAX-M8性能稳定、定位精度高价格贵、配置复杂产品级定位设备
手机 GPS手机基带芯片精度高、有算法加持无法直接用到自研硬件App 定位、出行导航
串口一体机模块+天线一体接线简单体积大、灵活性差快速原型验证

从成本和开发效率角度看,VK2828U7G5 最有吸引力的地方在于:它给你的是“半成品”,但恰好把最麻烦的射频部分做好了。STM32 只需要通过串口收 NMEA 数据,剩下的解析、滤波、显示全部由你控制,这对学习 GPS 数据解析和嵌入式串口编程非常友好。我之前做课程设计时也试过用带显示的一体化模块,但那种模块输出格式封闭,想滤波、想融合其他传感器数据几乎不可能,换到 VK2828U7G5 后自由度立刻上来了。

还有一个隐藏优点:这类模块有 1PPS(秒脉冲)引脚,将来做授时、时间同步、多设备协同定位时可以直接用。对一名工程师来讲,模块能被人为“压榨”出多少功能,往往比模块本身贵几块钱更重要。

1.3 模块引脚与硬件参数速览

VK2828U7G5 常见版本会引出 6 到 14 个引脚,其中必须关注的有这几个:

  • VCC:电源正极,常见支持 3.3V 或 5V,具体看模块丝印,一般建议 3.3V 供电保持电平一致。
  • GND:电源地。
  • TXD:模块串口发送脚,接 STM32 的 RX。
  • RXD:模块串口接收脚,接 STM32 的 TX,主要用于配置模块参数。
  • PPS / 1PPS:秒脉冲输出,每次定位成功后每秒输出一个高电平脉冲。
  • RF_IN / ANT:天线接口,通常为 IPEX 座,需要外接有源或无源 GPS 天线。

模块默认波特率虽然是 9600,但要注意,某些批次可能被卖家预先改成了 115200 或者其他值。遇到串口输出乱码时,不要急着怀疑代码,先用 USB-TTL 转接器直接接模块调到不同波特率看看,这是最基础的排查动作。

2. 硬件接线与 STM32 工程准备

2.1 接线就三根线,但电平匹配不能省

VK2828U7G5 模块工作在3.3V TTL 电平,所以和 STM32 连接时非常方便,不用额外加电平转换。三根线分别是:

  • 模块VCC→ STM32 的 3.3V
  • 模块GND→ STM32 的 GND
  • 模块TXD→ STM32 的PA10(USART1_RX)
  • 模块RXD→ STM32 的PA9(USART1_TX)(如果只需要读数据,这根可以不接)

有源天线就比较讲究了。如果你买的是 IPEX 接口的有源天线,天线供电一般是经过模块内部处理的,模块的VCC会同时给天线供电;但个别裸板会有V_ANT引脚专门给天线供电,接法要仔细看数据手册。我见过不少同学把有源天线的电源直接接到 STM32 的 3.3V 引脚上,导致模块供电不稳、定位信号时有时无。

另外,模块和 STM32 如果分别独立供电,必须保证共地,也就是两边的 GND 连在一起。否则串口信号的电平参考点不同,接收端会出现偶发乱码甚至完全收不到数据。这个问题在用 USB-TTL 模块调试时特别常见,因为 USB-TTL 的 GND 和 STM32 的 GND 来自不同电源路径。

2.2 用 STM32CubeMX 五分钟建好串口接收工程

这里我用 HAL 库和 CubeMX 来讲,因为现在新项目用 HAL 库已经是主流,代码可读性和移植性都比以前的标准库好不少。打开 STM32CubeMX 后,选一颗 STM32F103C8T6 最小系统板,需要配置的地方只有三个:

  1. RCCHSE (Crystal/Ceramic Resonator),使用外部晶振。
  2. SYS里的DebugSerial Wire,这样能保留 SWD 调试口。
  3. USART1选择Asynchronous模式,波特率设 9600,字长 8 位,无校验,1 位停止位。

然后在 NVIC 设置页里,把USART1 global interrupt使能打勾。这一步很容易被忽略,如果不开中断,后面那套基于中断的接收代码跑不起来。

生成工程后,在main.c的用户代码区添加接收字节的变量和回调函数:

uint8_t gps_byte; // 单个接收字节 uint8_t gps_frame_buf[256]; // 一帧 NMEA 数据缓冲 volatile uint16_t gps_frame_len = 0; // 当前帧长度 volatile uint8_t gps_frame_ready = 0; // 帧接收完成标志

main()里开启串口接收中断:

HAL_UART_Receive_IT(&huart1, &gps_byte, 1);

然后在stm32f1xx_it.cUSART1_IRQHandler里调用HAL_UART_IRQHandler(&huart1),这步 CubeMX 已经生成好了。接着重写HAL_UART_RxCpltCallback

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { if (gps_frame_len < 255) { gps_frame_buf[gps_frame_len++] = gps_byte; if (gps_byte == '\n') { gps_frame_buf[gps_frame_len] = '\0'; gps_frame_ready = 1; gps_frame_len = 0; } } else { gps_frame_len = 0; } HAL_UART_Receive_IT(&huart1, &gps_byte, 1); } }

主循环里只要判断gps_frame_ready是否为 1,然后向解析函数传入gps_frame_buf即可。这套代码看起来简单,但有几个细节值得展开讲。

第一个细节:为什么每次接收完一个字节后要立刻再次调用HAL_UART_Receive_IT?因为 HAL 库的串口中断接收是“一次性”的,接收完指定字节数后如果不再调用,后续数据就进不了中断。第二个细节:为什么用\n判断一帧结束?NMEA 语句的标准结束符是\r\n,也就是回车换行,用\n作为帧结束标志,是因为回车符\r可能在解析时不方便处理,我选择在解析前把字符串中的\r过滤掉。第三个细节:缓冲区溢出时我把长度清零,这是为了防止数据错位后一直解析乱码,重置缓冲是最稳妥的做法。

2.3 上电前检查清单

实际项目中,我见过很多“板子调不通”的案例,最后发现都是低级问题。这里把检查清单列出来,上电前照做,能省一晚上的折腾时间:

  • [ ] 模块供电电压是 3.3V 还是 5V,与 STM32 是否匹配
  • [ ] TXD 和 RXD 是否交叉连接,两个 TX 直接对 TX 是收不到数据的
  • [ ] GND 是否共地
  • [ ] 天线是否接好,IPEX 座有没有卡到位
  • [ ] CubeMX 里有没有开启串口中断
  • [ ] 主循环里HAL_UART_Receive_IT是否已经调用过一次

3. NMEA 0183 协议解析:从字节流到经纬度

3.1 先看懂一帧 NMEA 数据到底在说什么

NMEA 0183 是一种纯文本协议,每一帧语句以$开头,以\r\n结尾,中间用逗号分隔字段。对定位项目来说,最常看的两条语句是$GPRMC$GPGGA$GPRMC是最小推荐定位信息,包含时间、状态、经纬度、速度、航向和日期;$GPGGA是固定定位数据,包含定位质量、卫星数量、海拔等。

拿一条真实的 RMC 语句来拆:

$GPRMC,121948.000,A,3017.5725,N,12007.5319,E,0.08,185.34,140524,,,A*62

字段按下标对应关系是:

字段含义示例
0帧头$GPRMC$GPRMC
1UTC 时间 hhmmss.sss121948.000
2定位状态 A=有效 V=无效A
3纬度 ddmm.mmmm3017.5725
4纬度半球 N/SN
5经度 dddmm.mmmm12007.5319
6经度半球 E/WE
7地面速度(节)0.08
8地面航向(度)185.34
9UTC 日期 ddmmyy140524
10磁偏角
11磁偏角方向
12模式指示A
13校验和,*后两位十六进制62

$GPGGA语句的字段布局类似,但包含定位质量指示、卫星数和海拔:

$GPGGA,121948.000,3017.5725,N,12007.5319,E,1,07,1.2,35.6,M,,M,,*55

这里第 6 个字段1表示单点定位,0表示无效定位,2表示差分定位;第 7 个字段是跟踪到的卫星数;第 8 个字段是水平精度因子 HDOP;第 9 个字段是海拔高度。

3.2 校验和计算:不是所有数据都能信

NMEA 语句的校验和位于*号之后的两位十六进制数,计算方法是:把$*之间所有字符做按位异或,结果转成十六进制后与*后两位比较。比如$GPRMC后面那些字符逐个异或,得到0x62,与语句末尾*62一致,说明这一帧没有被干扰。

int nmea_check(const char *nmea) { const char *p = nmea + 1; // 跳过 $ unsigned char sum = 0; while (*p != '*' && *p != '\0') { sum ^= (unsigned char)*p; p++; } if (*p == '*') { int check = 0; sscanf(p + 1, "%2x", &check); return (check == sum) ? 0 : -1; } return -1; }

这一步不能省。我在实际测试中发现,当模块天线接触不良、周围有大功率干扰源时,偶尔会有个别字节翻转导致经纬度数据完全错乱。如果解析前不校验,错值可能被当真值用;校验后,这一帧直接丢弃等待下一帧,系统稳定性会有本质提升。

3.3 字段提取:别用 strtok,自己写才安全

解析 NMEA 最忌讳直接用strtok,因为strtok会原地修改被分割字符串,把逗号替换成\0,而你的原始缓冲区可能还要用来做调试输出,破坏掉之后想打印原帧就麻烦了。更稳妥的方式是写一个按索引提取字段的函数,用strchr找逗号位置,然后拷贝子串。

int nmea_field(const char *buf, int field, char *out, int max_len) { const char *start = buf; int i; if (buf[0] != '$') return -1; for (i = 0; i < field; i++) { start = strchr(start, ','); if (!start) return -1; start++; } const char *end = strchr(start, ','); if (!end) end = strchr(start, '*'); if (!end) return -1; int len = (int)(end - start); if (len >= max_len) len = max_len - 1; memcpy(out, start, len); out[len] = '\0'; return 0; }

设计上有个小技巧:字段结束位置优先找逗号,找不到逗号就找*,这样最后一个字段也不会落空。字段提取函数通用性很好,GGA、RMC、GSA 语句都能用同一个函数。

3.4 解析 RMC 并转换成十进制度数

拿到 RMC 语句后,先判断状态字段是不是A,如果是V说明定位无效,可以直接跳过;然后提取经纬度字符串和方向字母,转成十进制度数。之所以必须转换,是因为 NMEA 里的纬度格式是“度分”格式,比如3017.5725表示 30 度 17.5725 分,直接用这个数当度数算,误差会大得离谱。

转换公式很简单:度分值中前两位是度数,后面的小数是分,十进制 = 度数 + 分数 / 60。纬度字符串是固定两位度,经度字符串是固定三位度,写转换函数时要注意这个区别。

double convert_to_decimal(const char *ddmm, char hemi) { double value = atof(ddmm); int deg = (int)(value / 100.0); double minute = value - deg * 100.0; double decimal = deg + minute / 60.0; if (hemi == 'S' || hemi == 'W') decimal = -decimal; return decimal; }

综合起来,一条完整解析流程大概是:

int parse_rmc(const char *line, double *lat, double *lon, float *speed_knot, uint8_t *valid) { char field[32]; if (strncmp(line, "$GPRMC", 6) != 0) return -1; if (nmea_check(line) != 0) return -1; if (nmea_field(line, 2, field, sizeof(field)) != 0) return -1; if (field[0] != 'A') { *valid = 0; return -1; } *valid = 1; if (nmea_field(line, 3, field, sizeof(field)) == 0) { char ns = line[0]; // 实际需要再取第4字段的字符 // 建议用 nmea_field(line, 4, ns_buf, 2) 取方向 } return 0; }

这里有个容易踩的坑:第 3 个字段是纬度数字,第 4 个字段才是 N/S,第 5 个字段是经度数字,第 6 个字段是 E/W。很多初学者只提取了纬度和经度,忘了带 N/S/E/W 符号,结果南半球或西经数据完全不对。由于我们国内常用场景是北纬东经,这种现象不容易暴露,但做产品出海或做全球定位时就会出事。

3.5 解析 GGA:定位质量与卫星数比经纬度更关键

有时 RMC 状态已经是A,但数据依然不可靠,比如在隧道口附近、高架桥下可能收到定位但漂移很大。这时看 GGA 语句比看 RMC 更有参考价值。GGA 第 6 字段定位质量和第 7 字段卫星数能告诉你当前定位是单点、差分还是无效,卫星数低于 4 颗时位置基本不可信。

建议在解析 GGA 时,把定位质量、卫星数、HDOP 和海拔一起存下来,后续做电子围栏、轨迹回放或姿态融合时都会用到这些辅助信息。很多项目只看经纬度,但经纬度本身没有置信度,一旦信号变差,定位点可能突然跳几百米,非常影响体验。

4. 从定位原理看精度:为什么 GPS 误差跑不掉

4.1 三边测量算法与“为什么必须至少 4 颗卫星”

GPS 定位的数学原理很多教材叫“三边测量”,其实就是解一个球面交点问题。每颗卫星在自己的轨道上广播位置和精确时间,接收机测量信号从卫星传到自己的时间差,乘以光速就得到一个“伪距”。以每颗卫星为球心、伪距为半径画球面,接收机一定在这个球面上。三个球面理论上可以交出一个点,但实际中接收机时钟和卫星时钟不同步,存在一个未知的钟差,所以位置变量其实是 4 个:经度、纬度、高度、钟差。

这就是为什么民用 GPS 至少要看到 4 颗卫星,而不是 3 颗。4 颗卫星对应 4 个方程,才能解出 4 个未知数。实际接收机内部会用牛顿迭代或最小二乘法求解非线性方程组:

[ \rho_i = \sqrt{(x - x_i)^2 + (y - y_i)^2 + (z - z_i)^2} + c \cdot \Delta t ]

其中 (\rho_i) 是第 i 颗星的伪距,((x_i, y_i, z_i)) 是卫星坐标,((x, y, z)) 是接收机坐标,(\Delta t) 是钟差,(c) 是光速。这个公式看起来很数学,但理解它有助于你想清楚一件事:如果某颗卫星的伪距因为多径或电离层产生偏差,解出来的位置一定会跟着偏。

4.2 影响定位精度的现实因素

很多人在户外空旷地测试,模块定位精度能到 2 到 3 米,但在城市峡谷、高架桥下、室内窗口旁却动不动偏几十米,这不是模块坏了,而是卫星信号环境变了。影响精度的主要因素有这么几个:

  • 卫星几何分布:当可见卫星集中在天空某一小片区域时,DOP(精度因子)会变大,定位误差会被放大。HDOP 越低,水平精度越好。
  • 多径效应:高楼、山体、大型金属结构反射卫星信号,接收机收到直达信号和反射信号的叠加,伪距出现正偏差。
  • 大气层延迟:电离层和对流层会让信号传播速度发生变化,这是系统性误差。
  • 天线性能:贴片陶瓷天线增益低,金属环境里信号被遮挡,导致能捕获的卫星数量减少。

所以,拿到一个定位模块不要一上来就骂精度差。先到户外开阔场地测试,如果户外能稳定在 3 米以内,说明模块正常;如果户外也漂十几米,那才需要检查天线、电源和模块配置。

4.3 提升定位体验的三板斧

第一,优先用有源天线,并且天线放在尽量开阔的位置。无源陶瓷天线在室内基本只能看到一两颗星,有源天线虽然贵一点,但信号增益明显更高,定位速度和稳定性提升非常可观。第二,解析时做合理性判断,比如速度超过 200 节、相邻两点距离突然跳变几百米,都应该做滤波丢弃。第三,如果项目对精度有更高要求,可以在 STM32 端做一步轻量级 Kalman 滤波,把 GPS 数据和加速度计、陀螺仪融合,可以把跳点压到很低。

这里还想到一个调试技巧:如果你在室内开发,等卫星信号等到怀疑人生,可以找一个能模拟 GNSS 信号的仪器或者回放之前保存的 NMEA 日志喂给解析代码,这样代码逻辑调试完全不依赖真实卫星环境,效率会高很多。专业测试设备可以模拟 GPS 卫星信号,但普通学生党用保存的 NMEA 日志回放也足够了,把串口收到的原始语句存成文本,改一下数据源就能反复测试。

4.4 别忽略 PPS 引脚:授时和同步是隐藏玩法

VK2828U7G5 的 PPS 引脚在定位有效后会每秒输出一个上升沿,这个脉冲与 UTC 时间的同步误差通常在几十纳秒到几微秒级别。虽然和高端授时模块没法比,但用在多设备时间同步、摄像头帧同步、数据采集打时间戳这些场景完全够用。用 STM32 的外部中断捕捉 PPS 上升沿,在中断里记录当前系统时钟,可以校准本地 RTC 的漂移。很多共享设备、巡检机器人项目就是用这个思路做多机时间对齐的。

在嵌入式上加一个定位模块,如果只是打印经纬度,那属于“能跑”;真正体现水平的是把数据源、时间源、状态源融进你自己的业务逻辑里。PPS 引脚就是那个容易被忽略但很有价值的“时间源”。

5. 实测验收与常见问题排查

5.1 怎么判断模块和代码真的正常

第一件事,先用 USB-TTL 接模块,在电脑串口助手里直接看原始 NMEA 数据。如果电脑上能看到稳定的$GPRMC$GPGGA,说明模块供电和天线没问题,问题在 STM32 侧;如果在电脑上就看不到数据或全是乱码,说明接线、波特率或天线有问题。这种“分段隔离排查法”能帮你快速把问题缩小到一个方向,而不是对着代码瞎猜。

第二件事,确保 STM32 代码能持续打印解析结果。我习惯把原始 RMC 语句、解析出的经纬度、卫星数、状态位一起通过调试串口发到电脑,用示波器看 PPS 脉冲是否出现。解析结果和原始语句同时打印的好处是:如果经纬度算错了,你能对照原始语句检查是字段提取问题还是转换公式问题。

5.2 我实际踩过的坑:高频问题速查表

下面这张表是全项目里最有价值的部分,全部来自我实际调试中遇到的问题,可以说每一条都是拿时间换来的。

现象可能原因解决办法
串口完全无数据TX/RX 没交叉,或波特率不对检查接线,模块 TX 必须接 STM32 RX;试 9600/115200
串口输出乱码波特率不匹配或电平不共地统一波特率,确保 GND 共地
有数据但状态一直是 V天线没接好或处于室内检查 IPEX 座,拿到窗边/户外测试
有 RMC 但经纬度明显不对度分未转成十进制,或 N/S/E/W 判断出错按公式 dd + mm/60 转换,注意方向字符
数据时有时无电源供电不足或有干扰用独立 3.3V LDO 供电,天线远离电机/电源
校验和偶发失败电磁干扰或串口波特率相对误差偏大检查地线,缩短杜邦线长度,必要时降低波特率
冷启动非常慢天线增益低或有源天线供电不对换有源天线,确认模块的 V_ANT 供电
定位点每小时漂移一次多径或可见卫星数低于 4 颗调整天线位置,在代码中增加有效状态判断

其中一个让我印象最深的坑是“天线座没卡紧”。IPEX 天线座如果没按到底,看起来接上了,实际信号极其微弱,导致室外也只能收到一两颗星。后来我养成一个习惯:每次上电前旋转一下 IPEX 头,听到清脆的“咔哒”声才算装好。这种机械上的小问题,代码再优化也没用。

还有一个坑是 STM32 的HAL_UART_Receive_IT调用时机。有些同学在回调里没有再次调用HAL_UART_Receive_IT,导致串口只收到第一帧数据就再也不更新了。代码看起来逻辑正确,但就是收不到后续数据,这种问题排查起来比较费时间,建议一开始就把“每个字节接收完成后必须重开接收中断”这条规则刻进脑子里。

5.3 从毕业设计到实际产品:这个项目还能扩展什么

如果你正在做毕业设计,VK2828U7G5 + STM32 这套组合可以往很多方向扩展。比如加上 EC20 或 ESP8266 模块,把 NMEA 解析出的经纬度通过 MQTT 上报到云平台,做共享设备定位或车辆轨迹回放;也可以用 STM32 的 Flash 保存定位数据,做离线黑匣子;还能把这个定位模块和已有的传感器项目融合,做一个带 GPS 打点的环境监测装置。核心价值在于你已经掌握了“串口协议解析”这个通用能力,以后面对任何串口传感器(激光雷达、惯导、气象站)都能快速上手。

很多同学在做项目时会纠结要不要用原子哥或者野火的现成例程,这些例程确实能帮你快速跑通,但如果你能自己完成 NMEA 解析,你对“数据从哪里来、误差从哪里来、代码为什么会卡住”的理解会完全不同。这也是我为什么坚持把校验、字段提取、度分转换这些细节全部讲透,而不只是给你一个可以直接抄的函数。

最后分享一个个人习惯:我在代码里会把解析出的 UTC 时间和日期转换成北京时间再显示,因为 GPS 默认给的是 UTC 时间,直接显示会让用户觉得时间慢了 8 小时。转换时要注意跨天问题,UTC 时间加 8 小时后如果超过 24 点,日期要加一天,这个细节别看它小,做产品时用户反馈会很直接。在你自己的项目里,建议也把“时间显示成什么时区、经纬度用什么坐标系、速度显示成节还是公里每小时”这些产品细节提前想清楚,别等技术联调时再手忙脚乱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询