在电赛(如无人机、自动驾驶小车题目)中,GPS 模块是绝对的核心传感器。很多新手一上来的常规操作是:开启串口 RXNE 接收中断,每收到一个字节进一次中断,存入数组,最后再用 C 语言标准库的sscanf函数去提取经纬度。
结果往往是灾难性的:由于单片机被高频的串口中断疯狂打断,你的电机 PID 计算出现严重延迟,小车疯狂抖动;而sscanf函数巨大的内存开销和极慢的执行速度,更是直接榨干了 CPU 的算力。
今天,我将分享一套真正在工业界和高阶电赛中使用的“零阻流” GPS 接收与解析架构。本文附带基于 STM32F4 标准库的完整源码(借鉴了逐飞科技的优秀解析逻辑),带你体验什么叫极致的丝滑。
一、 硬件级减负:UART5 + DMA + IDLE 空闲中断
如果你还在用USART_IT_RXNE逐字节接收,请立刻扔掉它。GPS 数据是一帧一帧发送的,我们完全可以请 DMA 这个“免费搬运工”代劳。
核心思想:
我们让 DMA 直接把 UART5 接收到的数据静默搬运到
gps_rx_buffer中,期间CPU 完全不参与。当 GPS 的一帧数据发完时,串口 RX 总线会闲置。此时触发IDLE(空闲)中断。
在空闲中断里,CPU 只需要做一件事:停止 DMA,计算接收到了多少个字节(
GPS_RX_BUF_SIZE - DMA_GetCurrDataCounter),立起gps_rx_flag标志位,然后重新开启 DMA 迎接下一帧。
避坑指南(见源码【修正2】与【修正3】):在重新配置并开启 DMA 之前,必须使用
DMA_ClearFlag清除所有的错误和传输完成标志位,否则 DMA 会直接死锁,再也接收不到数据!
二、 字符串解析的“降维打击”:抛弃sscanf
NMEA 协议的语句(如$GNRMC和$GNGGA)是以逗号分隔的。 标准的sscanf("%f,%f,%d...", ...)虽然写起来爽,但它会引入庞大的浮点运算库,极其耗时。
在源码中,我们采用了一种极其巧妙的手动寻址法:
get_parameter_index(uint8_t num, char *str):这个函数的作用是“数逗号”。你要找经度?它直接数到第 5 个逗号,返回数据的起始指针。str_to_double/str_to_int:拿到指针后,自己写一个简单的 ASCII 码转数字的while循环。
这种纯手工的内存推演算法,执行速度比sscanf快上数十倍,为你的姿态解算(如对接 JY901S 等 IMU 数据)留出了极其充裕的 CPU 时间!
三、 最容易被忽略的“硬核操作”:GPS 模块瘦身
代码最末尾的gps_init函数,是整篇文章的“灵魂”所在。
默认出厂的 GPS 模块更新率通常只有 1Hz(1秒更新一次),并且会疯狂吐出一大堆无用的报文(GSV 卫星视图、GLL 经纬度等)。如果你强行把更新率提到 10Hz,庞大的字符量会瞬间撑爆串口带宽。
高端玩法:通过发送 Hex 指令对芯片进行底层配置。
提速:发送配置帧,强制模块将更新率提升到 10Hz(100ms 刷新一次),满足高速运动控制需求。
瘦身:发送一系列
close_xxx指令,强制关闭 GLL、GSA、GRS、GSV 等无关报文。精准保留:只留下
RMC(包含速度、时间、经纬度)和GGA(包含海拔高度、搜星数量),将总线负担降到最低!
四、 总结
从 DMA 的硬件搬运,到手写的高效字符串解析,再到对 GPS 芯片的底层协议阉割。这套代码完美诠释了嵌入式开发的精髓:榨干每一滴硬件性能,不浪费哪怕一个 CPU 时钟周期。代码已经奉上,赶紧下载烧录,去感受那 10Hz 丝滑且精准的定位数据吧!
#include "bsp_gps.h" // 全局变量定义 uint8_t gps_rx_buffer[GPS_RX_BUF_SIZE]; volatile uint16_t gps_rx_len = 0; volatile uint8_t gps_rx_flag = 0; void GPS_UART5_DMA_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 【修正1】确保外设时钟先于DMA时钟使能 // 使能UART5和其GPIO所在的总线时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART5, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE); // 最后使能DMA时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); // GPIO 复用映射 GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_UART5); // PC12 -> UART5_TX GPIO_PinAFConfig(GPIOD, GPIO_PinSource2, GPIO_AF_UART5); // PD2 -> UART5_RX // GPIO 初始化 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // TX: PC12 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_Init(GPIOC, &GPIO_InitStructure); // RX: PD2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_Init(GPIOD, &GPIO_InitStructure); // UART5 初始化 USART_DeInit(UART5); USART_InitStructure.USART_BaudRate = baudrate; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(UART5, &USART_InitStructure); // 【修正2】更彻底的DMA流复位和配置流程 // UART5_RX 对应 DMA1_Stream0, Channel 4 DMA_DeInit(DMA1_Stream0); // 必须等待DeInit完成,否则后续配置可能失败 while (DMA_GetCmdStatus(DMA1_Stream0) != DISABLE); DMA_InitStructure.DMA_Channel = DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(UART5->DR); DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)gps_rx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = GPS_RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 使用Normal模式,每次接收完手动重置 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_Init(DMA1_Stream0, &DMA_InitStructure); // 【修正3】确保在使能DMA流之前清除所有相关标志位 DMA_ClearFlag(DMA1_Stream0, DMA_FLAG_FEIF0 | DMA_FLAG_DMEIF0 | DMA_FLAG_TEIF0 | DMA_FLAG_HTIF0 | DMA_FLAG_TCIF0); // 使能DMA流 DMA_Cmd(DMA1_Stream0, ENABLE); // 使能串口DMA接收请求和空闲中断 USART_DMACmd(UART5, USART_DMAReq_Rx, ENABLE); USART_ITConfig(UART5, USART_IT_IDLE, ENABLE); // NVIC 中断配置 NVIC_InitStructure.NVIC_IRQChannel = UART5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 最后使能UART5 USART_Cmd(UART5, ENABLE); } // 中断服务函数保持不变 void UART5_IRQHandler(void) { if(USART_GetITStatus(UART5, USART_IT_IDLE) != RESET) { volatile uint32_t temp; temp = UART5->SR; temp = UART5->DR; (void)temp; DMA_Cmd(DMA1_Stream0, DISABLE); while (DMA_GetCmdStatus(DMA1_Stream0) != DISABLE); gps_rx_len = GPS_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Stream0); gps_rx_flag = 1; // 清除可能残留的DMA标志位,为下次启动做准备 DMA_ClearFlag(DMA1_Stream0, DMA_FLAG_FEIF0 | DMA_FLAG_DMEIF0 | DMA_FLAG_TEIF0 | DMA_FLAG_HTIF0 | DMA_FLAG_TCIF0); DMA_SetCurrDataCounter(DMA1_Stream0, GPS_RX_BUF_SIZE); DMA_Cmd(DMA1_Stream0, ENABLE); } } // 发送函数保持不变 void uart_putbuff_std(uint8_t *buff, uint32_t len) { for(uint32_t i = 0; i < len; i++) { while(USART_GetFlagStatus(UART5, USART_FLAG_TXE) == RESET); USART_SendData(UART5, buff[i]); } while(USART_GetFlagStatus(UART5, USART_FLAG_TC) == RESET); }