引言
在嵌入式项目中,GPS模块(如ATGM336H、NEO-6M等)常被用于获取位置、速度和时间信息。这些模块通常通过串口输出符合NMEA-0183协议的ASCII语句,如$GNGGA、$GNRMC等。解析这些语句并将其转换为结构化数据,是开发导航、定位功能的第一步。本文分享一个轻量级、可移植的GPS解析驱动,基于STM32 HAL库实现DMA+空闲中断接收,并包含完整的NMEA解析器。
驱动设计目标
-
可移植性:核心解析代码不依赖特定硬件,仅使用标准C库,易于移植到其他MCU。
-
高效性:采用DMA+空闲中断接收,减少CPU干预;解析过程无动态内存分配,适合资源受限的嵌入式环境。
-
稳定性:具备校验和验证、环形缓冲区防溢出、行缓冲区溢出保护等机制。
-
易用性:提供简洁的API,开发者只需初始化、在中断回调中传递数据、在主循环中处理即可获得解析后的GPS数据。
整体架构
驱动分为三层:
-
硬件抽象层:通过HAL库与UART外设交互,配置DMA和中断。
-
数据接收层:利用环形缓冲区暂存DMA收到的字节流,并在主循环中提取完整的NMEA行。
-
协议解析层:对每一行进行校验、字段分割,并根据语句类型(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.h,stdlib.h,ctype.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的分享