简介:这款基于STM32F103与ATGM332D-5N模块的GPS定位例程,适合嵌入式初学者和物联网开发者参考,目标是解决STM32与GPS模块的串口通信、NMEA协议解析以及定位数据提取等核心问题。压缩包共133个文件,以C源码和H头文件为主,同时包含Keil工程配置、编译生成的AXF/HEX文件以及bat清理脚本等,整体仅1.4MB,结构清晰,便于快速定位主程序与驱动代码。已有2430人学习下载。例程覆盖RCC时钟初始化、USART波特率匹配、GPGGA/GPRMC语句解析和经纬度提取等关键环节,可帮助读者打通从硬件接线到串口调试、再到数据输出的完整链路。同时保留完整Keil工程配置,适合直接修改移植。
1. 一块 STM32F103 把 GPS 定位数据吃干榨净,难点根本不在硬件
一块几十元的 STM32F103C8T6,接一只 NEO-8M 或 ATGM336H 模块,就能稳定输出经纬度、速度、航向和 UTC 时间,这套组合在共享定位终端、车载定位器、环境监测小车里到处都是。很多工程师以为 GPS 定位设计的难点在射频电路,实际上手后才发现,真正让人熬夜的是软件侧:串口丢帧、GPRMC 一直是 V 状态、度分格式怎么转成地图能用的十进制度,还有 GPS 翻转补丁没打导致跨周后坐标凭空偏出去几百公里。这篇文章从串口接线、NMEA 0183 解析、校验和算法讲到 C/C++ 工程里的封装与误差处理,把 GPS 模块变成 STM32F103 上稳定可用的数据源,新手可以照抄代码,熟手也能在参数和边界处理里找到值得抠的细节。
2. GPS 模块选型与 STM32F103 串口接线:先把信号干净地拿进来
2.1 先决定用哪颗 GPS 芯片,别只看价格下单
STM32F103 的例程里,GPS 模块选型往往被一句话带过,实际上模块差异直接决定串口参数和后期调试方式。目前开源项目里常见的几颗芯片分别是 u-blox NEO-6M/NEO-8M、中科微 ATGM336H 系列,以及国产的 UM980 这类 RTK 级产品,它们在 STM32F103 场景下的定位完全不同。
| 模块方案 | 常用供电 | 默认波特率 | 输出协议 | 典型场景 |
|---|---|---|---|---|
| u-blox NEO-8M | 3.3V(板载 LDO 可接 5V) | 9600 | NMEA 0183 / UBX | 低成本手持、车载定位 |
| ATGM336H-5N | 3.3V | 9600(可配) | NMEA 0183 | 对国产化有要求的项目 |
| NEO-M8N | 3.3V | 9600 | NMEA / UBX | 低功耗电池供电设备 |
| RTK 模块(如 ZED-F9P) | 3.3V/5V | 115200 | UBX / RTCM | 厘米级定位,超出本例程范围 |
选型时除了看价格,还要看三个容易被忽略的点:第一,模块是否内置天线馈电电路,这决定了你能否直接接有源天线;第二,默认波特率是否支持通过命令修改,ATGM336H 的波特率在上电后可以用配置语句写死,而部分模块需要外接 EEPROM 保存配置;第三,语句输出频率,NEO-8M 默认 1Hz,如果你做高速移动载体,需要通过 UBX-CFG-RATE 命令提高到 5Hz 或 10Hz,但 STM32F103 解析压力也会随之上升。
我一般会建议项目上用 ATGM336H 或 NEO-8M 起步,理由很简单:资料多、底子稳、默认 9600 波特率对 STM32F103 的 USART 压力小,解析代码也最容易调通。等整条链路跑顺了再换更高性能模块,只需要改波特率和对应的配置命令,解析层完全不用动。
2.2 供电、电平与无源陶瓷天线:三处一不留神就烧板子
GPS 模块和 STM32F103 连线看起来只有四根线,但这四个细节我见过太多人踩坑。首先是电平匹配,STM32F103 的 GPIO 是 5V 容忍,但 GPS 模块的串口 TX/RX 通常是 3.3V CMOS 电平,直接用一个 5V 单片机的 TX 去驱动 3.3V 的 RX 一般没问题,反过来如果模块是 5V 逻辑,就必须加电平转换,否则 STM32F103 的 PA10(USART1_RX)可能被拉坏。
其次是供电纹波,GPS 接收机对电源噪声比想象中敏感,模块的 VCC 上最好并联一个 10uF 钽电容和一个 100nF 陶瓷电容,如果模块和电机、继电器共用电源,还要串磁珠隔离。很多“GPS 收不到星”的故障排查到最后其实是电源纹波太大导致接收灵敏度下降。
第三是天线。板载无源陶瓷天线是大多数评估板的默认配置,成本低但抗干扰差,屏蔽盖下方贴近金属就会失锁。如果项目要外接有源天线,需要确保模块的 ANT_BIAS 引脚或者 RF_IN 馈电电路能输出 3.3V/5V 偏置电压。常见的做法是在射频输入端串联一个 10nH 到 47nH 的电感给天线馈电,同时在馈电点并联一个 100pF 电容接地,这个电路在多数模块的硬件参考手册里有完整图,直接抄参考设计即可。
注意:如果你拿到的是无源天线模块,又强行插有源天线,天线放大器没供电,信号强度不会提升反而可能更差。判断模块是否有源馈电,看规格书里是否有 ANT_BIAS 或 VCC_RF 引脚。
2.3 用 USART1 加中断把第一句 NMEA 拉回来
接线固定后,先不急着写解析,第一步是把 GPS 的原始输出在串口上打出来。USART1 的 PA9/PA10 通常被用作调试串口,这里建议 GPS 挂 USART2,调试信息走 USART1,互不干扰,后面在 PC 上看日志会舒服很多。这里以 STM32F103 标准外设库或 HAL 库为例,CubeMX 配置如下:USART2 异步模式,波特率 9600,8 数据位、无校验、1 停止位,开启 USART2 全局中断。
/* USART2 初始化:GPS 模块挂这里,9600-8-N-1 */ static void MX_USART2_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); }初始化完成后,调用一次HAL_UART_Receive_IT(&huart2, &rx_byte, 1),让串口每收到一个字节就进入一次中断,然后在回调函数里把字节转发到调试串口:
uint8_t rx_byte; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { /* 先把原始字节发到调试串口,确认 GPS 在输出 */ HAL_UART_Transmit(&huart1, &rx_byte, 1, 10); /* 重新开启单字节接收,保证下一字节还能进中断 */ HAL_UART_Receive_IT(&huart2, &rx_byte, 1); } }这段代码的逻辑是:GPS 模块上电冷启动后,一般 30 秒到几分钟内会开始向串口持续发送$GPRMC、$GPGGA等语句,单片机每收到一个字节就进中断,原样转发到 USART1。参数说明上,HAL_UART_Receive_IT的第三个参数填 1 表示只接收一个字节,配合回调函数实现逐字节流式处理,这是嵌入式串口接收 GPS 数据最常用的做法,内存占用小、响应快,也方便后续接入解析状态机。
在 PC 端用串口助手打开 USART1 对应的 COM 口,波特率同为 9600,如果能看到以$GPRMC开头、以*加两位十六进制数结尾的行,就说明 GPS 数据已经进入单片机。看不到就依次检查模块电源指示灯、TX 是否接到 PA3(USART2_RX)、串口助手波特率是否被模块的配置覆盖过。这里如果用的是 ST-Link 的虚拟串口,设备管理器里出现感叹号通常是驱动没装好,属于另一条排错线,和 GPS 本身无关。
3. NMEA 0183 逐字段解析:把 GPRMC 变成结构体
3.1 先读懂一帧 GPRMC,字段索引要背下来
NMEA 0183 是 GPS 模块最通用的输出协议,语句以$开头,以*加两位十六进制校验和结尾,字段用逗号分隔。STM32F103 例程里最常用的是$GPRMC,它包含时间、定位状态、经纬度、速度、航向和日期,一帧就能拿到定位所需的全部信息。
| 字段索引 | 含义 | 示例值 |
|---|---|---|
| 0 | 语句标识 | GPRMC |
| 1 | UTC 时间 hhmmss.ss | 082315.000 |
| 2 | 定位状态,A=有效,V=无效 | A |
| 3 | 纬度 ddmm.mmmm(度分格式) | 3103.4567 |
| 4 | 北纬 N / 南纬 S | N |
| 5 | 经度 dddmm.mmmm(度分格式) | 12011.7890 |
| 6 | 东经 E / 西经 W | E |
| 7 | 对地速度(节) | 12.34 |
| 8 | 对地航向(度) | 270.5 |
| 9 | UTC 日期 ddmmyy | 140425 |
| 10 | 磁偏角 | 003.2 |
| 11 | 磁偏角方向 | E |
| 12 | 定位模式指示 | A |
一帧真实的 GPRMC 长这样:$GPRMC,082315.000,A,3103.4567,N,12011.7890,E,12.34,270.5,140425,003.2,E,A*5F。解析的核心不是用strstr去整帧查找,而是把每个字段按逗号拆开,按索引取值。这里要注意的是,速度字段单位是节,转 km/h 要乘 1.852,很多例程漏掉这个换算,导致显示速度和手机地图差一倍。
定位模式字段比我们想象中更有用。NMEA 4.0 之后,字段 12 的A表示自主定位,D表示差分定位,E表示估算定位,如果这个字段是E,即使前面字段 2 是A,坐标可信度也要打折扣。STM32F103 端的判断逻辑应该把字段 2 和字段 12 同时考虑,而不是只看单一状态位。
3.2 用状态机代替字符串查找,串口回调直接喂字节
裸机环境下用HAL_UART_Receive_IT逐字节接收时,最忌讳在中断里做strlen、strstr这类耗时操作。正确的做法是把每个字节喂给一个状态机,状态机只做字符分类和字节拷贝,等一整帧接收完成后,再在主循环里做正式解析。
typedef enum { GPS_IDLE = 0, /* 等待起始符 $ */ GPS_TAG, /* 读取语句名,如 GPRMC */ GPS_BODY, /* 读取数据体,按逗号分字段 */ GPS_CHKSUM /* 读取 * 后面的两位校验和 */ } GpsParseState; static GpsParseState state = GPS_IDLE; static uint8_t tag[6]; static uint8_t tag_len = 0; static uint8_t body[96]; static uint8_t body_len = 0; static uint8_t calc_xor = 0; /* 边收边算的异或值 */ static uint8_t rcv_xor = 0; /* 从帧尾读到的异或值 */ static uint8_t chk_cnt = 0; void gps_byte_rx(uint8_t c) { switch (state) { case GPS_IDLE: if (c == '$') { state = GPS_TAG; tag_len = 0; calc_xor = 0; chk_cnt = 0; } break; case GPS_TAG: if (c == ',') { state = GPS_BODY; body_len = 0; } else if (tag_len < sizeof(tag) - 1) { tag[tag_len++] = c; calc_xor ^= c; /* $ 之后到 * 之前的所有字符参与异或 */ } break; case GPS_BODY: if (c == '*') { state = GPS_CHKSUM; } else { calc_xor ^= c; if (body_len < sizeof(body) - 1) { body[body_len++] = c; } } break; case GPS_CHKSUM: if (hex_val(c) >= 0) { rcv_xor = (rcv_xor << 4) | hex_val(c); if (++chk_cnt == 2) { if (rcv_xor == calc_xor) { gps_dispatch_frame(tag, tag_len, body, body_len); } state = GPS_IDLE; } } else { state = GPS_IDLE; /* 非法字符,扔掉整帧 */ } break; } }这段状态机的逻辑是:在GPS_TAG阶段同时完成异或累加,在GPS_BODY阶段把数据体拷进缓冲区,读到*后进入校验和阶段,收满两位十六进制字符后做一次比对,一致才交给上层解析。calc_xor是边收边算的,不是等收完再遍历,这保证了中断回调的时间复杂度是 O(1),不会被长帧拖住。hex_val是一个把 ASCII 字符0-9A-F转成数值的小函数,注意 GPS 模块输出的校验和是大写十六进制,解析时要兼容小写。
注意:NMEA 帧结尾是
\r\n,状态机不需要显式处理这两个字符,读到*后两个校验和字符收完就直接复位。如果模块配置了其它语句输出,比如$GPGGA,这个状态机同样能正确切分,只需要在gps_dispatch_frame里按tag区分语句类型即可。
3.3 校验和计算:算不对就丢掉,不要用错误数据
NMEA 的校验和算法非常简单:把$和*之间所有字符的 ASCII 码做按位异或,结果用大写十六进制表示。上面状态机里已经实时算了calc_xor,但这里单独再讲一遍算法,是因为很多例程会在收完帧后用循环重算,在波特率 9600 下其实感觉不到性能差异,一旦波特率提到 115200,每帧重算一次循环就白白浪费几十微秒。
/* 标准 NMEA 校验和:对 $ 到 * 之间所有字节做异或 */ static uint8_t nmea_checksum(const uint8_t *buf, uint16_t len) { uint8_t cs = 0; for (uint16_t i = 0; i < len; i++) { cs ^= buf[i]; } return cs; }使用方式是把 GPRMC 帧体(不含$,不含*xx)传入,得到的结果和帧尾两位十六进制数做比较。参数说明:buf指向帧体起始位置,len是帧体长度,函数返回一个 0x00 到 0xFF 的校验字节。这里有个常见误区,校验和只覆盖 ASCII 字符本身,不做任何移位或求补操作,和 Modbus CRC 完全是两回事,不要套用 CRC16 的算法。实践中即使校验和不一致,也不建议直接丢弃整帧,可以加上一帧错误计数,连续出错才触发重新初始化串口,这样能避免偶发干扰导致 GPS 数据长时间中断。
3.4 定位状态 A 和 V 的差别,以及 GGA 什么时候更合适
GPRMC 字段 2 的A表示当前定位有效,V表示接收机已经输出数据但定位不可用。刚上电的 GPS 模块在搜到足够卫星之前,会持续输出大量V状态帧,这时经纬度字段可能是上一次定位的缓存值,也可能是零填充,直接使用会酿成大错。STM32F103 端必须在解析后检查状态位,只有A状态的数据才允许写入定位结构体。
/* 只接受有效定位,V 状态直接跳过 */ if (body[2] == 'A') { gps_data.valid = 1; gps_data.latitude = parse_latitude(body + 3); gps_data.longitude = parse_longitude(body + 5); } else { gps_data.valid = 0; }$GPGGA则多提供海拔高度、卫星数和 HDOP 水平精度因子,如果项目需要显示海拔或评估定位质量,建议同时解析 GGA。两者在 STM32F103 例程里可以共用同一个状态机,tag为GPRMC时更新位置和速度,tag为GPGGA时更新高度和卫星数,互补使用。对于飞行器或坡度较大的车载场景,只用 RMC 会拿不到高度信息,这时候 GGA 的字段 9(椭球高)就派上用场了。
4. 度分转换、UTC 时区换算与 C++ 封装落地
4.1 度分格式转十进制度:差一位小数点就偏出去一公里多
NMEA 输出的纬度是ddmm.mmmm,经度是dddmm.mmmm,这是六十进制的度分格式,而地图 API 和高德、百度坐标系需要的是十进制度。转换公式是十进制度 = 度 + 分 / 60,以纬度3103.4567为例,31 是度,03.4567是分,转换结果是31 + 3.4567 / 60 = 31.0576117。注意这里的分是带小数的,不能先取整再除,否则会引入几百米的误差。
在 STM32F103 上用 C 实现这个转换,最简单的写法是直接对浮点数做整数和小数拆分:
/* 把 NMEA 度分浮点数转换成十进制度 */ float nmea_to_decimal(float ddmm_mmmm) { int deg = (int)(ddmm_mmmm / 100.0f); /* 取度:3103.4567 / 100 = 31 */ float min = ddmm_mmmm - deg * 100.0f; /* 取分:3103.4567 - 3100 */ return deg + min / 60.0f; }参数说明:ddmm_mmmm是从 NMEA 帧里用atof转出来的浮点数,函数先把整数部分除以 100 取出度,再用原值减掉整百度得到分,最后做一次除法。南纬和西经需要在返回后取负数,代码里一般留到上层处理,因为 NMEA 帧里N/S/E/W是独立字段,转换函数本身不应该关心象限。浮点精度方面,STM32F103 的硬件 FPU 是单精度,十进制度保留 6 位小数已经对应约 0.1 米的精度,例程够用,但如果后续要做高精度定位,建议改成 double 或直接以整数形式输出。
4.2 UTC 时间到北京时间:跨日处理是例程里最容易漏的
GPS 模块输出的时间和日期都是 UTC,和北京时间差 8 个小时。STM32F103 例程里最常见的做法是在解析出时、分、秒后各加 8 小时,但这样会算错跨日场景,比如 UTC 时间23:30:00加 8 小时后应该是第二天早上07:30:00,直接把小时加到 31 就显然不对了。
| UTC 时间 | 北京时间 | 日期变化 |
|---|---|---|
| 00:30:00 | 08:30:00 | 同一天 |
| 16:00:00 | 次日 00:00:00 | 日期加 1 |
| 23:30:00 | 次日 07:30:00 | 日期加 1 |
正确处理顺序是先转换时间,再判断日期是否进位。我一般建议先做小时加 8,如果结果大于等于 24 就减 24 并让日期加 1,但日期加 1 之后还要处理月末和年末,这就复杂了。更稳妥的做法是引入一个极简的日期补算函数,或者把日期转成 UNIX 时间戳加 8 小时后再转回年月日。前者代码量小,后者逻辑最不容易出错,但需要额外的日期运算库。例程阶段推荐第一种,配合一个days_in_month查表处理月份天数即可,注意 2 月要考虑闰年。
typedef struct { uint16_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; } gps_datetime_t; /* UTC 转北京时间,日期如果在加 8 小时后跨日,自动进位 */ void utc_to_beijing(gps_datetime_t *dt) { dt->hour += 8; if (dt->hour >= 24) { dt->hour -= 24; dt->day += 1; if (dt->day > days_in_month(dt->year, dt->month)) { dt->day = 1; dt->month += 1; if (dt->month > 12) { dt->month = 1; dt->year += 1; } } } }days_in_month函数建议用静态查表实现,20 年内闰年规则固定,不需要写完整的万年历算法。这段代码的边界情况是月末跨日和年末跨日,逻辑上只要先处理小时进位,再处理天数溢出,顺序不能反,否则月末判断会用到错误的月份值。时区换算做完后,时间字段就可以直接用于显示和日志记录,但注意 GPS 模块本身也有本地时间设置能力,通过 NMEA 命令可以改时区,那样例程里就不需要再做换算,代价是模块配置掉电丢失,所以多数例程选择在单片机端换算。
4.3 用 C++ 把解析器封装成组件,移植到其它 MCU 也更方便
标题里提到 C/C++,实际项目中我倾向于用 C 写底层解析,用 C++ 做一层薄封装,这样 STM32F103 上可以用 C++ 编译,也可以脱离 HAL 库移植到其它平台。C++ 封装的核心是GpsParser类,内部维护状态机、解析结果和时间换算逻辑,对外只暴露push()和一组只读的 getter。
class GpsParser { public: GpsParser() : valid_(false), lat_(0.0f), lon_(0.0f) {} /* 串口中断里直接调用,逐字节喂入 */ void push(uint8_t byte) { gps_byte_rx(byte); } /* 坐标是否有效,对应 GPRMC 的 A/V 状态 */ bool hasFix() const { return valid_; } float latitude() const { return lat_; } float longitude() const { return lon_; } float speed() const { return speed_; } /* 单位:km/h */ float course() const { return course_; } /* 单位:度 */ private: bool valid_; float lat_; float lon_; float speed_; float course_; };封装的关键在两点:一是把状态机相关变量从全局静态改成成员变量,这样同一个解析器可以同时处理多个串口或模块,实例化两个GpsParser对象分别对应两个 GPS 模块,互不干扰;二是把内部 buffer 的大小做成模板参数,比如template <size_t BODY_MAX>,内存占用可控,在 STM32F103 这种资源紧张的 MCU 上尤其重要。C++ 封装后,上层的定位逻辑只需要判断hasFix()再取值,代码可读性明显提升,也方便写单元测试——你可以在 PC 上直接用真实 NMEA 日志喂给GpsParser跑测试,不用烧录一次看一次串口。
编译时要注意 STM32F103 的 C++ 运行时默认不启用new/delete,封装类最好完全避开动态内存分配,所有 buffer 都在对象内部静态分配。使用arm-none-eabi-g++时,需要在 Makefile 里把启动文件换成带 C++ 初始化支持的版本,同时启用-fno-exceptions -fno-rtti来减小代码体积,否则默认异常和 RTTI 支持会吃掉不少 Flash。
4.4 把坐标送给显示设备、树莓派或 K210 前的最后一道工序
解析出来的十进制度数不能直接拿去画地图,还有两类问题要处理:坐标系和输出格式。GPS 模块输出的是 WGS-84 坐标系,高德地图用的是 GCJ-02,百度地图用 BD-09,如果直接拿 WGS-84 坐标在高德地图上打点,会出现几百米的偏移。STM32F103 的资源不适合跑完整坐标转换算法,常见做法是只在单片机端做简单的 WGS-84 转 GCJ-02,网上有成熟公式,大约几十行代码,但对北纬 55 度以上的高纬度地区精度会下降,工程上通常判断若项目只在中国大陆使用就做转换,否则保持原生坐标上传服务器。
输出方式上,最直观的是通过 USART1 以格式化字符串发送:
/* 格式化输出坐标和时间,方便在串口助手里观察 */ char line[128]; snprintf(line, sizeof(line), "%.6f,%.6f,%.1f,%.1f,%02d:%02d:%02d\r\n", gps_data.latitude, gps_data.longitude, gps_data.speed, gps_data.course, gps_data.hour, gps_data.minute, gps_data.second); HAL_UART_Transmit(&huart1, (uint8_t*)line, strlen(line), 100);这套输出的核心是 5 维数据:纬度、经度、速度、航向和时间,树莓派 3B+ 这类 Linux 板卡只要用串口或 USB 转接读取这个格式即可,K210 这类视觉模组也可以通过 UART 拿到坐标后做地理围栏逻辑。车载导航屏和导航终端通常直接消费 NMEA 原句,串口配置工具能改的往往只是端口、波特率和语句过滤规则,所以保留原始 NMEA 输出通道和格式化输出通道并存是比较稳妥的架构。
5. 误差修正、GPS 翻转补丁与静态验证技巧
5.1 GPS 误差不是白噪音,多径和 PDOP 才是精度大头
GPS 定位误差主要来自卫星钟差、电离层延迟、对流层延迟、多径效应和接收机噪声,其中多径是城市峡谷场景里最让人头疼的。卫星信号打到玻璃幕墙上反射后再进入天线,和直射信号叠加,伪距测量就会出现几米到几十米的偏差。STM32F103 端能做的主要是软件滤波,不要指望单片机级代码能消除多径,但可以通过两个手段降低影响:
| 误差来源 | 典型量级 | STM32F103 端处理方式 |
|---|---|---|
| 多径效应 | 5-30 米 | 丢弃低仰角卫星,或对坐标做滑动平均 |
| 电离层延迟 | 2-10 米 | 依赖模块内部算法,例程无法干预 |
| PDOP 过大 | 3 倍以上几何误差 | 读取 GGA 的 PDOP 字段,超过阈值标记为低质量 |
| 接收机钟差 | 纳秒级 | 模块已处理,输出时间即校正后 |
项目里可以每秒输出一次 GGA 中的 PDOP 值,PDOP 大于 6 时直接视为定位不可信。对于静态放置的定位设备,坐标会在真实位置附近无规则漂移,直径约 3 到 5 米,这时候做滑动平均能有效平滑,但如果设备在移动,滑动平均反而会引入滞后,所以滤波窗口必须根据运动状态动态切换——有速度时缩短窗口,静止时拉长窗口。
5.2 GPS 翻转补丁:严格说不是 bug,是周计数溢出
GPS 系统使用 10 bit 存储周计数,每 1024 周翻转一次,最近一次翻转发生在 2019 年 4 月 6 日。翻转本身是系统设计如此,但如果接收机固件没有做补丁处理,周计数归零后,解算出的日期会倒退 20 年,位置可能偏移数百公里。STM32F103 例程里如果直接使用模块输出的年月日字段,这个问题不会暴露,因为模块内部已经做了补丁;但如果通过 UBX 协议读取 raw 的周计数,或者使用老旧固件的模块,就需要自己在应用层判断。
/* 简单翻转判断:当前周数远小于上一帧时,补一个 1024 周期 */ if (week < last_week && last_week - week > 100) { week += 1024; }这段代码用在读取原始周数的场景,逻辑是如果新周数和上一帧相比突然减小超过 100,就判定发生翻转并自动加上 1024。参数阈值 100 是为了避免正常的数据抖动误判,GPS 周数是单调递增的,一帧内不可能倒退 100 周。补丁处理完后,推荐同步做一次日期校验,生成年月日之后再和系统 RTC 对比,如果差出十几二十年,说明补丁没有生效,此时应直接丢弃坐标并输出告警,而不是把错误日期写入日志。
5.3 静态验证:手机地图对坐标,比看串口日志直观得多
最后一招是验证整个 STM32F103 定位设计的正确性。拿着设备到户外开阔地固定不动,等 GPS 状态变为 A 后,用手机地图在同一位置打点,对比设备输出的坐标和手机显示坐标。两者画在同一地图上差 3 到 5 米是正常水平,如果是 WGS-84 直接叠在 GCJ-02 地图上,差几百米则说明坐标转换那层没做,和模块性能无关。
验证时还要记录冷启动时间:从上电到第一条 A 状态数据的耗时,正常开阔地应该在 30 到 90 秒之间,超过 3 分钟就要回头查天线馈电和电源纹波。用两张时间戳日志叠加对比,能看出模块是否有周期性丢帧,如果每 60 秒固定丢 5 秒数据,多半是板上有射频干扰源,比如排针上的 SPI 时钟线在特定频率上压制了 GPS 频段——这种问题换模块是没用的,改布局或加屏蔽才是正解。
本文还有配套的精品资源,点击获取