
自己做单片机项目这些年凡是涉及到定位、轨迹、时间同步的需求我基本上都会抓一块 VK2828U7G5 这种串口 GPS 模块回来接上 STM32 先跑通再说。这个模块的好处是便宜、接线简单、输出标准 NMEA 0183 协议买回来接上串口就能看到数据但真正把数据变成能用的经纬度坐标还得自己在固件里做一层解析而那些定位不准、冷启动半天、坐标偏到马路对面、老固件时间跳回 2000 年之类的坑不动手实测几次根本发现不了。这篇文章就把我基于 STM32 VK2828U7G5 做 GPS 定位和 NMEA 解析的完整过程写出来从硬件怎么接到协议怎么拆再到代码实现和排查经验一次讲透。1. 项目定位与整体链路设计1.1 为什么选 VK2828U7G5 而不是其他方案很多朋友在给板子加定位功能的时候第一反应是买一块带天线的“GPS 模块”但实际上市面上的方案差别非常大。带 GPRS/4G 通信的 SIM868、SIM7600 这种把定位和联网做在一起功耗大、调试也难手机蓝牙 GPS 模块又依赖外部设备转发不适合做独立的嵌入式节点。VK2828U7G5 是典型的纯 GNSS 接收机模块输出串口数据内部使用的是 u-blox 方案处理 GPS、北斗、GLONASS 信号都没问题数据更新率默认 1Hz满足大部分轨迹记录和时间同步场景完全够用。选它还有个很现实的原因模块本身不带显示屏、不带协议栈就是电源、串口、PPS 三组引脚对 STM32 开发者来说几乎没有学习成本。而且 VK2828U7G5 核心输出的就是标准 NMEA 语句今天用 STM32F103 跑通了把串口接在树莓派、ESP32、K210 上同样能解析代码逻辑几乎可以复用这点对做项目扩展非常重要。1.2 一条完整 GPS 数据链路是怎么工作的我们要做的系统本质上就是一条单向数据链路GPS 卫星信号被模块内部的射频前端接收经过去噪、捕获、跟踪、定位解算之后把经纬度、速度、时间、卫星数等结果封装成 NMEA 文本从 UART 引脚不断往外吐STM32 端只需要做好两件事——稳定接收数据流然后把文本解析成结构体。链路中不需要握手协议模块上电自动开始输出也不需要单片机给模块发指令很省心。在实际固件里我习惯把接收和处理拆成两个层次串口中断负责把字节收进缓冲区主循环里的解析器负责从缓冲区中切出完整帧并提取字段。这样做的好处是接收速率和解析耗时互不阻塞GPS 模块哪怕用比较高的 38400 波特率连续输出也不会丢帧。数据解析之后再交给应用层去处理比如显示在 OLED 上、写入 SD 卡、通过 LoRa 上报到上位机平台。1.3 项目环境与工具准备我这次用的主控是 STM32F103C8T6开发环境是 Keil MDK STM32CubeMXGPS 模块是 VK2828U7G5板载陶瓷天线同时准备了外置有源天线做对比测试。CubeMX 版本用的比较新固件包选 STM32Cube FW_F1 V1.8.x 都行。如果你用的是老版本 Keil 或者标准外设库代码逻辑一样只是寄存器操作方式有区别不影响本文的解析思路。准备材料清单如下STM32F103C8T6 最小系统板一块VK2828U7G5 GPS 模块一个USB-TTL 调试工具用于看串口输出杜邦线若干、有源天线可选电脑端串口助手、Python 环境用于坐标转换验证2. 硬件连接与天线选择技巧2.1 VK2828U7G5 引脚说明和 STM32 接线对照VK2828U7G5 模块引出的引脚一般包括 VCC、GND、TXD、RXD、PPS有的版本还会引出 V_BCKP 用于接备用电池。VCC 标称支持 3.3V 至 5V我测试时直接给 3.3V 供电这样模块的 UART 电平天然和 STM32 匹配不用额外做电平转换。电源来自 STM32 板载的 3.3V LDO 就行模块正常工作的电流大约在 40mA 到 80mA 之间启动搜星瞬间会高一点电流余量足够。接线对应关系如下表VK2828U7G5 引脚接 STM32 引脚说明VCC3.3V模块供电推荐 3.3VGNDGND共地TXDPA10 (USART1_RX)模块发送数据给单片机RXDPA9 (USART1_TX)单片机给模块发指令平时不用PPSPA0 (可不用)秒脉冲输出做时间同步用我之前犯过一个错误直接把模块的 TXD 接到了 STM32 的 3.3V 电压域忘了确认模块是高电平还是开漏输出结果串口偶尔收到乱码。VK2828U7G5 的 TXD 是 3.3V CMOS 电平3.3V 供电时和 STM32 直接接没问题。如果你给模块供了 5V那最好确认清楚电平范围实在不确定就加一路电平转换别拿板子去赌。2.2 无源陶瓷天线和有源天线到底怎么选VK2828U7G5 板载的是一颗无源陶瓷天线这种天线体积小、成本低在没有遮挡的户外场景下也能正常定位。但我实测下来的感受是在阳台、窗边、车内这类半遮挡环境下靠无源天线要等比较久才能定位冷启动经常超过 40 秒甚至更久一旦走到室内或者把天线贴在金属面上基本就是长时间搜不到星。如果你的设备是固定安装在户外或者车顶强烈建议换上外置有源天线也就是常说的“有源 GPS 天线”或者“GPS 有源吸盘天线”。有源天线内部自带 LNA 低噪声放大器天线接收到的微弱卫星信号先被放大再送入模块能显著改善弱信号环境下的表现。需要注意的是有源天线需要供电购买时一定要确认模块是否提供天线馈电引脚VK2828U7G5 一般有 ANT 引脚或者通过 VCC 直接给外部天线供电接法上要把天线电源引脚的电压选对否则天线不工作或者烧坏 LNA。我的建议很简单固定场景优先选外置有源天线哪怕线长一点、成本高十几块钱定位稳定性的提升非常明显。模块自带的陶瓷天线只适合在实验桌和露天开阔地测试。2.3 供电纹波、备用电池和晶振电容计算GPS 模块对电源纹波比较敏感如果电源噪声太大射频前端的灵敏度会下降直接表现就是搜星变慢、定位精度变差。我给 GPS 模块供电时会在模块 VCC 引脚附近加一颗 100uF 电解电容和一颗 100nF 陶瓷电容尽量靠近模块放置这是很便宜但很有效的做法。另外模块和 STM32 之间共地一定要可靠地线接触不良会导致串口数据错乱。V_BCKP 引脚是用来给模块内部的 RTC 和备份内存供电的。接一颗 CR1220 纽扣电池或者一个 1uF 电容能让模块在掉电后保留星历下次上电热启动速度会快很多。如果没有备用电池每次上电都是冷启动实测在开阔地也需要二三十秒才能定位所以项目对定位时间敏感的话这块别省。关于 STM32 外部晶振的电容计算顺便提一下如果主控用 8MHz 无源晶振规格书上通常会给出负载电容 CL比如 18pF。常见估算公式是 C1 C2 ≈ 2 × (CL - Cs)Cs 是 PCB 引脚寄生电容一般取 3pF 到 5pF算下来大约 26pF 到 30pF实际选型常用 22pF 到 30pF。配错电容会导致晶振不起振或者频率偏差而串口波特率依赖系统时钟时钟偏差会造成 GPS 数据解析出乱码这个我在实践中真遇到过。3. NMEA 0183 报文协议拆解3.1 NMEA 帧的基本格式NMEA 0183 是 GPS 接收机最常用的文本输出协议。每一帧都以美元符号 $ 开头然后是五个字符的语句标识符后面是逗号分隔的数据字段最后是星号 * 加两位十六进制校验和以回车换行结束。举个例子$GNRMC,150000.000,A,2233.12345,N,11356.78901,E,0.00,0.00,060424,,,A*5F\r\n其中 $GNRMC 表示这是一条推荐最小定位信息帧G 开头表示 GNSS 系统N 表示北斗和 GPS 等多系统组合如果是老式 GPS-only 模块则输出 $GPRMC。实际的字段数量每个语句不同但格式框架始终一致所以解析器的通用做法就是接收完整一行检查帧头按逗号切分字段再对关键字段做转换。3.2 GNRMC 帧字段详解GNRMC 是我在日常项目中最常用的一帧因为它包含了时间、定位状态、经纬度、速度、航向和日期信息非常全。把上面的示例帧按字段序号拆开字段位置示例含义1150000.000UTC 时间时:分:秒.毫秒2A定位状态A 表示有效定位V 表示无效32233.12345纬度格式为 ddmm.mmmmm4N北纬/南纬标识511356.78901经度格式为 dddmm.mmmmm6E东经/西经标识70.00对地速度单位节Knots80.00航迹角单位度9060424UTC 日期日/月/年6日4月24年10空磁偏角一般不使用11空磁偏角方向12A定位模式指示符A 为自主定位D 为差分定位N 为无效解析时最需要关注的是字段 2 的状态位状态为 V 时后面的经纬度数据可能是上一次定位的残留或者无效值直接拿来做轨迹记录会产生不存在的跳点。所以我在解析器里面会优先检查这个字符。3.3 GNGGA 帧字段详解GNGGA 提供的是定位质量和高程信息也很有用尤其是需要海拔数据或者要判断当前定位精度时。示例帧$GNGGA,150000.000,2233.12345,N,11356.78901,E,1,07,1.2,45.6,M,-3.2,M,,*4E字段位置示例含义1150000.000UTC 时间2,32233.12345,N纬度及方向4,511356.78901,E经度及方向61定位质量指示0无效1GPS 定位2差分定位4RTK 固定解707当前参与定位的卫星数量81.2HDOP 水平精度因子越小越好9,1045.6,M海拔高度和单位11,12-3.2,M大地水准面高度差和单位13,14空差分数据龄期和差分站 IDGGA 里的卫星数量和 HDOP 对判断定位质量非常重要。卫星数量超过 4 颗通常才能实现三维定位HDOP 在 1 到 2 之间属于比较好的状态大于 5 说明卫星几何分布很差定位误差会明显增大。3.4 校验和计算与坏帧过滤NMEA 帧的校验和非常简单把 $ 和 * 之间所有字符做异或结果就是星号后面的两位十六进制数。比如示例帧里 $ 和 * 之间的内容是 GNGGA,150000.000,... , 将这些 ASCII 码逐个异或最终得到 0x4E也就是 *4E。接收端可以用下面这段代码校验uint8_t calc_checksum(const char *buf, uint16_t len) { uint8_t cs 0; for (uint16_t i 0; i len; i) { cs ^ (uint8_t)buf[i]; } return cs; }我通常在串口中断中先把一整行收进缓冲区主循环里解析之前先做一次校验和验证不通过直接丢弃。因为 GPS 模块输出的语句很多偶尔混入半个字节乱码并不会导致系统崩溃但使用校验和能减少很多奇怪问题尤其是当你的串口线比较长或者环境有干扰时这个步骤一定不能省。4. STM32 串口接收与 NMEA 解析实现4.1 用 CubeMX 配置串口和一些基础外设我新建工程时用 STM32CubeMX 选择 STM32F103C8Tx 芯片将 PA9、PA10 配置为 USART1 的 TX 和 RX波特率设 9600数据位 8停止位 1无校验。GPS 模块默认输出波特率一般是 9600如果找不到数据第一件事就是确认波特率是不是被改成 38400 或者 115200 了。开启 USART1 的全局中断后在 NVIC 设置里把优先级调低一点避免影响系统其他实时任务。如果项目后期还要用 I2C 读取传感器或者驱动电机串口中断优先级不建议设成最高。CubeMX 生成代码后主函数里的串口初始化会默认包含在 MX_USART1_UART_Init 中不要删掉。4.2 串口接收缓冲区的设计GPS 输出的 NMEA 语句是流式的每行几十个字节到一百多字节不定。最简单可靠的接收方式是逐字节中断接收每次进入中断接收一个字节判断是否为新帧起始符再判断是否收到换行符凑成完整一行后置标志位。对 9600 波特率来说每秒钟约 960 字节逐字节中断完全不会丢数据。我测试过 VK2828U7G5 每秒钟大概输出 5 到 10 条语句中断压力不大。下面是我常用的接收代码#define GPS_FRAME_MAX 256 uint8_t gps_rx_byte; uint8_t gps_frame[GPS_FRAME_MAX]; volatile uint16_t gps_frame_len 0; volatile uint8_t gps_frame_ok 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (gps_rx_byte $) { gps_frame_len 0; // 新帧开始 } if (gps_frame_len GPS_FRAME_MAX - 1) { gps_frame[gps_frame_len] gps_rx_byte; if (gps_rx_byte \n) { gps_frame[gps_frame_len] \0; gps_frame_ok 1; } } HAL_UART_Receive_IT(huart1, gps_rx_byte, 1); } } int main(void) { // ... 初始化省略 HAL_UART_Receive_IT(huart1, gps_rx_byte, 1); while (1) { if (gps_frame_ok) { gps_frame_ok 0; process_nmea_line((char *)gps_frame); } } }这段代码有个隐藏问题如果中途丢了$或者\n整个帧边界可能错位。我在实际项目里还会加一个超时机制比如 200ms 内没收到换行就强制把缓冲区清空避免两个帧粘在一起干扰解析。如果你希望进一步降低中断负载可以用 USART 空闲中断加 DMA 的方式接收一整串数据HAL_UARTEx_ReceiveToIdle_DMA在 CubeMX 生成的 HAL 库里可以直接调用。不过对于拿 GPS 练手的朋友逐字节中断已经足够先把逻辑跑通优化的事情后面再说。4.3 NMEA 解析器实现要点NMEA 解析的本质是字符串处理。我见过很多同学用strtok直接切分字符串但在嵌入式环境中strtok会修改原字符串而且不是线程安全的所以我更倾向于写一个“按序号取字段”的辅助函数。这个函数从头开始扫描逗号数到第 n 个逗号后返回字段内容代码简单可控。char *nmea_field(char *line, int idx) { static char buf[32]; char *p line; int cur 0; while (p *p cur idx) { p strchr(p, ,); if (!p) return NULL; p; cur; } if (!p || *p \0) return NULL; int len 0; while (*p *p ! , *p ! * len 31) { buf[len] *p; } buf[len] \0; return buf; }取到字段之后最关键的是把 NMEA 里的度分格式转成我们习惯的小数度。NMEA 纬度2233.12345表示 22 度 33.12345 分转换公式就是度 分 / 60再根据南北纬、东西经加上正负号double nmea_to_decimal(const char *degmin, char dir) { double raw atof(degmin); int deg (int)(raw / 100.0); double minute raw - deg * 100.0; double decimal deg minute / 60.0; if (dir S || dir W) { decimal -decimal; } return decimal; }这条代码在解析 RMC 的纬度和经度时可以直接复用。经纬度转换出来的 double 类型在 Cortex-M3 上运算会慢一点但 GPS 一秒才更新一次这点开销完全可以忽略。如果你后续要传给 8 位单片机或者其他资源紧张的平台可以考虑用整数表示经纬度比如放大 1e7 倍存成 int32_t但本文涉及的 STM32F103 直接上 double 没问题。4.4 完整解析函数与时间日期处理GNRMC 的解析函数我通常写成这样解析成功后把数据填入结构体typedef struct { int valid; double latitude; double longitude; float speed_knots; float course; uint8_t hour, minute, second; uint8_t day, month; uint16_t year; } gps_info_t; int parse_rmc_line(char *line, gps_info_t *gps) { if (strncmp(line, $GNRMC, 6) ! 0 strncmp(line, $GPRMC, 6) ! 0) { return -1; } char *status nmea_field(line, 2); char *lat nmea_field(line, 3); char *lat_dir nmea_field(line, 4); char *lon nmea_field(line, 5); char *lon_dir nmea_field(line, 6); char *spd nmea_field(line, 7); char *crs nmea_field(line, 8); char *date nmea_field(line, 9); if (!status || status[0] ! A) { gps-valid 0; return -1; } if (!lat || !lat_dir || !lon || !lon_dir) return -1; gps-valid 1; gps-latitude nmea_to_decimal(lat, lat_dir[0]); gps-longitude nmea_to_decimal(lon, lon_dir[0]); gps-speed_knots atof(spd); gps-course atof(crs); // 时间字段hhmmss.sss char *time_str nmea_field(line, 1); if (time_str) { gps-hour (time_str[0] - 0) * 10 (time_str[1] - 0); gps-minute (time_str[2] - 0) * 10 (time_str[3] - 0); gps-second (time_str[4] - 0) * 10 (time_str[5] - 0); } // 日期字段ddmmyy if (date) { gps-day (date[0] - 0) * 10 (date[1] - 0); gps-month (date[2] - 0) * 10 (date[3] - 0); gps-year 2000 (date[4] - 0) * 10 (date[5] - 0); } return 0; }注意 NMEA 里的时间是 UTC 时间也就是格林尼治时间。我们国内调试时要换算成北京时间直接在小时上加 8 小时注意跨天回绕如果 hour 8 24小时减 24日期加一天。日期跨月、跨年的计算逻辑最好用一个判断函数不要只改小时。GPS 模块本身输出的星期几信息一般没有需要根据年月日自己算我一般直接用库函数mktime处理如果你的平台不带完整 C 库就用经典蔡勒公式。4.5 数据输出和串口调试技巧解析出经纬度后在调试阶段我会把结果直接从 USART2 打印到电脑串口工具方便在屏幕上看实时数据。打印格式建议用 CSV 或者 JSON比如printf(GPS,%d,%.6f,%.6f,%.2f,%.2f,%02d-%02d-%02d %02d:%02d:%02d\r\n, gps.valid, gps.latitude, gps.longitude, gps.speed_knots * 1.852, gps.course, gps.year, gps.month, gps.day, gps.hour, gps.minute, gps.second);刚开始调串口时如果看不到任何输出优先检查模块是否上电、TXD 是否真的接在 STM32 的 RX、两边波特率是否一致。我有一个排查顺序口诀先电压、再接线、后波特率。电压短路烧模块是最可惜的。这个 CSV 格式在后续做上位机或者树莓派接收时也很方便Python 用pandas.read_csv()一读就能画轨迹不需要再做协议转换。5. 实际应用中最容易忽略的误差、周翻转和坐标转换5.1 GPS 误差来源和信号质量判断GPS 不是厘米级定位普通民用模块误差在 2 到 10 米都很正常。误差来源主要有卫星钟差和星历误差、信号穿过电离层和对流层时产生的延迟、城市高楼间的多径反射以及卫星几何分布造成的定位解算放大。实际使用中我们最直观能观察到的就是卫星数量和 HDOP 值。卫星数多不代表定位精度就高还要看 HDOP。HDOP 小于 1 是极好的几何分布1 到 2 是正常水平2 到 5 定位精度会下降大于 5 基本只能看趋势不能做精细轨迹。所以解析 GGA 帧时我建议把 HDOP 也存下来判断数据是否可信时用“状态 A 卫星数 4 HDOP 3”作为门槛。这个习惯在我做轨迹记录项目时救了命否则在城市峡谷里走一圈轨迹上全是乱七八糟的漂移点。另外GPS 的速度单位是节1 节等于 1.852 公里/小时打印给用户看时要记得换算。如果你在做车载设备速度数据用 GPS 的比轮速传感器更稳定但延迟也稍微大一点这个需要结合具体项目权衡。5.2 GPS 周翻转补丁问题GPS 系统有一个底层的时间计数机制叫“GPS 周”。全球定位系统从 1980 年 1 月 6 日开始计时以周为单位累加用 10 位二进制数来表示周数最大只能到 1023 周满了之后会翻转回 0。2019 年 4 月 6 日深夜GPS 周计数就经历了这样一次翻转。如果接收机固件没有做对应处理日期计算会跳回 20 年前导致日志文件时间错乱、加密通信验证失败甚至某些设备无法定位。这跟我们在 STM32 上使用有什么关系如果你手上是库存比较老的 VK2828U7G5 模块或者内部固件版本太旧就有可能在特定时间点输出错误的日期。解决办法有两个方向一是用官方工具升级模块固件u-blox 系列可以用 u-center 软件连接模块查看固件版本并升级二是在单片机解析侧做补偿——当解析出年份明显异常时比如小于 2015自动加上 1024 周对应的大约 19.6 年。我在自己的解析器里加了一个简单的年份修正函数防止模块固件没升级时日志时间混乱。如果你只是做实验不用在意这个问题但产品要长期部署这功能值得保留。判断方法也很简单定位正常后看看模块输出的日期是否是当前日期。5.3 WGS84 坐标和高德、百度坐标偏移的转换模块直接输出的经纬度坐标是 WGS84 坐标系也就是全球通用的 GPS 原始坐标系。但国内地图厂商出于安全考虑会将坐标进行一次加偏处理得到 GCJ-02 坐标系也就是俗称的“火星坐标系”高德地图、腾讯地图用的都是这套坐标。百度地图又在 GCJ-02 基础上做了一次二次偏移变成 BD-09。所以直接拿 STM32 输出的原始经纬度标到高德地图上你会发现定位点偏出去几十米甚至上百米这非常常见不是 GPS 模块坏了。解决办法是在上位机或者 PC 端处理时把 WGS84 坐标转成目标坐标系。下面这段 Python 脚本可以辅助验证转换逻辑用法是输入 WGS84 经纬度输出高德使用的 GCJ-02 经纬度import math def wgs84_to_gcj02(lng, lat): a 6378245.0 ee 0.00669342162296594323 dlat transform_lat(lng - 105.0, lat - 35.0) dlng transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) return lng dlng, lat dlat def transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret在嵌入式端如果不想引入这么复杂的三角函数可以先把原始坐标通过串口传回服务器用 Python 或 Java 在服务器端做转换效果一样。如果你要把转换做成 C 语言版本我也可以告诉你查表法或者简化公式的注意事项但核心逻辑就是上面的数学公式。5.4 我踩过的其他坑GPS 项目看起来简单实际操作容易在细节上翻车。下面几个问题都是我在调试时真实遇到过的。第一模块放在桌上定位很久没有信号。这其实不一定是模块坏了而是桌面离窗户太远头顶被楼板挡住GPS 信号进不来。解决办法是尽量把天线放到离窗户近、朝天空无遮挡的位置或者用延长线引出室外。我在室内调试时经常直接趴到窗边才能定位。第二定位成功后数据偶发跳点。排查后发现是 USB-TTL 调试工具供电不稳定GPS 模块和 STM32 共用一根 USB 供电线电机一转电源电压就掉模块瞬间丢失信号。后来给模块单独用线性稳压供电跳点立刻消失。第三串口输出大量乱码。排查后发现是模块供电 5V而 STM32 的 RX 是 3.3V 电平导致电平不匹配。把模块供电降到 3.3V 后正常。所以一定要先确认电平兼容。第四用了有源天线还是定位慢。有源天线分 3.3V 和 5V 两种供电规格接错电压轻则无信号重则烧坏内部 LNA。买天线前必须确认模块引脚电压接反或者接错电压后模块收星数量会明显偏低。6. 项目扩展与实用调试经验6.1 把 GPS 数据推到树莓派或 K210 做二次开发STM32 解析完 GPS 以后数据不要只停留在单片机里面很多时候要交给上位机做显示或算法处理。最简单的方案是通过串口把文本帧转发到树莓派树莓派上跑 Python 接收也可以转发给 K210 做视觉和定位融合。这个场景里我在 4.5 节提到的 CSV 输出格式就很实用上位机只需要按行读取不需要实现 NMEA 协议。树莓派端用pyserial读取串口数据的示例逻辑如下import serial ser serial.Serial(/dev/ttyAMA0, 115200, timeout1) while True: line ser.readline().decode(errorsignore).strip() if line.startswith(GPS,): parts line.split(,) # parts[1] 有效状态, parts[2] 纬度, parts[3] 经度 ... print(parts)树莓派和 STM32 之间通信时两个 3.3V UART 直接连接即可注意共地。如果树莓派用的专用 GPS 扩展板有的默认输出路径是/dev/ttyS0需要打开串口配置这里就不展开了。6.2 轨迹记录和时间同步的高级玩法解析 GPS 只是第一步项目落地时通常还要做两件事轨迹记录和精准时间同步。轨迹记录就是在解析到有效定位后把时间、经纬度、速度写入 SD 卡。我在 STM32 上用的方案是 FatFS 文件系统加 CSV 文件追加写入实测每秒写一条记录16GB SD 卡能写很久。时间同步是 GPS 模块的另一大价值。GPS 系统自带原子钟时间精度很高。模块的 PPS 引脚每秒输出一个上升沿边沿和 GPS 秒时刻对齐误差通常在几十纳秒级别。把 PPS 接到 STM32 的外部中断引脚结合 RMC 帧里的整秒时间就能实现系统时钟自动校时。这个方案对数据采集设备、电力监测终端的意义很大。如果只需要秒级同步直接解析 RMC 里的 UTC 时间就够了。6.3 根据我的经验总结的调试小技巧最后分享几个花时间换来的小技巧。第一测试 GPS 之前先到空旷地方把模块摆平确认首次定位成功后再拿回实验室修改代码。这样可以排除“定位环境差”这个干扰变量。第二串口助手打印 GPS 数据时建议打开 HEX 显示看一眼确认数据里面没有异常字符。很多乱码问题在 HEX 模式下能直接定位到是电平问题还是波特率问题。第三解析 GNRMC 和 GNGGA 不等于解析全部协议。不同版本的 VK2828U7G5 模块输出的语句名可能略有差别有些老模块只输出 GP 开头有些新模块输出 GN 开头。写解析器时最好对$GPRMC和$GNRMC都兼容。第四日志记录里一定要保留原始 NMEA 字符串。我一开始只存解析后的经纬度后来做误差分析时发现数据不对却没有原始数据可以追溯。在 SD 卡或调试串口里把原始帧存下来后面所有问题都好查。第五模块定位成功后会发生“经纬度在一定范围内抖动”这是正常现象不是程序 Bug。如果你要在固定点测精度可以连续采集 100 个点再求平均而不是盯着单点看。实际项目里我也常用滑动平均滤波来处理定位点效果比直接输出稳定很多。