STM32+VK2828U7G5模块GPS定位+NMEA解析

摘要

本文使用 STM32F103C8T6 单片机搭配 VK2828U7G5 卫星定位模块以及 I2C‑OLED 屏幕,实现卫星定位数据读取、NMEA 协议解析,最终把定位状态、经纬度、卫星数量、北京时间显示在 OLED 屏幕上。 VK2828U7G5 模块完成卫星信号接收与定位解算,通过串口输出标准 NMEA‑0183 文本;STM32 利用串口中断接收数据,做校验、解析、单位换算,输出到 OLED。本文包含硬件接线、定位原理、协议解析、故障排查、实物演示以及源码存放章节,适合单片机课程学习、定位模块入门调试。

视频讲解:STM32+VK2828U7G5模块GPS定位+NMEA解析https://www.bilibili.com/video/BV1TVtS6REK4/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

硬件环境:STM32F103C8T6 最小系统板、VK2828U7G5 定位模块、I2C 接口 OLED 显示屏、配套 GPS 天线

一、整体工作流程

整个工程数据流向:

  1. GPS / 北斗等导航卫星向外广播卫星轨道、精确时间信息
  2. VK2828U7G5 天线接收卫星射频信号
  3. 模块内部计算伪距,完成位置、速度、时间解算(PVT)
  4. 模块 TX 引脚串口输出 NMEA 语句,波特率 9600 8N1
  5. STM32 的 USART3(PB11)使用中断方式接收串口数据
  6. 对 NMEA 语句做校验,解析 RMC、GGA、GSV 有效语句
  7. 完成经纬度、时间、速度单位换算
  8. 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 关键数据换算

  1. 经纬度(度分格式转十进制度) 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 存负数。

  1. 时间转换 NMEA 输出 UTC 世界时,程序给 UTC 时间 + 8 小时换算北京时间;同时处理跨午夜、跨月、跨年、闰年的日期进位。
  2. 速度、海拔换算 RMC 速度单位为节,换算:1节 = 1.852 km/h; GGA 海拔单位米,程序放大 100 倍,以厘米定点数存储。

五、串口中断 + 环形缓冲区处理机制

GPS 模块会不间断持续输出 NMEA 报文,OLED 屏幕刷新需要消耗时间。如果采用轮询读取串口,OLED 刷新期间到来的串口字节会直接丢失。

解决方案:USART3 接收中断 + 512 字节环形缓冲区 工作逻辑:

  1. 串口每收到 1 字节,立刻触发 USART3 中断服务函数
  2. 在中断函数内部,只把收到的字节写入环形缓冲区,立刻退出中断,不做复杂运算
  3. 主循环中调用 GPS 处理函数,从缓冲区读取字节,拼接完整 NMEA 语句
  4. 检测到$开头、\r\n结尾的完整报文,执行校验和校验
  5. 校验成功,判断是否 RMC/GGA/GSV,解析更新 GPS 数据结构体;其他类型语句直接丢弃
  6. 更新完成后,主循环刷新 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 硬件实物准备清单

  1. STM32F103C8T6 最小系统板 ×1
  2. VK2828U7G5 定位模块 + 配套陶瓷 GPS 天线 ×1
  3. 0.96 寸 I2C OLED 屏幕 ×1
  4. 杜邦线若干
  5. USB 下载器供电下载程序

8.2 演示操作步骤

  1. 按照上面表格完成全部接线,确认 GND 全部共地,仔细核对 PB8、PB9、PB11 引脚,不要接错。
  2. 将 GPS 天线放置在窗边、室外开阔位置;金属外壳、手、遮挡物不要盖住天线。GPS 卫星信号强度非常微弱,遮挡会严重影响搜星。
  3. 使用下载器把工程固件烧录进 STM32 单片机,上电运行。
  4. 上电初期 OLED 会打印GPS: NO DATA,确认接线无误后,很快切换为SEARCHING搜星状态。
  5. 观察屏幕VIEW可见卫星数量慢慢上涨;等待模块搜星完成,状态变为GPS: FIX OK,此时经纬度、北京时间正常刷新。
  6. 可以拿金属物体遮挡 GPS 天线,观察 SAT 参与定位卫星数下降,定位状态丢失变回 SEARCHING;拿开天线遮挡,等待重新定位。

8.3 演示现象解读

  1. 刚上电需要时间搜星,冷启动(长时间断电)定位时间会更长;热重启(短时间断电)定位速度快。
  2. 即使屏幕时间已经在跳动更新,也不代表定位成功,一定要看FIX OK标识和 Q 定位质量。
  3. 定位成功后,静止状态下经纬度会有小幅跳动,属于普通消费级导航模块正常现象,不能作为高精度测量设备使用。

九、常见故障排查

OLED 现象 可能故障点 排查建议
一直 NO DATA PB11 收不到任何字节 检查 GPS TX 是否接到 PB11,模块供电,GND 是否共地
NMEA ERROR 波特率不对,信号干扰 确认模块波特率 9600 8N1;缩短杜邦线,检查地线
SEARCHING 长时间不变 通信正常,搜星条件差 天线放到开阔窗边,等待搜星
VIEW 有数值,SAT=0 能看到卫星,但还没有用于定位 继续等待,改善天线摆放位置
时间刷新,没有坐标 模块拿到卫星时间,未完成定位 判断定位成功以 FIX OK 和 Q 值为准
定位频繁丢失 遮挡、供电不稳、电磁干扰 固定天线,检查电源,减少长杜邦线

十、总结

VK2828U7G5 接收导航卫星射频信号,在模块内部完成伪距计算、位置速度时间解算,输出 NMEA 格式串口文本。STM32F103C8T6 通过 USART3 中断配合环形缓冲区接收报文,完成报文校验、语句解析、单位换算,最后把定位信息输出到 OLED 屏幕。

核心要点:定位运算全部由定位模块完成,单片机只做数据接收解析显示;调试时区分串口通信正常和定位有效两个状态,天线摆放环境直接决定搜星快慢。

相关推荐
白搞电子18 分钟前
ESP-Mosaico 掌上游戏开发:LVGL 抛硬币动画、音频混音与 BMI270 摇晃触发
单片机·嵌入式硬件·物联网·游戏·音视频
深圳市恒锐丰科技杨生19 分钟前
EG1163 (S) 高压大电流降压同步整流 DC‑DC 控制器|外置 MOS 降压电源方案
嵌入式硬件·硬件工程
安河桥。36 分钟前
V4L2架构在工程实践中的应用
嵌入式硬件·音视频
新晨单片机设计1 小时前
S004A-基于STM32单片机密码锁【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus
启芯硬件1 小时前
《硬件电路设计实战100例》专栏核心信息,定位及设计案例分享
人工智能·经验分享·嵌入式硬件·硬件工程·高速仿真
深圳市恒锐丰科技杨生1 小时前
EG1162 高压大电流降压同步整流 DC‑DC 控制器|外置 MOS 降压电源方案
嵌入式硬件·硬件工程
DevLoom_2 小时前
Mac 移动硬盘不显示?2026整理汇总完整解决方案
stm32·单片机·macos
Yiran_G2 小时前
LoRa在智慧农业中的应用:无线传感网络如何实现远程环境监测?
嵌入式硬件·物联网
佳児素花痴╮2 小时前
STM32U575RIT6(SPI+数码管+LED)
stm32·单片机·嵌入式硬件