引言

在嵌入式项目中,GPS模块(如ATGM336H、NEO-6M等)常被用于获取位置、速度和时间信息。这些模块通常通过串口输出符合NMEA-0183协议的ASCII语句,如$GNGGA$GNRMC等。解析这些语句并将其转换为结构化数据,是开发导航、定位功能的第一步。本文分享一个轻量级、可移植的GPS解析驱动,基于STM32 HAL库实现DMA+空闲中断接收,并包含完整的NMEA解析器。

驱动设计目标

  • 可移植性:核心解析代码不依赖特定硬件,仅使用标准C库,易于移植到其他MCU。

  • 高效性:采用DMA+空闲中断接收,减少CPU干预;解析过程无动态内存分配,适合资源受限的嵌入式环境。

  • 稳定性:具备校验和验证、环形缓冲区防溢出、行缓冲区溢出保护等机制。

  • 易用性:提供简洁的API,开发者只需初始化、在中断回调中传递数据、在主循环中处理即可获得解析后的GPS数据。

整体架构

驱动分为三层:

  1. 硬件抽象层:通过HAL库与UART外设交互,配置DMA和中断。

  2. 数据接收层:利用环形缓冲区暂存DMA收到的字节流,并在主循环中提取完整的NMEA行。

  3. 协议解析层:对每一行进行校验、字段分割,并根据语句类型(GGA/RMC)更新数据。

数据接收:DMA + 空闲中断 + 环形缓冲区

STM32的UART支持空闲中断,当总线空闲时触发,非常适合接收不定长数据。配合DMA的循环模式,可以持续接收数据而无需CPU频繁介入。

关键实现

// 初始化时启动DMA接收
HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_dma_buf, RX_BUF_SIZE);

// 空闲中断回调中,将DMA缓冲区数据搬运到环形缓冲区
void GPS_UART_RxEventCallback(GPS_Handle *gps, uint16_t Size) {
    for (uint16_t i = 0; i < Size; i++) {
        uint16_t next_head = (gps->rx_ring_head + 1) % RING_BUF_SIZE;
        if (next_head != gps->rx_ring_tail) {
            gps->rx_ring_buf[gps->rx_ring_head] = gps->rx_dma_buf[i];
            gps->rx_ring_head = next_head;
        }
    }
    // 重新启动DMA
    HAL_UARTEx_ReceiveToIdle_DMA(...);
}

环形缓冲区的读写索引分别在中断和主循环中更新,确保数据安全。

解析器设计:逐行解析,增量更新

NMEA语句以$开头,\r\n结尾,每行长度通常不超过100字节。解析器在GPS_Process中逐字符读取环形缓冲区,遇到\n时认为一行结束。

行提取与解析

while (head != tail) {
    ch = ring_buf[tail++];
    if (ch == '$') line_len = 0;          // 新行开始
    if (line_len < LINE_BUF_SIZE-1) line_buf[line_len++] = ch;
    if (ch == '\n') {                      // 行结束
        line_buf[line_len] = '\0';
        parse_nmea_sentence((char*)line_buf, &gps_data);
        line_len = 0;
    }
}

解析核心:增量更新

为了避免局部变量未初始化导致的随机值(如之前遇到的HDOP异常),解析函数直接修改传入的gps_data_t结构体,仅更新当前语句包含的字段,保留其他字段的历史值。这样,GGA提供定位信息,RMC提供速度、日期,两者互补。

GGA解析(部分)
static int parse_gga(char *fields[], gps_data_t *data) {
    data->hour = ...; data->minute = ...; data->second = ...;
    data->latitude = nmea_to_degrees(fields[2]);
    if (fields[3][0] == 'S') data->latitude = -data->latitude;
    data->longitude = nmea_to_degrees(fields[4]);
    if (fields[5][0] == 'W') data->longitude = -data->longitude;
    data->fix_quality = atoi(fields[6]);
    data->satellites = atoi(fields[7]);
    data->hdop = atof(fields[8]);
    data->altitude = atof(fields[9]);
    data->valid = (data->fix_quality > 0);
    return 0;
}
RMC解析(部分)
static int parse_rmc(char *fields[], gps_data_t *data) {
    // 时间、日期、状态
    data->valid = (fields[2][0] == 'A');
    data->latitude = nmea_to_degrees(fields[3]);
    if (fields[4][0] == 'S') data->latitude = -data->latitude;
    data->longitude = nmea_to_degrees(fields[5]);
    if (fields[6][0] == 'W') data->longitude = -data->longitude;
    data->speed_knots = atof(fields[7]);
    data->speed_kmph = data->speed_knots * 1.852f;
    data->course = atof(fields[8]);
    // 日期转换...
    return 0;
}

语句类型识别

由于现代GPS模块常输出$GNGGA(北斗+GPS混合),解析器通过检查类型字段的后三个字符("GGA"或"RMC")来区分,无需硬编码前缀。

数据结构

  • gps_data_t:存储所有解析后的GPS信息,包括时间、日期、经纬度、海拔、速度、航向、卫星数、HDOP、有效性标志等。

  • GPS_Handle:驱动句柄,包含UART句柄、DMA缓冲区、环形缓冲区、读写索引、数据就绪标志以及最新的解析数据。

API使用示例

GPS_Handle gps;

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if (huart == &huart1) GPS_UART_RxEventCallback(&gps, Size);
}

int main() {
    HAL_Init(); MX_USART1_UART_Init();
    GPS_Init(&gps, &huart1);

    while (1) {
        GPS_Process(&gps);
        if (gps.gps_data.valid) {
            printf("Lat: %.6f, Lon: %.6f\n", gps.gps_data.latitude, gps.gps_data.longitude);
            gps.gps_data.valid = 0;  // 清除标志,等待下一次有效数据
        }
        HAL_Delay(100);
    }
}

实际调试效果

可移植性说明

  • 硬件依赖:仅通过HAL库的HAL_UART_Transmit(用于printf重定向)和DMA相关函数,若移植到其他平台,只需替换这些底层接口,核心解析代码无需改动。

  • 编译器:使用标准C库(string.hstdlib.hctype.h),无平台限制。

  • 内存:所有缓冲区均为静态或局部数组,无动态内存分配,适合裸机或RTOS环境。

调试与优化

  • 原始数据打印:通过宏DEBUG_PRINT_RAW控制,方便观察原始NMEA流。

  • 环形缓冲区大小:根据GPS输出频率调整,一般512字节足够。

  • 解析性能:每秒几十条语句,解析耗时极短,不影响主循环。

总结

本文设计的GPS解析驱动充分考虑了嵌入式开发的实际需求:高效接收、稳健解析、易于移植。通过DMA+空闲中断降低CPU负载,环形缓冲区避免数据丢失,增量更新确保数据完整性,简洁的API降低使用门槛。该驱动已在实际项目中验证,可稳定运行于STM32系列MCU。

改进空间

  • 支持更多NMEA语句(如GSA、GSV)。

  • 增加经纬度格式转换(度分秒转度)。

  • 添加UTC转本地时间的函数。

希望这篇博客能为你的GPS项目开发提供参考。如有任何问题或建议,欢迎交流讨论。

参考工程

通过网盘分享的文件:GPS_NMEA_STM32F103C6T6.zip
链接: https://pan.baidu.com/s/16TK2Zk2i5J2slXHXYs19iw?pwd=9pr5 提取码: 9pr5 
--来自百度网盘超级会员v8的分享

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐