摘要
本文使用 STM32F103C8T6 单片机搭配 VK2828U7G5 卫星定位模块以及 I2C‑OLED 屏幕,实现卫星定位数据读取、NMEA 协议解析,最终把定位状态、经纬度、卫星数量、北京时间显示在 OLED 屏幕上。 VK2828U7G5 模块完成卫星信号接收与定位解算,通过串口输出标准 NMEA‑0183 文本;STM32 利用串口中断接收数据,做校验、解析、单位换算,输出到 OLED。本文包含硬件接线、定位原理、协议解析、故障排查、实物演示以及源码存放章节,适合单片机课程学习、定位模块入门调试。
硬件环境:STM32F103C8T6 最小系统板、VK2828U7G5 定位模块、I2C 接口 OLED 显示屏、配套 GPS 天线
一、整体工作流程
整个工程数据流向:
- GPS / 北斗等导航卫星向外广播卫星轨道、精确时间信息
- VK2828U7G5 天线接收卫星射频信号
- 模块内部计算伪距,完成位置、速度、时间解算(PVT)
- 模块 TX 引脚串口输出 NMEA 语句,波特率 9600 8N1
- STM32 的 USART3(PB11)使用中断方式接收串口数据
- 对 NMEA 语句做校验,解析 RMC、GGA、GSV 有效语句
- 完成经纬度、时间、速度单位换算
- OLED 屏幕刷新展示定位状态、经纬度、卫星数、北京时间

通俗理解:VK2828U7G5 负责算位置,输出文本;STM32 只负责读文本、解析、显示。
二、硬件电路接线
⚠️重点提醒:所有模块 GND 必须共地,否则会出现乱码、收不到数据等诡异问题。
| VK2828U7G5 模块引脚 | STM32F103C8T6 引脚 | 功能说明 |
|---|---|---|
| VCC | 5V | 定位模块供电 |
| TX | PB11 | 模块串口输出,连接 USART3_RX |
| GND | GND | 公共地 |
| OLED 模块引脚 | STM32F103C8T6 引脚 | 功能说明 |
|---|---|---|
| SCL | PB8 | 软件 I2C 时钟 |
| SDA | PB9 | 软件 I2C 数据 |
| VCC | 5V | OLED 供电 |
| GND | GND | OLED 电源地 |

为什么只接 VK2828U7G5 的 TX 引脚
串口引脚命名是以发送端视角定义: VK2828U7G5 的 TX → STM32 PB11(USART3_RX) VK2828U7G5 的 GND ↔ STM32 GND
本项目只读取模块主动上报的 NMEA 数据,不需要向模块下发配置命令,因此不需要连接 STM32 的 PB10(USART3_TX)。 如果后续需要修改模块波特率、关闭某些 NMEA 输出语句,才需要额外连接 STM32_TX 到模块 RX 引脚。
串口电平注意事项
VK2828U7G5 的 TX 输出是普通 TTL 串口电平,不是 RS232 正负电平,不能直接接 RS232 设备。更换其他 GPS 模块时,务必确认模块输出电平匹配 STM32 IO 输入电压。
三、VK2828U7G5 定位基础原理
导航卫星持续广播自身轨道参数与高精度原子钟时间。 定位模块天线收到卫星信号,对比信号发射时刻和接收时刻,可以算出信号在空中传播时间,乘以光速得到模块到卫星的距离。
卫星到接收机距离 ≈ 信号传播时间 × 光速
接收机时钟和卫星原子钟存在时间偏差,计算出来的距离叫伪距 。 需要求解 4 个未知数:接收机 X/Y/Z 三维坐标 + 接收机时钟偏差。 至少需要稳定接收 4 颗卫星信号,才能完成三维定位解算。 接收到的卫星数量越多、卫星在天空分布越分散,定位结果越稳定。
重要误区:模块收到卫星时间 ≠ 定位成功。调试不能只看时间有没有更新,必须同时看定位有效标志、定位质量、卫星数量。
模块内部已经完成信号捕获、跟踪、数学解算。STM32 不需要参与复杂定位运算,只读取模块输出好的定位结果即可。
四、USART 串口与 NMEA 协议解析
4.1 USART3 串口配置
PB11 为 USART3_RX,串口参数固定:
- 波特率:9600bps
- 数据位:8
- 校验位:无
- 停止位:1
- 简写:9600 8N1
串口空闲电平为高电平;每一个字节由起始位、数据位、停止位组成。单片机和模块两边波特率、数据格式必须完全一致,否则收到全部是乱码。
4.2 NMEA 语句基础格式
NMEA‑0183 是 GPS 模块通用 ASCII 文本输出协议,单条语句格式:
cpp
$语句头,字段1,字段2,字段3,...*校验和\r\n
各字段含义:
| 字段 | 说明 |
|---|---|
| $ | NMEA 语句起始标记 |
| GN/GP/BD | 导航系统标识,GN 代表多系统组合导航 |
| RMC/GGA/GSV | 语句类型 |
| , | 数据字段分隔符 |
| *XX | 两位十六进制校验和 |
| \r\n | 回车换行,代表一条语句结束 |
示例:$GNRMC代表组合导航输出 RMC 语句;程序会识别语句末尾三位字符,兼容 GP、BD 等不同前缀。
4.3 NMEA 校验和机制
校验和采用逐字节异或运算:计算范围从$后面第一个字符开始,到*前一个字符结束。$、*以及后面两位校验字符不参与运算。 程序计算得到的校验值和报文内*后面的 XX 比对,校验通过才解析这条数据,避免串口干扰、丢字节带来错误定位数据。
4.4 本工程用到的三类 NMEA 语句
| 语句 | 获取数据 |
|---|---|
| RMC | 定位状态、UTC 时间、UTC 日期、经纬度、地面运动速度 |
| GGA | UTC 时间、经纬度、定位质量、参与定位卫星数量、海拔高度 |
| GSV | 可见卫星总数量 |
RMC 中状态 A = 定位有效,V = 定位无效;GGA 的定位质量 Q>0 代表定位有效。
GGA 定位质量 Q 值说明:
| Q 值 | 含义 |
|---|---|
| 0 | 无有效定位 |
| 1 | 单点定位(普通 GPS 定位,演示最常见) |
| 2 | 差分定位 |
| 4 | RTK 固定解 |
| 5 | RTK 浮点解 |
4.5 关键数据换算
- 经纬度(度分格式转十进制度) NMEA 输出格式:纬度 ddmm.mmmm;经度 dddmm.mmmm 换算公式:
十进制度 = 度 + 分 ÷ 60
举例:纬度3112.3456,N 度 = 31,分 = 12.3456 十进制度 = 31 + 12.3456 / 60 = 31.205760°N
STM32F103 为减少浮点运算开销,程序采用定点数存储,把十进制度乘以 1000000 保存整数;北纬 N、东经 E 存正数;南纬 S、西经 W 存负数。
- 时间转换 NMEA 输出 UTC 世界时,程序给 UTC 时间 + 8 小时换算北京时间;同时处理跨午夜、跨月、跨年、闰年的日期进位。
- 速度、海拔换算 RMC 速度单位为节,换算:
1节 = 1.852 km/h; GGA 海拔单位米,程序放大 100 倍,以厘米定点数存储。
五、串口中断 + 环形缓冲区处理机制
GPS 模块会不间断持续输出 NMEA 报文,OLED 屏幕刷新需要消耗时间。如果采用轮询读取串口,OLED 刷新期间到来的串口字节会直接丢失。
解决方案:USART3 接收中断 + 512 字节环形缓冲区 工作逻辑:
- 串口每收到 1 字节,立刻触发 USART3 中断服务函数
- 在中断函数内部,只把收到的字节写入环形缓冲区,立刻退出中断,不做复杂运算
- 主循环中调用 GPS 处理函数,从缓冲区读取字节,拼接完整 NMEA 语句
- 检测到
$开头、\r\n结尾的完整报文,执行校验和校验 - 校验成功,判断是否 RMC/GGA/GSV,解析更新 GPS 数据结构体;其他类型语句直接丢弃
- 更新完成后,主循环刷新 OLED 屏幕
关键点:中断服务函数只做字节缓存,禁止在中断内做字符串分割、坐标换算、OLED 刷屏,保证中断快速退出,防止串口数据溢出丢失。
环形缓冲区依靠写指针、读指针实现循环读写;缓冲区满时会记录溢出计数,用来判断主循环处理速度是否跟不上模块输出速度。
程序关键源文件说明:
| 文件 | 函数 | 功能 |
|---|---|---|
| GPS.c | GPS_Init() | USART3、PB11、串口接收中断初始化 |
| GPS.c | USART3_IRQHandler() | 串口中断,读取收到字节 |
| GPS.c | GPS_ReceiveByte() | 字节写入环形缓冲区 |
| GPS.c | GPS_Process() | NMEA 组帧、校验 |
| GPS.c | GPS_ParseSentence() | 解析 RMC/GGA/GSV 报文 |
| GPS.c | GPS_GetData() | 获取 GPS 数据快照 |
| main.c | Demo_ShowGPS() | 格式化定位数据输出 OLED |
六、OLED 屏幕显示效果说明
定位成功时 OLED 显示示例:
cpp
GPS: FIX OK
LAT:N 31.205760
LON:E 121.123456
SAT:08 VIEW:15 Q:1
2026-09-02 15:20:30
各个状态含义:
| OLED 显示文本 | 状态说明 |
|---|---|
| GPS: NO DATA | PB11 完全没有收到串口字节 |
| GPS: NMEA ERROR | 收到字节,但报文校验失败,无法解析 |
| GPS: SEARCHING | NMEA 通信正常,模块还没有完成定位 |
| GPS: FIX OK | 已经得到有效定位解算 |
| LAT | 纬度,附带南北半球标记 |
| LON | 经度,附带东西半球标记 |
| SAT | 当前参与定位的卫星数量 |
| VIEW | 搜索到的可见卫星总数 |
| Q | GGA 定位质量编号 |
| 日期时间 | UTC 转换后的北京时间 |
没有定位成功的时候,经纬度位置显示
--.------,区分 "串口收到数据" 和 "定位结果有效" 两个不同状态。
七、源代码存放章节
本章节用于存放工程源码。
main.c代码
cpp
/*
* STM32F103C8T6 + VK2828U7G5 teaching demonstration
*
* GPS TX -> PB11 (USART3_RX, 9600 8N1)
* OLED -> PB8 (SCL), PB9 (SDA)
*/
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "GPS.h"
#define DISPLAY_PERIOD_LOOPS 20u
#define MAIN_LOOP_DELAY_MS 10u
static void Demo_ShowStartup(void);
static void Demo_ShowGPS(const GPS_Data *gps);
static void Demo_FormatCoordinate(char *text, const char *name,
char direction, int32_t coordinate_1e6,
uint8_t degree_digits);
static void Demo_FormatStatus(char *text, const GPS_Data *gps);
static void Demo_FormatSatellites(char *text, const GPS_Data *gps);
static void Demo_FormatDateTime(char *text, const GPS_Data *gps);
static char *Demo_AppendText(char *output, const char *text);
static char *Demo_AppendFixedUInt(char *output, uint32_t value,
uint8_t digits);
int main(void)
{
GPS_Data gps;
uint8_t display_counter;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0u);
OLED_DisplayTurn(0u);
Demo_ShowStartup();
GPS_Init();
delay_ms(500u);
display_counter = DISPLAY_PERIOD_LOOPS;
while (1)
{
(void)GPS_Process();
if (display_counter >= DISPLAY_PERIOD_LOOPS)
{
display_counter = 0u;
GPS_GetData(&gps);
Demo_ShowGPS(&gps);
}
display_counter++;
delay_ms(MAIN_LOOP_DELAY_MS);
}
}
static void Demo_ShowStartup(void)
{
OLED_Clear();
OLED_ShowString(0u, 0u, (u8 *)"VK2828U7 GPS", 16u);
OLED_ShowString(0u, 20u, (u8 *)"USART3 RX: PB11", 12u);
OLED_ShowString(0u, 36u, (u8 *)"BAUD: 9600 8N1", 12u);
OLED_ShowString(0u, 52u, (u8 *)"WAITING NMEA...", 12u);
OLED_Refresh();
}
static void Demo_ShowGPS(const GPS_Data *gps)
{
char line[22];
OLED_Clear();
Demo_FormatStatus(line, gps);
OLED_ShowString(0u, 0u, (u8 *)line, 12u);
if ((gps->fix_valid != 0u) && (gps->position_valid != 0u))
{
Demo_FormatCoordinate(line, "LAT:", gps->latitude_ns,
gps->latitude_1e6, 2u);
OLED_ShowString(0u, 13u, (u8 *)line, 12u);
Demo_FormatCoordinate(line, "LON:", gps->longitude_ew,
gps->longitude_1e6, 3u);
OLED_ShowString(0u, 26u, (u8 *)line, 12u);
}
else
{
OLED_ShowString(0u, 13u, (u8 *)"LAT: --.------", 12u);
OLED_ShowString(0u, 26u, (u8 *)"LON: ---.------", 12u);
}
Demo_FormatSatellites(line, gps);
OLED_ShowString(0u, 39u, (u8 *)line, 12u);
Demo_FormatDateTime(line, gps);
OLED_ShowString(0u, 52u, (u8 *)line, 12u);
OLED_Refresh();
}
static void Demo_FormatCoordinate(char *text, const char *name,
char direction, int32_t coordinate_1e6,
uint8_t degree_digits)
{
char *output;
uint32_t absolute;
output = Demo_AppendText(text, name);
*output++ = direction;
*output++ = ' ';
absolute = (coordinate_1e6 < 0) ?
(uint32_t)(-coordinate_1e6) : (uint32_t)coordinate_1e6;
output = Demo_AppendFixedUInt(output, absolute / 1000000u,
degree_digits);
*output++ = '.';
output = Demo_AppendFixedUInt(output, absolute % 1000000u, 6u);
*output = '\0';
}
static void Demo_FormatStatus(char *text, const GPS_Data *gps)
{
char *output;
output = Demo_AppendText(text, "GPS: ");
if (gps->rx_bytes == 0u)
{
output = Demo_AppendText(output, "NO DATA");
}
else if (gps->valid_sentences == 0u)
{
output = Demo_AppendText(output, "NMEA ERROR");
}
else if (gps->fix_valid != 0u)
{
output = Demo_AppendText(output, "FIX OK");
}
else
{
output = Demo_AppendText(output, "SEARCHING");
}
*output = '\0';
}
static void Demo_FormatSatellites(char *text, const GPS_Data *gps)
{
char *output;
output = Demo_AppendText(text, "SAT:");
output = Demo_AppendFixedUInt(output, gps->satellites_used, 2u);
output = Demo_AppendText(output, " VIEW:");
output = Demo_AppendFixedUInt(output, gps->satellites_visible, 2u);
output = Demo_AppendText(output, " Q:");
output = Demo_AppendFixedUInt(output, gps->fix_quality, 1u);
*output = '\0';
}
static void Demo_FormatDateTime(char *text, const GPS_Data *gps)
{
char *output;
if ((gps->date_valid == 0u) || (gps->time_valid == 0u))
{
output = Demo_AppendText(text, "TIME: --:--:--");
*output = '\0';
return;
}
output = Demo_AppendFixedUInt(text, gps->datetime.year, 4u);
*output++ = '-';
output = Demo_AppendFixedUInt(output, gps->datetime.month, 2u);
*output++ = '-';
output = Demo_AppendFixedUInt(output, gps->datetime.day, 2u);
*output++ = ' ';
output = Demo_AppendFixedUInt(output, gps->datetime.hour, 2u);
*output++ = ':';
output = Demo_AppendFixedUInt(output, gps->datetime.minute, 2u);
*output++ = ':';
output = Demo_AppendFixedUInt(output, gps->datetime.second, 2u);
*output = '\0';
}
static char *Demo_AppendText(char *output, const char *text)
{
while (*text != '\0')
{
*output++ = *text++;
}
return output;
}
static char *Demo_AppendFixedUInt(char *output, uint32_t value,
uint8_t digits)
{
uint32_t divisor;
divisor = 1u;
while (digits > 1u)
{
divisor *= 10u;
digits--;
}
do
{
*output++ = (char)('0' + (value / divisor) % 10u);
divisor /= 10u;
}
while (divisor != 0u);
return output;
}
GPS.c代码
cpp
#include "GPS.h"
#include "stm32f10x.h"
#include <string.h>
/*
* 文件名称:GPS.c
* 功能说明:完成 VK2828U7G5 的 USART3 接收、NMEA 成帧、校验和数据解析。
* 设计要点:中断函数只保存字节,字符串处理放在主循环中完成,从而缩短中断时间。
*/
/* 环形缓冲区通过按位与运算回绕,因此缓冲区长度必须是 2 的整数次幂。 */
#define GPS_RX_BUFFER_MASK (GPS_RX_BUFFER_SIZE - 1u)
#if ((GPS_RX_BUFFER_SIZE == 0u) || \
((GPS_RX_BUFFER_SIZE & GPS_RX_BUFFER_MASK) != 0u))
#error "GPS_RX_BUFFER_SIZE must be a power of two"
#endif
/* USART3 中断写入、主循环读出的单生产者/单消费者环形缓冲区。 */
static volatile uint8_t gps_rx_buffer[GPS_RX_BUFFER_SIZE];
static volatile uint16_t gps_rx_head;
static volatile uint16_t gps_rx_tail;
static volatile uint32_t gps_rx_bytes;
static volatile uint32_t gps_rx_overflows;
/* 主循环使用的 NMEA 语句缓冲区和最近一次有效定位数据。 */
static char gps_line[GPS_NMEA_MAX_LENGTH];
static uint16_t gps_line_length;
static uint8_t gps_collecting;
static GPS_Data gps_latest;
static uint8_t GPS_PopByte(uint8_t *byte);
static uint8_t GPS_ChecksumValid(const char *sentence);
static uint8_t GPS_IsSentence(const char *sentence, const char *type);
static uint8_t GPS_GetField(const char *sentence, uint8_t index,
const char **field, uint16_t *length);
static uint8_t GPS_ParseRMC(const char *sentence, GPS_Data *data);
static uint8_t GPS_ParseGGA(const char *sentence, GPS_Data *data);
static uint8_t GPS_ParseGSV(const char *sentence, GPS_Data *data);
/* 将 hhmmss.sss 格式的 UTC 时间字段转换为时、分、秒。 */
static uint8_t GPS_ParseTime(const char *field, uint16_t length,
GPS_DateTime *datetime);
/*
* 将 ddmmyy 格式的 UTC 日期转换为年、月、日。
* 80~99 年按 1980~1999 处理,00~79 年按 2000~2079 处理。
*/
static uint8_t GPS_ParseDate(const char *field, uint16_t length,
GPS_DateTime *datetime);
/*
* 将 NMEA 坐标转换为十进制度定点数。
* 纬度格式为 ddmm.mmmm,经度格式为 dddmm.mmmm,输出单位为 0.000001 度。
*/
static uint8_t GPS_ParseCoordinate(const char *field, uint16_t length,
uint8_t degree_digits, char direction,
int32_t *coordinate_1e6);
static uint8_t GPS_ParseUInt8(const char *field, uint16_t length,
uint8_t *value);
static int32_t GPS_ParseScaled(const char *field, uint16_t length,
uint32_t scale, uint8_t allow_sign,
uint8_t *valid);
static int8_t GPS_HexValue(char value);
static uint8_t GPS_IsDigit(char value);
static void GPS_ApplyTimeZone(GPS_DateTime *datetime, uint8_t date_valid);
static void GPS_IncrementDate(GPS_DateTime *datetime);
static void GPS_DecrementDate(GPS_DateTime *datetime);
static uint8_t GPS_DaysInMonth(uint16_t year, uint8_t month);
/* 使用模块默认的 9600 波特率完成初始化。 */
void GPS_Init(void)
{
GPS_InitWithBaudRate(GPS_DEFAULT_BAUDRATE);
}
/*
* 按指定波特率初始化 GPS 接收硬件。
* PB11 使用 USART3 默认映射并配置为浮空输入,串口只开启接收功能。
*/
void GPS_InitWithBaudRate(uint32_t baudrate)
{
GPIO_InitTypeDef gpio;
USART_InitTypeDef usart;
NVIC_InitTypeDef nvic;
if (baudrate == 0u)
{
baudrate = GPS_DEFAULT_BAUDRATE;
}
GPS_Reset();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO,
ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
/* 选择 USART3 默认映射:PB10 为 TX,PB11 为 RX。 */
GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, DISABLE);
GPIO_PinRemapConfig(GPIO_FullRemap_USART3, DISABLE);
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = GPIO_Pin_11;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &gpio);
USART_DeInit(USART3);
USART_StructInit(&usart);
usart.USART_BaudRate = baudrate;
usart.USART_WordLength = USART_WordLength_8b;
usart.USART_StopBits = USART_StopBits_1;
usart.USART_Parity = USART_Parity_No;
usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart.USART_Mode = USART_Mode_Rx;
USART_Init(USART3, &usart);
nvic.NVIC_IRQChannel = USART3_IRQn;
nvic.NVIC_IRQChannelPreemptionPriority = 2u;
nvic.NVIC_IRQChannelSubPriority = 2u;
nvic.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic);
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
USART_Cmd(USART3, ENABLE);
}
/* 清空环形缓冲区、NMEA 成帧状态、解析结果及所有统计计数。 */
void GPS_Reset(void)
{
USART_ITConfig(USART3, USART_IT_RXNE, DISABLE);
gps_rx_head = 0u;
gps_rx_tail = 0u;
gps_rx_bytes = 0u;
gps_rx_overflows = 0u;
gps_line_length = 0u;
gps_collecting = 0u;
memset((void *)gps_rx_buffer, 0, sizeof(gps_rx_buffer));
memset(gps_line, 0, sizeof(gps_line));
memset(&gps_latest, 0, sizeof(gps_latest));
gps_latest.latitude_ns = '-';
gps_latest.longitude_ew = '-';
/* GPS_Reset 也允许在串口初始化完成后单独调用,此时需要恢复接收中断。 */
if ((RCC->APB1ENR & RCC_APB1Periph_USART3) != 0u)
{
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
}
}
/*
* 将中断收到的一个字节放入环形缓冲区。
* 缓冲区已满时保留原有数据、丢弃新字节,并累计溢出次数。
*/
void GPS_ReceiveByte(uint8_t byte)
{
uint16_t next;
gps_rx_bytes++;
next = (uint16_t)((gps_rx_head + 1u) & GPS_RX_BUFFER_MASK);
if (next == gps_rx_tail)
{
gps_rx_overflows++;
return;
}
gps_rx_buffer[gps_rx_head] = byte;
gps_rx_head = next;
}
/* USART3 接收中断:读取 PB11 收到的字节并交给环形缓冲区。 */
void USART3_IRQHandler(void)
{
if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
GPS_ReceiveByte((uint8_t)(USART_ReceiveData(USART3) & 0x00FFu));
}
else if (USART_GetFlagStatus(USART3, USART_FLAG_ORE) != RESET)
{
/* 先读状态寄存器再读数据寄存器,可以清除串口溢出错误。 */
(void)USART_ReceiveData(USART3);
}
}
/*
* 处理环形缓冲区中等待解析的全部字节。
* NMEA 语句以 '$' 开始、以换行符结束,回车符不写入语句缓冲区。
*/
uint8_t GPS_Process(void)
{
uint8_t byte;
uint8_t status;
uint8_t parsed_count;
parsed_count = 0u;
while (GPS_PopByte(&byte) != 0u)
{
if (byte == (uint8_t)'$')
{
gps_collecting = 1u;
gps_line_length = 0u;
gps_line[gps_line_length++] = '$';
}
else if (gps_collecting != 0u)
{
if (byte == (uint8_t)'\r')
{
continue;
}
if (byte == (uint8_t)'\n')
{
gps_line[gps_line_length] = '\0';
gps_latest.nmea_sentences++;
status = GPS_ParseSentence(gps_line, &gps_latest);
if (status == GPS_PARSE_OK)
{
gps_latest.valid_sentences++;
if (parsed_count < 255u)
{
parsed_count++;
}
}
else if (status == GPS_PARSE_CHECKSUM_ERROR)
{
gps_latest.checksum_errors++;
}
gps_collecting = 0u;
gps_line_length = 0u;
}
else if (gps_line_length < (GPS_NMEA_MAX_LENGTH - 1u))
{
gps_line[gps_line_length++] = (char)byte;
}
else
{
/* 语句过长时丢弃当前内容,并等待下一个 '$' 重新同步。 */
gps_collecting = 0u;
gps_line_length = 0u;
}
}
}
return parsed_count;
}
/* 将最近一次解析结果和中断接收统计复制给调用者。 */
void GPS_GetData(GPS_Data *data)
{
if (data == 0)
{
return;
}
*data = gps_latest;
data->rx_bytes = gps_rx_bytes;
data->rx_overflows = gps_rx_overflows;
}
/*
* 校验并解析一条完整 NMEA 语句。
* 兼容 GP、GN、BD、GB 等不同导航系统的语句头。
*/
uint8_t GPS_ParseSentence(const char *sentence, GPS_Data *data)
{
if ((sentence == 0) || (data == 0) || (sentence[0] != '$'))
{
return GPS_PARSE_FORMAT_ERROR;
}
if (GPS_ChecksumValid(sentence) == 0u)
{
return GPS_PARSE_CHECKSUM_ERROR;
}
if (GPS_IsSentence(sentence, "RMC") != 0u)
{
return GPS_ParseRMC(sentence, data);
}
if (GPS_IsSentence(sentence, "GGA") != 0u)
{
return GPS_ParseGGA(sentence, data);
}
if (GPS_IsSentence(sentence, "GSV") != 0u)
{
return GPS_ParseGSV(sentence, data);
}
return GPS_PARSE_UNSUPPORTED;
}
/* 从环形缓冲区取出一个字节;缓冲区为空时返回 0。 */
static uint8_t GPS_PopByte(uint8_t *byte)
{
if ((byte == 0) || (gps_rx_tail == gps_rx_head))
{
return 0u;
}
*byte = gps_rx_buffer[gps_rx_tail];
gps_rx_tail = (uint16_t)((gps_rx_tail + 1u) & GPS_RX_BUFFER_MASK);
return 1u;
}
/*
* 验证 NMEA 异或校验和。
* '$' 与 '*' 之间的所有字符逐字节异或,结果应等于星号后的两位十六进制数。
*/
static uint8_t GPS_ChecksumValid(const char *sentence)
{
uint8_t checksum;
int8_t high;
int8_t low;
uint16_t index;
checksum = 0u;
index = 1u;
while ((sentence[index] != '\0') && (sentence[index] != '*'))
{
checksum ^= (uint8_t)sentence[index];
index++;
}
if ((sentence[index] != '*') || (sentence[index + 1u] == '\0') ||
(sentence[index + 2u] == '\0'))
{
return 0u;
}
high = GPS_HexValue(sentence[index + 1u]);
low = GPS_HexValue(sentence[index + 2u]);
if ((high < 0) || (low < 0))
{
return 0u;
}
return (checksum == (uint8_t)(((uint8_t)high << 4) | (uint8_t)low)) ?
1u : 0u;
}
/* 判断语句类型是否为指定的三字符类型,例如 RMC、GGA 或 GSV。 */
static uint8_t GPS_IsSentence(const char *sentence, const char *type)
{
const char *field;
uint16_t length;
if (GPS_GetField(sentence, 0u, &field, &length) == 0u)
{
return 0u;
}
if (length < 5u)
{
return 0u;
}
/* 从语句头末尾匹配类型,因此可兼容 GP、GN、BD、GB 等设备标识。 */
return ((field[length - 3u] == type[0]) &&
(field[length - 2u] == type[1]) &&
(field[length - 1u] == type[2])) ? 1u : 0u;
}
/*
* 根据逗号位置取得指定 NMEA 字段。
* index 从 0 开始,字段 0 是 GNRMC、GNGGA 等语句头。
*/
static uint8_t GPS_GetField(const char *sentence, uint8_t index,
const char **field, uint16_t *length)
{
uint8_t current;
uint16_t position;
uint16_t start;
if ((sentence == 0) || (field == 0) || (length == 0))
{
return 0u;
}
current = 0u;
position = (sentence[0] == '$') ? 1u : 0u;
start = position;
while ((sentence[position] != '\0') && (sentence[position] != '*'))
{
if (sentence[position] == ',')
{
if (current == index)
{
*field = &sentence[start];
*length = (uint16_t)(position - start);
return 1u;
}
current++;
start = (uint16_t)(position + 1u);
}
position++;
}
if (current == index)
{
*field = &sentence[start];
*length = (uint16_t)(position - start);
return 1u;
}
return 0u;
}
/*
* 解析 RMC 推荐最小定位信息:
* 字段 1 为 UTC 时间,2 为定位状态,3~6 为经纬度,
* 字段 7 为节速,字段 9 为 UTC 日期。
*/
static uint8_t GPS_ParseRMC(const char *sentence, GPS_Data *data)
{
const char *field;
uint16_t length;
GPS_DateTime datetime;
uint8_t time_valid;
uint8_t date_valid;
uint8_t number_valid;
uint8_t coordinates_valid;
char ns;
char ew;
int32_t latitude;
int32_t longitude;
int32_t knots_1e3;
datetime = data->datetime;
time_valid = 0u;
date_valid = 0u;
coordinates_valid = 0u;
/* RMC 的日期和时间同时有效时,再执行 UTC+8 及日期跨天处理。 */
if (GPS_GetField(sentence, 1u, &field, &length) != 0u)
{
time_valid = GPS_ParseTime(field, length, &datetime);
}
if (GPS_GetField(sentence, 9u, &field, &length) != 0u)
{
date_valid = GPS_ParseDate(field, length, &datetime);
}
if (time_valid != 0u)
{
GPS_ApplyTimeZone(&datetime, date_valid);
data->datetime = datetime;
data->time_valid = 1u;
if (date_valid != 0u)
{
data->date_valid = 1u;
}
}
/* 状态字符 A 表示定位数据有效,V 表示当前定位无效。 */
data->fix_valid = 0u;
if ((GPS_GetField(sentence, 2u, &field, &length) != 0u) &&
(length == 1u) && (field[0] == 'A'))
{
data->fix_valid = 1u;
}
ns = '-';
ew = '-';
latitude = 0;
longitude = 0;
if ((GPS_GetField(sentence, 4u, &field, &length) != 0u) &&
(length == 1u))
{
ns = field[0];
}
if ((GPS_GetField(sentence, 6u, &field, &length) != 0u) &&
(length == 1u))
{
ew = field[0];
}
if ((GPS_GetField(sentence, 3u, &field, &length) != 0u) &&
(GPS_ParseCoordinate(field, length, 2u, ns, &latitude) != 0u) &&
(GPS_GetField(sentence, 5u, &field, &length) != 0u) &&
(GPS_ParseCoordinate(field, length, 3u, ew, &longitude) != 0u))
{
coordinates_valid = 1u;
}
data->position_valid = coordinates_valid;
if (coordinates_valid != 0u)
{
data->latitude_ns = ns;
data->longitude_ew = ew;
data->latitude_1e6 = latitude;
data->longitude_1e6 = longitude;
}
/* RMC 速度单位为节:1 节 = 1.852 km/h。 */
data->speed_kmh_1e2 = 0u;
if (GPS_GetField(sentence, 7u, &field, &length) != 0u)
{
knots_1e3 = GPS_ParseScaled(field, length, 1000u, 0u,
&number_valid);
if ((number_valid != 0u) && (knots_1e3 >= 0))
{
data->speed_kmh_1e2 =
(uint32_t)(((uint32_t)knots_1e3 * 1852u + 5000u) / 10000u);
}
}
return GPS_PARSE_OK;
}
/*
* 解析 GGA 定位数据:
* 字段 1 为 UTC 时间,2~5 为经纬度,6 为定位质量,
* 字段 7 为参与定位的卫星数,字段 9 为海拔高度(米)。
*/
static uint8_t GPS_ParseGGA(const char *sentence, GPS_Data *data)
{
const char *field;
uint16_t length;
GPS_DateTime datetime;
uint8_t value;
uint8_t number_valid;
char ns;
char ew;
int32_t latitude;
int32_t longitude;
int32_t altitude;
datetime = data->datetime;
if ((GPS_GetField(sentence, 1u, &field, &length) != 0u) &&
(GPS_ParseTime(field, length, &datetime) != 0u))
{
GPS_ApplyTimeZone(&datetime, 0u);
data->datetime.hour = datetime.hour;
data->datetime.minute = datetime.minute;
data->datetime.second = datetime.second;
data->time_valid = 1u;
}
/* 定位质量大于 0 表示 GGA 已经给出有效定位。 */
value = 0u;
if (GPS_GetField(sentence, 6u, &field, &length) != 0u)
{
(void)GPS_ParseUInt8(field, length, &value);
}
data->fix_quality = value;
data->fix_valid = (value > 0u) ? 1u : 0u;
value = 0u;
if (GPS_GetField(sentence, 7u, &field, &length) != 0u)
{
(void)GPS_ParseUInt8(field, length, &value);
}
data->satellites_used = value;
ns = '-';
ew = '-';
latitude = 0;
longitude = 0;
if ((GPS_GetField(sentence, 3u, &field, &length) != 0u) &&
(length == 1u))
{
ns = field[0];
}
if ((GPS_GetField(sentence, 5u, &field, &length) != 0u) &&
(length == 1u))
{
ew = field[0];
}
if ((GPS_GetField(sentence, 2u, &field, &length) != 0u) &&
(GPS_ParseCoordinate(field, length, 2u, ns, &latitude) != 0u) &&
(GPS_GetField(sentence, 4u, &field, &length) != 0u) &&
(GPS_ParseCoordinate(field, length, 3u, ew, &longitude) != 0u))
{
data->position_valid = 1u;
data->latitude_ns = ns;
data->longitude_ew = ew;
data->latitude_1e6 = latitude;
data->longitude_1e6 = longitude;
}
else
{
data->position_valid = 0u;
}
/* 将以米为单位的海拔转换为厘米定点数保存。 */
if (GPS_GetField(sentence, 9u, &field, &length) != 0u)
{
altitude = GPS_ParseScaled(field, length, 100u, 1u, &number_valid);
if (number_valid != 0u)
{
data->altitude_cm = altitude;
}
}
return GPS_PARSE_OK;
}
/* 解析 GSV 字段 3,取得当前语句报告的可见卫星总数。 */
static uint8_t GPS_ParseGSV(const char *sentence, GPS_Data *data)
{
const char *field;
uint16_t length;
uint8_t value;
value = 0u;
if ((GPS_GetField(sentence, 3u, &field, &length) != 0u) &&
(GPS_ParseUInt8(field, length, &value) != 0u))
{
data->satellites_visible = value;
}
return GPS_PARSE_OK;
}
static uint8_t GPS_ParseTime(const char *field, uint16_t length,
GPS_DateTime *datetime)
{
uint8_t index;
if ((field == 0) || (datetime == 0) || (length < 6u))
{
return 0u;
}
for (index = 0u; index < 6u; index++)
{
if (GPS_IsDigit(field[index]) == 0u)
{
return 0u;
}
}
datetime->hour = (uint8_t)((field[0] - '0') * 10 + field[1] - '0');
datetime->minute = (uint8_t)((field[2] - '0') * 10 + field[3] - '0');
datetime->second = (uint8_t)((field[4] - '0') * 10 + field[5] - '0');
if ((datetime->hour > 23u) || (datetime->minute > 59u) ||
(datetime->second > 59u))
{
return 0u;
}
return 1u;
}
static uint8_t GPS_ParseDate(const char *field, uint16_t length,
GPS_DateTime *datetime)
{
uint8_t index;
uint8_t day;
uint8_t month;
uint8_t year;
uint16_t full_year;
if ((field == 0) || (datetime == 0) || (length < 6u))
{
return 0u;
}
for (index = 0u; index < 6u; index++)
{
if (GPS_IsDigit(field[index]) == 0u)
{
return 0u;
}
}
day = (uint8_t)((field[0] - '0') * 10 + field[1] - '0');
month = (uint8_t)((field[2] - '0') * 10 + field[3] - '0');
year = (uint8_t)((field[4] - '0') * 10 + field[5] - '0');
full_year = (year >= 80u) ? (uint16_t)(1900u + year) :
(uint16_t)(2000u + year);
if ((month < 1u) || (month > 12u) || (day < 1u))
{
return 0u;
}
if (day > GPS_DaysInMonth(full_year, month))
{
return 0u;
}
datetime->year = full_year;
datetime->month = month;
datetime->day = day;
return 1u;
}
static uint8_t GPS_ParseCoordinate(const char *field, uint16_t length,
uint8_t degree_digits, char direction,
int32_t *coordinate_1e6)
{
uint8_t valid;
uint16_t index;
int32_t degrees;
int32_t minutes_1e6;
int32_t coordinate;
if ((field == 0) || (coordinate_1e6 == 0) ||
(length <= degree_digits))
{
return 0u;
}
degrees = 0;
for (index = 0u; index < degree_digits; index++)
{
if (GPS_IsDigit(field[index]) == 0u)
{
return 0u;
}
degrees = degrees * 10 + (field[index] - '0');
}
minutes_1e6 = GPS_ParseScaled(&field[degree_digits],
(uint16_t)(length - degree_digits),
1000000u, 0u, &valid);
if ((valid == 0u) || (minutes_1e6 < 0) ||
(minutes_1e6 >= 60000000L))
{
return 0u;
}
/* 十进制度 = 度 + 分 / 60;加 30 用于整数除法四舍五入。 */
coordinate = degrees * 1000000L + (minutes_1e6 + 30L) / 60L;
if ((direction == 'S') || (direction == 'W'))
{
coordinate = -coordinate;
}
else if ((direction != 'N') && (direction != 'E'))
{
return 0u;
}
if (((degree_digits == 2u) && (coordinate > 90000000L)) ||
((degree_digits == 2u) && (coordinate < -90000000L)) ||
((degree_digits == 3u) && (coordinate > 180000000L)) ||
((degree_digits == 3u) && (coordinate < -180000000L)))
{
return 0u;
}
*coordinate_1e6 = coordinate;
return 1u;
}
/* 将纯数字字段转换为 0~255 的无符号整数。 */
static uint8_t GPS_ParseUInt8(const char *field, uint16_t length,
uint8_t *value)
{
uint16_t index;
uint16_t result;
if ((field == 0) || (value == 0) || (length == 0u))
{
return 0u;
}
result = 0u;
for (index = 0u; index < length; index++)
{
if (GPS_IsDigit(field[index]) == 0u)
{
return 0u;
}
result = (uint16_t)(result * 10u + (uint16_t)(field[index] - '0'));
if (result > 255u)
{
return 0u;
}
}
*value = (uint8_t)result;
return 1u;
}
/*
* 将十进制字符串转换为指定倍率的定点数。
* 例如 scale=100 时,字符串 "12.34" 转换为整数 1234。
*/
static int32_t GPS_ParseScaled(const char *field, uint16_t length,
uint32_t scale, uint8_t allow_sign,
uint8_t *valid)
{
uint16_t index;
uint8_t negative;
uint8_t decimal_seen;
uint8_t digit_seen;
uint32_t integer;
uint32_t fraction;
uint32_t place;
*valid = 0u;
if ((field == 0) || (length == 0u) || (scale == 0u))
{
return 0;
}
index = 0u;
negative = 0u;
if ((allow_sign != 0u) && ((field[0] == '-') || (field[0] == '+')))
{
negative = (field[0] == '-') ? 1u : 0u;
index++;
}
decimal_seen = 0u;
digit_seen = 0u;
integer = 0u;
fraction = 0u;
place = scale;
for (; index < length; index++)
{
if (field[index] == '.')
{
if (decimal_seen != 0u)
{
return 0;
}
decimal_seen = 1u;
}
else if (GPS_IsDigit(field[index]) != 0u)
{
digit_seen = 1u;
if (decimal_seen == 0u)
{
integer = integer * 10u + (uint32_t)(field[index] - '0');
}
else if (place > 1u)
{
place /= 10u;
fraction += (uint32_t)(field[index] - '0') * place;
}
}
else
{
return 0;
}
}
if (digit_seen == 0u)
{
return 0;
}
*valid = 1u;
if (negative != 0u)
{
return -(int32_t)(integer * scale + fraction);
}
return (int32_t)(integer * scale + fraction);
}
/* 将一个十六进制字符转换为 0~15,字符无效时返回 -1。 */
static int8_t GPS_HexValue(char value)
{
if ((value >= '0') && (value <= '9'))
{
return (int8_t)(value - '0');
}
if ((value >= 'A') && (value <= 'F'))
{
return (int8_t)(value - 'A' + 10);
}
if ((value >= 'a') && (value <= 'f'))
{
return (int8_t)(value - 'a' + 10);
}
return -1;
}
/* 判断字符是否为十进制数字。 */
static uint8_t GPS_IsDigit(char value)
{
return ((value >= '0') && (value <= '9')) ? 1u : 0u;
}
/* 将 UTC 时间转换为配置的本地时区,并在有日期时处理跨天。 */
static void GPS_ApplyTimeZone(GPS_DateTime *datetime, uint8_t date_valid)
{
int16_t hour;
hour = (int16_t)datetime->hour + (int16_t)GPS_TIMEZONE_HOURS;
while (hour >= 24)
{
hour -= 24;
if (date_valid != 0u)
{
GPS_IncrementDate(datetime);
}
}
while (hour < 0)
{
hour += 24;
if (date_valid != 0u)
{
GPS_DecrementDate(datetime);
}
}
datetime->hour = (uint8_t)hour;
}
/* 日期增加一天,同时正确处理月末、年末和闰年。 */
static void GPS_IncrementDate(GPS_DateTime *datetime)
{
uint8_t days;
if ((datetime->month < 1u) || (datetime->month > 12u))
{
return;
}
days = GPS_DaysInMonth(datetime->year, datetime->month);
datetime->day++;
if (datetime->day > days)
{
datetime->day = 1u;
datetime->month++;
if (datetime->month > 12u)
{
datetime->month = 1u;
datetime->year++;
}
}
}
/* 日期减少一天,同时正确处理月初、年初和闰年。 */
static void GPS_DecrementDate(GPS_DateTime *datetime)
{
if ((datetime->month < 1u) || (datetime->month > 12u))
{
return;
}
if (datetime->day > 1u)
{
datetime->day--;
return;
}
if (datetime->month > 1u)
{
datetime->month--;
}
else
{
datetime->month = 12u;
if (datetime->year > 0u)
{
datetime->year--;
}
}
datetime->day = GPS_DaysInMonth(datetime->year, datetime->month);
}
/* 返回指定月份的天数,二月会根据公历闰年规则计算。 */
static uint8_t GPS_DaysInMonth(uint16_t year, uint8_t month)
{
static const uint8_t days[12] =
{
31u, 28u, 31u, 30u, 31u, 30u,
31u, 31u, 30u, 31u, 30u, 31u
};
uint8_t result;
if ((month < 1u) || (month > 12u))
{
return 31u;
}
result = days[month - 1u];
if ((month == 2u) &&
(((year % 400u) == 0u) ||
(((year % 4u) == 0u) && ((year % 100u) != 0u))))
{
result = 29u;
}
return result;
}
GPS.h代码
cpp
#ifndef __GPS_H
#define __GPS_H
#include <stdint.h>
/*
* 文件名称:GPS.h
* 功能说明:STM32F103C8T6 的 VK2828U7G5 GPS/北斗模块驱动接口。
*
* 硬件连接:
* VK2828U7G5 TX -> PB11(USART3_RX)
* 串口通信参数 -> 9600 波特率、8 位数据位、无校验、1 位停止位
*
* 本例只接收模块输出的 NMEA 数据,因此不使用 PB10(USART3_TX)。
* 驱动支持解析 RMC、GGA 和 GSV 语句,时间统一转换为北京时间(UTC+8)。
*/
#define GPS_DEFAULT_BAUDRATE 9600u
#define GPS_TIMEZONE_HOURS 8
#define GPS_NMEA_MAX_LENGTH 96u
#define GPS_RX_BUFFER_SIZE 512u
#define GPS_PARSE_OK 0u
#define GPS_PARSE_UNSUPPORTED 1u
#define GPS_PARSE_FORMAT_ERROR 2u
#define GPS_PARSE_CHECKSUM_ERROR 3u
typedef struct
{
uint16_t year; /* 年,例如 2026。 */
uint8_t month; /* 月,范围为 1~12。 */
uint8_t day; /* 日,范围为 1~31。 */
uint8_t hour; /* 时,范围为 0~23。 */
uint8_t minute; /* 分,范围为 0~59。 */
uint8_t second; /* 秒,范围为 0~59。 */
} GPS_DateTime;
typedef struct
{
uint8_t time_valid; /* 时间字段有效标志:1 有效,0 无效。 */
uint8_t date_valid; /* 日期字段有效标志:1 有效,0 无效。 */
uint8_t fix_valid; /* 定位有效标志:1 已定位,0 未定位。 */
uint8_t position_valid; /* 经纬度字段有效标志。 */
uint8_t fix_quality; /* GGA 定位质量,0 表示未定位。 */
uint8_t satellites_used; /* 当前参与定位的卫星数量。 */
uint8_t satellites_visible; /* 当前 NMEA GSV 语句报告的可见卫星数。 */
char latitude_ns; /* 纬度半球:N 为北纬,S 为南纬。 */
char longitude_ew; /* 经度半球:E 为东经,W 为西经。 */
int32_t latitude_1e6; /* 纬度,单位为 0.000001 度。 */
int32_t longitude_1e6; /* 经度,单位为 0.000001 度。 */
int32_t altitude_cm; /* 海拔高度,单位为厘米。 */
uint32_t speed_kmh_1e2; /* 地面速度,单位为 0.01 km/h。 */
GPS_DateTime datetime; /* 北京时间(UTC+8)。 */
uint32_t rx_bytes; /* USART3 累计收到的字节数。 */
uint32_t nmea_sentences; /* 累计收到的完整 NMEA 语句数。 */
uint32_t valid_sentences; /* 累计成功解析的受支持语句数。 */
uint32_t checksum_errors; /* NMEA 校验失败次数。 */
uint32_t rx_overflows; /* 接收环形缓冲区溢出次数。 */
} GPS_Data;
/*
* 函数功能:按默认 9600 波特率初始化 PB11、USART3 和接收中断。
* 参数说明:无。
* 返回值:无。
*/
void GPS_Init(void);
/*
* 函数功能:按指定波特率初始化 PB11、USART3 和接收中断。
* 参数说明:baudrate 为串口波特率;传入 0 时使用默认 9600。
* 返回值:无。
*/
void GPS_InitWithBaudRate(uint32_t baudrate);
/*
* 函数功能:清空接收缓冲、解析结果和通信统计信息。
* 参数说明:无。
* 返回值:无。
*/
void GPS_Reset(void);
/*
* 函数功能:处理环形缓冲区中的字节,并解析完整的 NMEA 语句。
* 参数说明:无。
* 返回值:本次调用成功解析的 RMC、GGA、GSV 语句数量。
*/
uint8_t GPS_Process(void);
/*
* 函数功能:读取最近一次解析得到的 GPS 数据快照。
* 参数说明:data 为输出结构体指针,不能为 0。
* 返回值:无。
*/
void GPS_GetData(GPS_Data *data);
/*
* 函数功能:校验并解析一条以字符串形式提供的完整 NMEA 语句。
* 参数说明:sentence 为 NMEA 字符串;data 用于保存解析结果。
* 返回值:GPS_PARSE_OK 表示成功,其他值表示不支持或语句有误。
*/
uint8_t GPS_ParseSentence(const char *sentence, GPS_Data *data);
/*
* 函数功能:将一个串口接收字节写入驱动环形缓冲区。
* 参数说明:byte 为收到的原始字节,通常由串口中断函数传入。
* 返回值:无。
*/
void GPS_ReceiveByte(uint8_t byte);
/* USART3 接收中断服务函数,PB11 收到数据时由硬件自动调用。 */
void USART3_IRQHandler(void);
#endif
八、实物演示章节
8.1 硬件实物准备清单
- STM32F103C8T6 最小系统板 ×1
- VK2828U7G5 定位模块 + 配套陶瓷 GPS 天线 ×1
- 0.96 寸 I2C OLED 屏幕 ×1
- 杜邦线若干
- USB 下载器供电下载程序

8.2 演示操作步骤
- 按照上面表格完成全部接线,确认 GND 全部共地,仔细核对 PB8、PB9、PB11 引脚,不要接错。
- 将 GPS 天线放置在窗边、室外开阔位置;金属外壳、手、遮挡物不要盖住天线。GPS 卫星信号强度非常微弱,遮挡会严重影响搜星。
- 使用下载器把工程固件烧录进 STM32 单片机,上电运行。
- 上电初期 OLED 会打印
GPS: NO DATA,确认接线无误后,很快切换为SEARCHING搜星状态。 - 观察屏幕
VIEW可见卫星数量慢慢上涨;等待模块搜星完成,状态变为GPS: FIX OK,此时经纬度、北京时间正常刷新。 - 可以拿金属物体遮挡 GPS 天线,观察 SAT 参与定位卫星数下降,定位状态丢失变回 SEARCHING;拿开天线遮挡,等待重新定位。
8.3 演示现象解读
- 刚上电需要时间搜星,冷启动(长时间断电)定位时间会更长;热重启(短时间断电)定位速度快。
- 即使屏幕时间已经在跳动更新,也不代表定位成功,一定要看
FIX OK标识和 Q 定位质量。 - 定位成功后,静止状态下经纬度会有小幅跳动,属于普通消费级导航模块正常现象,不能作为高精度测量设备使用。
九、常见故障排查
| OLED 现象 | 可能故障点 | 排查建议 |
|---|---|---|
| 一直 NO DATA | PB11 收不到任何字节 | 检查 GPS TX 是否接到 PB11,模块供电,GND 是否共地 |
| NMEA ERROR | 波特率不对,信号干扰 | 确认模块波特率 9600 8N1;缩短杜邦线,检查地线 |
| SEARCHING 长时间不变 | 通信正常,搜星条件差 | 天线放到开阔窗边,等待搜星 |
| VIEW 有数值,SAT=0 | 能看到卫星,但还没有用于定位 | 继续等待,改善天线摆放位置 |
| 时间刷新,没有坐标 | 模块拿到卫星时间,未完成定位 | 判断定位成功以 FIX OK 和 Q 值为准 |
| 定位频繁丢失 | 遮挡、供电不稳、电磁干扰 | 固定天线,检查电源,减少长杜邦线 |
十、总结
VK2828U7G5 接收导航卫星射频信号,在模块内部完成伪距计算、位置速度时间解算,输出 NMEA 格式串口文本。STM32F103C8T6 通过 USART3 中断配合环形缓冲区接收报文,完成报文校验、语句解析、单位换算,最后把定位信息输出到 OLED 屏幕。
核心要点:定位运算全部由定位模块完成,单片机只做数据接收解析显示;调试时区分串口通信正常和定位有效两个状态,天线摆放环境直接决定搜星快慢。