简介:基于STM32的GPS导航完整工程包,面向嵌入式学习者、电子竞赛参与者及需要快速搭建导航原型的开发者,内含可直接导入的Keil MDK工程。包内共有548个文件,压缩后20.23MB,主要包含124个.h头文件、93个.c源码文件、86个.o目标文件、81个.d依赖文件,以及uv2工程文件、链接脚本、汇编启动文件等,覆盖从源码到编译产物的完整Keil MDK项目结构。资料已有3059人学习下载。内容围绕GPS数据接收、NMEA协议解析、定位与地图处理、路径规划、UC/GUI界面显示、用户交互和电源管理等模块展开,可具体参考其UART/SPI外设配置、GPGGA/GPGLL报文解析代码、窗口与控件创建方式,以及工程文件组织方式。对于希望将STM32与图形界面结合实现导航类功能的开发者,这份资料能提供清晰的模块划分、可复用的代码框架,并降低从零搭建系统的时间成本。
1. 基于 STM32 的 GPS 导航:为什么要自己动手而不是买现成模块
很多人第一次接触“基于 STM32 的 GPS 导航”这个标题,脑子里浮现的第一个问题是:淘宝上几十块钱的 GPS 模块插上就能用,手机导航更是免费又精准,为什么还有人要在 STM32 上折腾一套导航系统?答案是:手机导航解决的是“人在哪、往哪走”,而 STM32 导航解决的是“机器在哪、该往哪走”的问题。当你做的是无人车、巡检小车、农业机器人、航模地面站,或者只是想把 GPS 数据融合进自己的传感器系统里,一个现成的导航终端根本满足不了需求——你需要的是能够自定义解析、自主决策、甚至和电机控制直接联动的整套方案。
这个标题真正要做的事情并不复杂:用一颗 STM32 单片机读取 GPS 模块输出的 NMEA 协议数据,解析出经纬度、速度、航向、卫星数等信息,再根据预设目标点计算出航向偏差和控制指令,最终在屏上显示或驱动运动机构完成导航任务。它覆盖了嵌入式开发里最典型的三层链路:传感器数据采集、数据解析与算法处理、执行与控制输出。适合正在学 STM32 的开发者、做智能车项目的学生,以及想给自己的设备加上定位导航能力的工程师。接下来这篇文章会把协议、硬件连接、源码、算法、参数整定和常见坑一条条讲清楚,照着做就能在桌面上跑通第一版。
2. GPS 导航的基本链路:从 NMEA 协议到单片机指令的完整流程
2.1 理解 NMEA 0183:GPS 模块其实只做一件事
GPS 模块本身并不“导航”,它的唯一任务就是把卫星接收到的定位信息按标准格式输出。绝大多数消费级模块使用的协议叫 NMEA 0183,它是一套纯文本的串口通信协议。每条语句以$开头,后跟 5 个字符的语句标识符,接着是逗号分隔的数据字段,最后是校验和。STM32 要做的工作不是帮模块算位置,而是读字符串、拆字段、转数值,再把结果用于后续计算。
我们最关心的语句是$GPRMC(推荐最小定位信息)和$GPGGA(定位信息)。RMC 里包含 UTC 时间、定位状态(A 表示有效)、纬度、北纬/南纬标识、经度、东经/西经标识、速度(节)、航向角(度)、日期和磁偏角。GGA 则提供卫星数、海拔高度等信息。一个典型的数据帧长这样:
$GPRMC,063612.00,A,3110.55802,N,12124.74660,E,0.027,131.21,260224,,,D*73解析这个字符串有两个常见误区。第一个是拿字符串拆分函数硬切所有字段,遇到字段缺失时数组越界;第二个是纬度格式3110.55802不是 31.1055802 度,而是“度分”格式——31 度 10.55802 分,需要额外换算成十进制度才能参与距离计算。我在第一次做的时候直接把这个数当十进制度算了,结果目标点偏离了几十公里,排查了半天才发现是这个格式问题。
2.2 硬件连接:串口、供电、天线位置的工程要点
GPS 模块大多通过 UART 和 STM32 通信,默认波特率常见的是 9600 或 115200,具体要看模块手册。连接上很简单:模块的 TX 接 STM32 的 RX,模块的 RX 接 STM32 的 TX,GND 共地,VCC 按模块要求接 3.3V 或 5V。这里有一个新手经常踩的坑:如果模块是 5V 逻辑电平,而 STM32 的 GPIO 容忍度不够,最好加一个电平转换或者选择 3.3V 逻辑的模块,否则串口数据会出现乱码或者偶尔丢字节的现象。
天线位置是个经验活。放在室内窗边能收到部分卫星,但定位精度和稳定性都得不到保证;放在金属外壳内部则大概率完全没有定位。我一般调试时把天线用胶带固定在窗框上,且天线陶瓷面朝上朝外。模块启动后首次定位需要冷启动时间,从十几秒到几分钟不等,这个时间段里串口会有输出但状态位不是 A,程序里必须做好超时和无效状态的处理,否则系统一开机就拿到一组无效坐标,后续全部计算没有意义。
2.3 最小可运行的串口接收框架:中断 + 环形队列
直接在主循环里用阻塞方式读串口会有一个问题:GPS 数据是持续不断输出的,每秒钟可能来好几条语句,主循环一旦被别的任务占用,数据就会丢失。常见的做法是把串口接收放在中断里,配合一个环形缓冲区,把完整的数据帧存下来,由主循环去解析。环形队列的好处是接收和解析解耦,中断只管往队列里写,主循环按自己的节奏取数据,不会互相卡死。
下面给出一个最精简的串口接收框架,基于 STM32 的 HAL 库编写。这个框架的作用是把串口收到的每个字节都存进一个环形缓冲区,然后应用程序从缓冲区里逐字节读取并拼帧:
// ring_buffer.h #define RING_BUF_SIZE 512 typedef struct { uint8_t buffer[RING_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer; void RingBuffer_Init(RingBuffer *rb); uint8_t RingBuffer_Write(RingBuffer *rb, uint8_t data); uint8_t RingBuffer_Read(RingBuffer *rb, uint8_t *data); uint16_t RingBuffer_Count(RingBuffer *rb);// ring_buffer.c #include "ring_buffer.h" void RingBuffer_Init(RingBuffer *rb) { rb->head = 0; rb->tail = 0; } uint8_t RingBuffer_Write(RingBuffer *rb, uint8_t data) { uint16_t next = (rb->head + 1) % RING_BUF_SIZE; if (next == rb->tail) { return 0; // 缓冲区满 } rb->buffer[rb->head] = data; rb->head = next; return 1; } uint8_t RingBuffer_Read(RingBuffer *rb, uint8_t *data) { if (rb->head == rb->tail) { return 0; // 缓冲区空 } *data = rb->buffer[rb->tail]; rb->tail = (rb->tail + 1) % RING_BUF_SIZE; return 1; } uint16_t RingBuffer_Count(RingBuffer *rb) { return (uint16_t)((rb->head + RING_BUF_SIZE - rb->tail) % RING_BUF_SIZE); }// 串口中断回调 extern RingBuffer gps_rb; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { static uint8_t rx_byte; if (huart->Instance == USART1) { RingBuffer_Write(&gps_rb, rx_byte); HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 重新使能接收中断 } }这里最关键的参数是RING_BUF_SIZE。GPS 一条 NMEA 语句最长不过 100 个字节左右,但模块可能连续输出多条语句,所以 256 字节勉强够用,512 字节更稳妥。缓冲区满了之后写入会失败,这时候不会造成内存越界,只会丢数据,因此解析端要尽量及时消费。需要注意的是,中断接收必须使用HAL_UART_Receive_IT一次接收一个字节的模式,并在回调里重新开启接收,否则只能收到第一字节就再也没数据进来。
主循环侧的帧同步可以检查$字符作为帧头,遇到\r\n作为帧尾,再把这一帧字符串送入解析函数。这个方案相比直接调用HAL_UART_Receive接收定长数组,最大的好处是每条 NMEA 语句长度不固定,不用预先知道长度就能正确切帧。缺点是如果两个$之间数据异常,比如模块复位后输出半截语句,需要做异常帧丢弃和重新同步的逻辑。
3. 解析与坐标换算:把经纬度字符串变成可计算的距离
3.1 GPRMC 解析函数的完整实现与字段边界处理
解析 NMEA 最常见的方式是按逗号拆分字符串,然后把需要的字段用strtof或atof转成浮点数。但这个做法在嵌入式平台上有一个隐患:GPS 模块输出字段在定位无效时可能为空,比如$GPRMC,,A,,,...这种情况下atof可能会返回 0,而程序并不知道这个 0 是真实值还是无效数据。正确的做法是先检查定位状态位,再决定是否解析经纬度。
下面这个函数实现了对 GPRMC 语句的解析,并做了字段缺失保护:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> typedef struct { uint8_t valid; // 定位是否有效 double latitude; // 十进制度纬度 double longitude; // 十进制度经度 float speed_knots; // 速度(节) float course_deg; // 航向角(度) uint8_t sat_count; // 卫星数量(来自GGA) uint8_t hour, minute, second; uint8_t day, month, year; // 日期 } GpsData; static double nmea_to_decimal(double ddmm, char direction) { int degrees = (int)(ddmm / 100); double minutes = ddmm - degrees * 100; double decimal = degrees + minutes / 60.0; if (direction == 'S' || direction == 'W') { decimal = -decimal; } return decimal; } uint8_t Gps_ParseRMC(const char *line, GpsData *gps) { char temp[128]; strncpy(temp, line, sizeof(temp) - 1); temp[sizeof(temp) - 1] = '\0'; char *token = strtok(temp, ","); int field_index = 0; char lat_dir = 'N', lon_dir = 'E'; double lat_raw = 0.0, lon_raw = 0.0; uint8_t status = 0; gps->valid = 0; while (token != NULL) { switch (field_index) { case 1: // UTC 时间 hhmmss if (strlen(token) >= 6) { gps->hour = (token[0] - '0') * 10 + (token[1] - '0'); gps->minute = (token[2] - '0') * 10 + (token[3] - '0'); gps->second = (token[4] - '0') * 10 + (token[5] - '0'); } break; case 2: // 定位状态 A/V status = (token[0] == 'A') ? 1 : 0; break; case 3: // 纬度 ddmm.mmmm lat_raw = atof(token); break; case 4: // 北纬/南纬 lat_dir = token[0]; break; case 5: // 经度 dddmm.mmmm lon_raw = atof(token); break; case 6: // 东经/西经 lon_dir = token[0]; break; case 7: // 地速(节) gps->speed_knots = atof(token); break; case 8: // 航向角 gps->course_deg = atof(token); break; case 9: // UTC 日期 ddmmyy if (strlen(token) >= 6) { gps->day = (token[0] - '0') * 10 + (token[1] - '0'); gps->month = (token[2] - '0') * 10 + (token[3] - '0'); gps->year = (token[4] - '0') * 10 + (token[5] - '0'); } break; default: break; } token = strtok(NULL, ","); field_index++; if (field_index > 12) break; } if (status) { gps->latitude = nmea_to_decimal(lat_raw, lat_dir); gps->longitude = nmea_to_decimal(lon_raw, lon_dir); gps->valid = 1; } return gps->valid; }这段代码的关键是用了strtok按逗号切分,并在每个 case 里对字段长度做了检查。nmea_to_decimal函数把“度分”格式转换成十进制度——这个转换是 GPS 解析里最容易翻车的地方,不少人直接用原始值参与距离计算,导致几百米的误差。strtok会原地修改字符串,所以这里把原始行先拷贝到临时数组里再操作,防止破坏原始帧缓冲区。
3.2 距离与航向计算:Haversine 公式和方位角
拿到十进制度的经纬度之后,下一步是计算当前位置和目标点的距离与方位角。距离计算推荐 Haversine 公式,它在几百米到几千公里的范围内都能保持较高精度,而且计算量在 STM32 上完全可接受。方位角公式用于告诉控制系统“目标在哪个方向”,是导航控制的关键输入。
#include <math.h> #define PI 3.14159265358979323846 #define EARTH_RADIUS_M 6371000.0 static double deg2rad(double deg) { return deg * PI / 180.0; } double Gps_DistanceMeters(double lat1, double lon1, double lat2, double lon2) { double dlat = deg2rad(lat2 - lat1); double dlon = deg2rad(lon2 - lon1); double a = sin(dlat / 2) * sin(dlat / 2) + cos(deg2rad(lat1)) * cos(deg2rad(lat2)) * sin(dlon / 2) * sin(dlon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); return EARTH_RADIUS_M * c; } double Gps_BearingDeg(double lat1, double lon1, double lat2, double lon2) { double phi1 = deg2rad(lat1); double phi2 = deg2rad(lat2); double dlambda = deg2rad(lon2 - lon1); double y = sin(dlambda) * cos(phi2); double x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(dlambda); double bearing = atan2(y, x) * 180.0 / PI; if (bearing < 0) bearing += 360.0; return bearing; }方位角返回的是 0 到 360 度的值,0 度代表正北,90 度代表正东。这个角度要和当前运动航向做差,才能得到转向控制量。这里有一个细节:GPS 模块输出的航向角是“运动方向”,静止不动时该值可能是个无效或者漂移的值。所以在控制逻辑里,如果速度低于某个阈值,应当忽略航向角,否则原地打转时角度乱跳,系统会给出错误的转向指令。
海平面高度在二维导航里一般用不到,但如果你做的是无人机这类需要高度信息的多旋翼平台,可以从 GPGGA 语句里解析海拔值。解析方法类似,只需要找到对应字段再atof,这里不再赘述。
4. 导航控制算法:比例转向 + 目标点到达判断 + 参数整定
4.1 转向控制:角差计算与死区处理
导航的核心控制逻辑其实不复杂:算出当前点和目标点的方位角,再用这个方位角和当前运动航向角做差,得到角差delta = bearing - course,然后用 PID 或者纯比例控制去驱动转向机构。角差要归一到 -180 到 180 度之间,否则会出现一个很典型的问题:目标在正北,当前航向是 359 度,角差算出来是 1 度(正确),但如果直接相减得到的是 -358 度,比例控制器就会给出一个大幅度的反向转向指令,车会画一个奇怪的圈。
归一化角差的代码非常短,但价值极高:
float NormalizeAngleDiff(float diff) { while (diff > 180.0f) diff -= 360.0f; while (diff < -180.0f) diff += 360.0f; return diff; }死区处理是另一个关键参数。GPS 属于低更新率传感器,常见模块输出频率在 1Hz 到 10Hz 之间,而路径角差本身带有噪声,如果转向系统对每个细微的角差都做响应,车会走出一条蛇形轨迹。一般做法是设置一个死区阈值,比如角差绝对值小于 5 度时不转向。这个值需要根据转向机构的响应速度来调:舵机响应快,死区可以小一点;差速驱动的电机响应慢,死区要放得宽一些。
4.2 到达判断:半径阈值 + 连续计数,避免“绕圈圈”
到达判断看似简单——距离小于阈值就停——但实际运行中有一个很常见的现象:当车靠近目标点时,GPS 定位误差可能达到两三米,车会在目标点附近反复进出,控制指令在“前进”和“停止”之间抖动,甚至绕着目标点打转。我在地面小车上遇到过这个情况,现象就是车到了目标点附近以后一直来回画圆弧,始终不触发到达条件。
解决方法是双条件:距离小于阈值同时持续若干个控制周期。比如设定到达半径 3 米,连续 5 个解析周期(约 5 秒)都满足条件,才认为到达。这样即使某一瞬间定位误差把距离拉到 4 米,只要下一时刻回到阈值内,连续计数不被打断,依然能正确判定到达。计数器一旦中断就重置,避免瞬时抖动造成的误判。
uint8_t Arrival_Check(double dist_m, float arrival_radius_m, uint8_t need_count) { static uint8_t cnt = 0; if (dist_m < arrival_radius_m) { cnt++; if (cnt >= need_count) { cnt = 0; // 到达成功,清零等待下次任务 return 1; } } else { cnt = 0; } return 0; }这个函数的三个参数都不难理解:arrival_radius_m取决于 GPS 精度与控制对象的尺寸,小车 2~5 米,无人机可以更小;need_count取决于控制周期时长和系统需要多大容忍度。注意每到达一个航点后要手动清零计数器,否则下次判断还会带着旧的计数状态。
4.3 必调参数:比例系数、死区、到达半径和更新率
在真实调试中,最容易影响导航效果的参数是比例系数Kp。它决定了角差对应的转向量大小:Kp 太小,转向迟钝,车在接近目标时会走一个很大的弧线,甚至超过目标点然后绕回来;Kp 太大,转向过猛,车会左右来回摆头,轨迹呈现明显的锯齿状。常见的起步值是 0.5~1.5,然后根据实际轨迹逐次调整。
下面这张表列出了我调试地面小车时常用的参数范围和调整方向:
| 参数 | 常见范围 | 数值过大时的现象 | 数值过小时的现象 | 调整建议 |
|---|---|---|---|---|
| Kp(转向比例) | 0.5 ~ 2.5 | 蛇形振荡、转向过猛 | 转弯弧线过大、到位慢 | 先给 1.0,观察轨迹再增减 |
| 死区 | 2° ~ 8° | 转向不灵敏、路径呈折线 | 高频抖动、舵机发热 | 以轨迹不抖动为准 |
| 到达半径 | 2 ~ 5 米 | 提前停车、未到目标点 | 在目标点附近反复进出 | 结合 GPS 静态漂移幅度来定 |
| 控制周期 | 200 ~ 500 ms | 响应跟不上动态变化 | CPU 占用高、舵机频繁动作 | 数据更新率为 1Hz 时取 500ms 即可 |
控制周期这里多说一句:GPS 模块输出频率固定,控制周期没有必要比数据更新率快太多。比如模块每秒输出一帧定位,控制周期设到 100ms 也拿不到新数据,反而让同一个位置被反复计算,浪费 CPU 还容易加剧转向抖动。我一般把控制周期设为数据更新周期的一半到一倍之间,用定时器中断触发控制任务,避免主循环忙等。
5. GPS 导航避坑指南:静漂、串口乱码、首次定位失败
5.1 现象:定位成功后坐标仍然缓慢漂移
定位状态是 A,卫星数也有七八颗,但屏幕上的坐标在静止状态下会缓慢移动,距离值在一两米范围内波动。这个现象叫“静态漂移”,是 GPS 系统的物理特性,不是代码 bug。原因主要有两个:卫星信号的星历误差和多路径效应——城市环境里信号经过墙面反射后到达接收机,路径变长造成伪距误差。无解的玄学是:同一个位置,上午漂移小、下午漂移大,和卫星星座几何分布有关。
解决的常用手段是做坐标平滑。最简单的是一阶低通滤波,把当前解析值和上一次有效值做加权平均。系数不要过大,否则车在真正转弯时会明显滞后。我的做法是只对静止状态做滤波,速度低于 0.1 节时启用强滤波,速度上来后立刻切换到原始值,这样既压住了静漂,又不影响动态响应。另外可以把到达半径设得比静漂范围大 50% 左右,从源头避免误判。
5.2 现象:串口输出乱码,解析帧同步失败
乱码通常有三种原因。第一种是波特率不匹配,模块实际输出 115200 而代码里配置成了 9600,现象是偶尔能解开一条帧但大多时候乱码。第二种是模块逻辑电平与 STM32 不兼容,5V 模块直接接 3.3V 引脚导致信号畸变。第三种是共地问题,模块和单片机之间没有连 GND,串口参考电平不一致。排查顺序很明确:先用 USB 转串口工具接模块,在电脑上看数据是否正常;再示波器看波形幅值是否在 0 到 3.3V 之间;最后检查两个板子的地是否连在一起。
还有一种隐蔽的情况是天线与模块之间的连接线过长或者接触不良,导致信号衰减到恰好能锁定卫星但数据误码率高。这种情况在串口屏幕上看到的是每隔一段时间出现一条异常短帧或者空帧,这时优先检查射频连接而不是改代码。
5.3 现象:上电后长期无法定位
模块上电后串口有数据,但状态位一直是 V,定位状态迟迟不变成 A。室内的概率最大,因为 GPS 信号经过墙壁衰减后强度不够,模块无法完成定位。另一个常见原因是天线放在金属物体附近——CT 金属外壳、铝合金底盘都会屏蔽信号。还有一种是模块之前使用过,内部保存的星历过期,冷启动需要重新下载卫星历书,这个过程在信号差的条件下可能持续 5 到 10 分钟。
工程上的习惯是把天线放置位置和上电时序做成可复现的固定流程:先把天线放到室外或者窗边,再给模块上电;如果条件允许,初始化时给模块发送冷启动命令(不同模块命令不同,常见的是发送特定的串口配置指令),清除旧星历,让它重新搜星。这种做法在环境不变的情况下能显著缩短首次定位时间。调试代码时还要给定位状态加一个超时判断,比如 3 分钟内没有拿到有效定位就报错并重启模块,避免系统挂在等待状态。
5.4 现象:到达目标点附近后绕圈不止
前面 4.2 节提到过连续计数方案,这里展开说一下实际调试中的经验。绕圈的根因有两个:一是到达半径小于 GPS 静漂幅度,系统在目标点附近永远无法稳定判定到达;二是控制周期太快或者 Kp 太大,车带着速度冲进半径区域后冲过头,又转回来再次冲入,形成周期振荡。
解决的组合拳是:把到达半径加大到静漂幅度的两倍以上;控制周期和 GPS 更新率对齐而不是比它快很多;在距离目标点小于两个到达半径时给速度一个下坡函数,让车提前减速而不是全速冲入。速度下坡不需要复杂算法,线性映射即可——距离越近目标速度越小,低于最小速度后直接停车判断。这个思路和工业上的“分段减速”本质一致,在低成本小车上是性价比最高的方案。
5.5 现象:解析出的经纬度在跳变,距离值偶尔异常大
如果解析出来的经纬度偶尔跳到非常离谱的位置,比如前一秒还在上海,下一秒到了非洲,通常是解析越界或者野指针问题。这个问题在 MCU 上比在电脑上更容易出现,因为单片机没有内存保护,读错数组地址不会直接崩溃,而是返回一个随机值。我排查过的一个具体案例是:GPRMC 语句里某个字段为空,strtok返回NULL,而代码继续atof(token),行为未定义,在小概率情况下读到相邻内存区域的非法数值。修复方式就是在每个字段使用前判空,这也是 3.1 节代码里为什么大量做了字段长度检查的原因。
另外要注意浮点数在 Cortex-M 系列的兼容性。老一些的 STM32F1 系列没有硬件浮点单元,用double做大量三角函数运算会占用大量 CPU 时间。如果控制周期要求高,建议整篇代码用float代替double,在 STM32F4 系列上可以用FPU加速,但也要在编译选项中开启硬浮点支持,否则性能差距能差出十倍。
6. 从单点导航到路径跟踪:航点队列、LCD 显示与实用调试技巧
单点导航能跑通之后,往上做最自然的扩展就是航点队列。所谓航点队列,就是把多个目标点按顺序存进一个数组,导航系统逐个到达、逐个弹出,全部到达后任务完成。这个扩展不需要改动底层解析和距离计算代码,只需要在到达判断的基础上增加航点状态管理。
一个简单的实现思路是维护一个数组和当前索引:
#define WAYPOINT_MAX 16 typedef struct { double lat; double lon; float arrival_radius_m; } Waypoint; typedef struct { Waypoint points[WAYPOINT_MAX]; uint8_t count; uint8_t current_index; uint8_t is_running; } WaypointMission; uint8_t WaypointMission_Update(WaypointMission *m, double cur_lat, double cur_lon) { if (!m->is_running) return 0; if (m->current_index >= m->count) { m->is_running = 0; // 全部完成 return 2; } Waypoint *wp = &m->points[m->current_index]; double dist = Gps_DistanceMeters(cur_lat, cur_lon, wp->lat, wp->lon); if (Arrival_Check(dist, wp->arrival_radius_m, 5)) { m->current_index++; return 1; // 到达一个航点 } return 0; }这个结构配合 LCD 显示可以很直观地看到系统状态:当前航点编号、距离目标还有多少米、方位角差多少、卫星数量、定位状态。LCD 显示本身很简单,但信息布局有讲究——把“是否有效定位”放在最显眼的位置,因为调试时你第一眼需要知道的是当前数据能不能信,而不是看具体经纬度。
调试技巧上,我强烈建议把解析后的数据回传一个调试串口,用串口助手记录完整运行日志。日志里除了经纬度,还要带上原始 NMEA 帧、解析状态、角差、控制量、判定状态。这样出现问题后翻日志,能看到是解析丢了帧还是控制参数不合适,而不是对着现象猜原因。有一个血泪经验:之前的某次调试中车辆轨迹偏移严重,查了一圈代码都没问题,最后看日志才发现是 GPS 模块的波特率被自己之前的配置命令改掉了——模块配置是非易失性的,改一次会一直生效。所以调试时电源共地、引脚连接确认完,一定还要确认模块配置参数,别跳过这一步完美复现问题。
硬件和算法都稳定后,可以做进一步的精度优化。比如结合陀螺仪做航向融合,在 GPS 更新间隙用惯性数据推算位置;或者把卡尔曼滤波引入位置估计,解决静漂和丢星期间的定位中断。这些属于进阶方向,但底层框架不用推倒重来——解析、航点管理、控制输出这些代码都可以原样保留,只替换“位置来源”这一个数据源即可。我自己做这类项目时的一个习惯是:随时保留一个“纯 GPS 模式”的编译开关,融合出问题的时候能快速切回来确认基础功能没有退化。
导航系统最怕的不是算错一次,而是无法判断“什么时候数据不可信”。所以最后留一个建议:在你的状态机上务必做出“定位无效/丢星”的显式状态,而不是靠坐标值兜底。丢星时坐标会停在最后一个有效值上,如果继续用这个值导航,车会朝着几分钟前的位置一路狂奔。显式的超时检测加上一个简单的状态指示,能省下你大量在露天场地来回跑的时间。希望这篇基于 STM32 做 GPS 导航的实战拆解能帮到你,少走我绕过的那几段弯路。
本文还有配套的精品资源,点击获取