ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 GPS数据解析实战:基于NMEA0183协议的经纬度提取与串口DMA接收

STM32 GPS数据解析实战:基于NMEA0183协议的经纬度提取与串口DMA接收 STM32的GPS数据提取程序设计说明——基于NMEA0183协议搞嵌入式这些年做过不少和定位相关的项目从最早的简易轨迹记录仪到后来的车载终端、共享设备定位模块几乎每一步都绕不开GPS数据解析这件事。很多新手拿到GPS模块串口能打印出$GNGGA、$GPRMC这样的字符串但真正要把经纬度、时间、速度提取出来用就卡住了——数据格式看着乱、逗号分割的字段对不上号、偶尔还会出现乱码或者校验失败。这篇东西就把我实际做过的STM32 GPS数据提取程序完整拆开讲一遍基于NMEA0183协议从协议格式、硬件接线一直讲到底层解析代码怎么写读完你也能自己写出一套稳定可靠的GPS数据解析程序。这套程序解决的核心问题很简单让STM32通过串口接收GPS模块输出的NMEA0183语句从中提取出经纬度、UTC时间、定位状态、对地速度等关键信息并且能稳定应对数据丢帧、字段异常、校验失败这些实际场景。不管是做毕设的物联网定位终端还是做实车测试的数据记录仪这套思路和代码可以直接拿去改。方案设计与协议拆解1.1 为什么选择NMEA0183协议GPS模块的输出格式五花八门但绝大多数消费级和工业级模块比如u-blox NEO-M8N、中科微ATGM336H、志远科技那些默认输出的都是NMEA0183格式。这个协议是航海电子协会定的标准本质上是ASCII码的文本流通过串口以波特率9600或者115200往外发。选NMEA0183而不是直接用模块厂商的二进制协议比如u-blox的UBX协议原因有三个第一是通用性强换任何品牌的模块代码基本不用动第二是调试方便随便一个串口助手就能看到原始数据对比解析结果非常直观第三是信息量够用经纬度、高度、速度、时间、卫星数全都在里面。NMEA0183的语句长这样$GNGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47每条语句以$开头以回车换行\r\n结束中间用逗号分割字段末尾的*后面跟两位十六进制校验值。这个格式看起来简单实际解析时坑不少后面会详细说。1.2 各类语句的用途与取舍NMEA0183里常用的语句有好几种GGA、RMC、GSA、GSV、VTG、GLL各有各的用途。实际做项目时不需要全解析按需求挑就行。语句全称核心内容典型用途GGAGlobal Positioning System Fix Data定位时间、经纬度、定位质量、卫星数、海拔定位状态判断、经纬度提取RMCRecommended Minimum Specific GPS/TRANSIT Data推荐最小定位数据含UTC时间、经纬度、速度、航向运动轨迹、速度计算GSAGPS DOP and Active Satellites定位模式、精度因子、参与定位的卫星编号定位质量分析GSVSatellites in View可见卫星信息、信噪比天线安装调试VTGCourse Over Ground and Ground Speed对地速度、航向测速场景我的习惯是优先解析GGA和RMC这两条。GGA负责定位状态和经纬度RMC负责时间和速度。有些老模块不输出RMC那就只解析GGA也能拿到核心数据。GSV一般不解析但调试天线位置时很有用——信噪比数据在GSV里哪天定位效果差先把GSV打出来看看卫星信号强度能省不少排查时间。1.3 经纬度格式的地理常识很多人在这一步栽跟头。NMEA0183里的经纬度是度分格式不是标准的小数度。比如GGA语句里的4807.038意思不是48.07038度而是48度07.038分。换算成小数度要这样算十进制度数 度 分 / 60 48度07.038分 48 7.038 / 60 48.1173度北纬N和东经E是正数南纬S和西经W是负数。这个换算要是做错了坐标飘出去好几公里你都不知道。后面代码里我会专门写一个转换函数。硬件连接与底层配置2.1 GPS模块与STM32的接线方案GPS模块和STM32之间用UART串口通信接线就三根线TX、RX、GND。模块的TX接STM32的RX模块的RX接STM32的TX注意交叉连接。我用得最多的是STM32F103系列串口资源丰富USART1做调试打印USART2接GPS模块互不干扰。如果用的是STM32F030这种小资源芯片只有一个串口那就得把GPS接到这个串口上调试信息改用IO口模拟或者干脆不要调试输出把RAM和Flash省下来给解析代码用。GPS模块的供电要注意很多模块是3.3V供电有些是5V买模块时看清规格。STM32的串口引脚是3.3V电平如果模块是5V供电但串口电平是3.3V兼容的可以直接连如果模块输出5V电平最好加个电平转换芯片比如MAX3232或者简单的电阻分压不然长期运行有烧引脚的风险。提示天线的位置对GPS信号影响极大。陶瓷天线要放在朝向天空的位置远离MCU和走线。我遇到过定位慢的问题把天线从板子中央挪到边缘冷启动时间从2分钟缩短到40秒。2.2 串口DMA接收配置要点GPS模块的数据是持续不断的文本流最高频时每秒输出10帧以上。如果用中断一个个字节接收频繁进中断会占用大量CPU时间而且数据稍微多点就容易丢字节。我的方案是用STM32的串口空闲中断加DMA接收一帧数据结束总线上空闲了才通知CPU处理效率高得多。以STM32F103标准库为例核心配置思路如下// 定义接收缓冲区 #define GPS_RX_BUF_SIZE 256 uint8_t gps_rx_buf[GPS_RX_BUF_SIZE]; volatile uint8_t gps_rx_len 0; volatile uint8_t gps_rx_complete 0; // 初始化串口2波特率96008数据位1停止位无校验 void GPS_UART_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; DMA_InitTypeDef DMA_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA9(TX) PA10(RX) 配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 串口配置 USART_InitStructure.USART_BaudRate 9600; 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(USART1, USART_InitStructure); // DMA配置接收方向 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)gps_rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; 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_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); // 使能串口空闲中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能DMA和串口 DMA_Cmd(DMA1_Channel5, ENABLE); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); USART_Cmd(USART1, ENABLE); // 配置NVIC NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }空闲中断的用法是关键。当串口接收完一帧数据总线上出现空闲状态时就会触发这个中断。这时候DMA已经把数据存到缓冲区了我们在中断里做的事情是记录接收到的数据长度清标志位然后置一个标志通知主循环来处理。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 清除IDLE标志先读SR再读DR USART_ReceiveData(USART1); // 计算已接收数据长度 gps_rx_len GPS_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 数据接收完成标志 gps_rx_complete 1; // 重新填充DMA目标地址 DMA_SetCurrDataCounter(DMA1_Channel5, GPS_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }注意DMA循环模式Circular模式下数据会覆盖写入缓冲区。如果主循环来不及处理新数据会覆盖旧数据。所以主循环里发现gps_rx_complete标志置位后要尽快拷贝数据到自己的处理缓冲区再清标志。这个缓冲区大小256字节能放下GPS模块一帧完整数据但放不下多帧。以一帧最大约120字节算256字节够用。2.3 波特率选择背后的考量GPS模块默认波特率通常是9600少数是115200。选哪个我的建议是能用9600就不升115200。原因是NMEA0183语句本身数据量不大GPS模块每秒最多输出10帧9600波特率完全够用而且波特率越低信号抗干扰能力越强长距离传输更稳。115200波特率虽然快但如果你的GPS模块和STM32之间的走线比较长或者环境电磁干扰大高波特率反而更容易出错。不过要注意波特率要以GPS模块实际配置为准。很多模块支持通过软件修改波特率参数但断电重启后可能会恢复默认值。我遇到过客户把模块配置成115200结果换了个环境上电后模块恢复9600程序卡在等待数据上。稳妥的做法是程序启动时自动检测波特率或者直接在代码配置里和模块实际状态保持一致。核心解析代码实现3.1 帧接收与完整性校验拿到串口数据后第一件事不是解析而是校验。NMEA0183每条语句末尾都有校验值格式是*后面跟两个十六进制字符。校验算法很简单把$和*之间的所有字符按位异或得到的结果就是校验值。比如$GPGGA,123519,4807.038,N,01131.000,E,1,08这一段每个字符的ASCII码依次异或最后得到的值是0x47所以语句末尾是*47。校验函数这样写// 校验NMEA帧数据 // 返回1表示校验通过0表示失败 uint8_t GPS_CheckSum(const char *buf, uint16_t len) { uint8_t check 0; uint16_t i 1; // 跳过$ // 找*位置 while (i len buf[i] ! *) { check ^ buf[i]; i; } // 如果没有找到*校验失败 if (i len) return 0; // 提取帧尾的两位十六进制校验值 char hex_str[3]; hex_str[0] buf[i 1]; hex_str[1] buf[i 2]; hex_str[2] \0; uint8_t check_sum (uint8_t)strtol(hex_str, NULL, 16); return (check check_sum) ? 1 : 0; }校验不能省。GPS模块输出的数据有可能因为干扰或者模块自身问题出错如果不过校验直接解析可能把错误的经纬度当成真值用在需要精确定位的场景里后果很严重。除了校验和还有一个细节判断一帧是否完整。NMEA0183语句以\r\n结束但我们在DMA空闲中断里接到的数据可能是多帧叠加的也可能是半截帧。所以进主循环后先要在缓冲区里找$开头和\r\n结尾的完整帧再做解析。我一般写一个简单的帧提取函数从缓冲区里逐字节扫描找到完整帧后一次性拷贝出来处理。3.2 GGA语句解析与经纬度转换GGA语句的格式标准如下按逗号分割$GNGGA,时间,纬度,纬度方向,经度,经度方向,定位质量,卫星数,水平精度因子,海拔,海拔单位,大地水准面高度,高度单位,差分时间,差分站ID*校验和字段编号从0开始索引含义示例0语句标识$GNGGA1UTC时间 hhmmss.sss1235192纬度 ddmm.mmmm4807.0383纬度方向 N/SN4经度 dddmm.mmmm01131.0005经度方向 E/WE6定位质量 0无效 1GPS定位 2差分定位17卫星数088水平精度因子0.99海拔545.4解析时要先按逗号把语句拆成字段然后逐个提取。在C语言里用strtok函数或者自己写个简单的分割函数都行。我自己写了一个轻量的字段提取函数避免strtok修改原字符串带来的麻烦// 从NMEA语句中提取第index个逗号分割的字段 // 返回字段在buf中的偏移地址找不到返回NULL char* GPS_GetField(char *buf, uint8_t index) { char *p buf; uint8_t i 0; // 跳过$ p; while (i index) { p strchr(p, ,); if (p NULL) return NULL; p; i; } return p; } // 解析GGA语句 void GPS_ParseGGA(char *buf, GPS_Info_t *gps) { char *field; char temp[16]; // 字段6定位质量 field GPS_GetField(buf, 6); if (field) { gps-fix_quality atoi(field); } // 字段2纬度 field GPS_GetField(buf, 2); if (field (field[0] ! ,)) { gps-lat GPS_ConvertDegMin(field); } // 字段3纬度方向 field GPS_GetField(buf, 3); if (field (field[0] S)) { gps-lat -gps-lat; } // 字段4经度 field GPS_GetField(buf, 4); if (field (field[0] ! ,)) { gps-lon GPS_ConvertDegMin(field); } // 字段5经度方向 field GPS_GetField(buf, 5); if (field (field[0] W)) { gps-lon -gps-lon; } // 字段7卫星数 field GPS_GetField(buf, 7); if (field) { gps-satellite_num atoi(field); } // 字段9海拔单位米 field GPS_GetField(buf, 9); if (field (field[0] ! ,)) { gps-altitude atof(field); } }这里GPS_ConvertDegMin函数就是把度分格式转小数度的关键// 将NMEA度分格式转换为十进制度数 // 输入4807.038 返回48.1173 double GPS_ConvertDegMin(char *deg_min) { double value atof(deg_min); int degree (int)(value / 100.0); double minute value - degree * 100.0; return degree minute / 60.0; }3.3 RMC语句解析与时间速度提取RMC语句包含时间、定位状态、经纬度、速度、航向和日期是最精简的一组数据。格式如下$GNRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A关键字段索引含义示例1UTC时间1235192定位状态 A有效 V无效A3纬度4807.0384纬度方向N5经度01131.0006经度方向E7对地速度节022.48航向度084.49UTC日期 ddmmyy230394速度字段的单位是节knots1节1海里/小时1.852公里/小时。要转成公里/小时就乘1.852要转成米/秒就乘0.5144。我做车载项目时通常需要公里/小时所以在解析函数里直接换算好存储。// 解析RMC语句 void GPS_ParseRMC(char *buf, GPS_Info_t *gps) { char *field; // 字段2定位状态 A/V field GPS_GetField(buf, 2); if (field (field[0] A)) { gps-pos_valid 1; } else { gps-pos_valid 0; } // 字段7对地速度节 field GPS_GetField(buf, 7); if (field (field[0] ! ,)) { gps-speed_kmh atof(field) * 1.852; } // 字段8航向 field GPS_GetField(buf, 8); if (field (field[0] ! ,)) { gps-course atof(field); } // 字段9日期格式ddmmyy field GPS_GetField(buf, 9); if (field (field[0] ! ,)) { // 拆出年月日 char temp[3]; temp[0] field[0]; temp[1] field[1]; temp[2] \0; gps-day atoi(temp); temp[0] field[2]; temp[1] field[3]; gps-month atoi(temp); temp[0] field[4]; temp[1] field[5]; gps-year atoi(temp) 2000; } }RMC的时间字段是UTC时间比北京时间慢8小时。如果需要本地时间要加8小时并处理日期进位。很多项目里GPS模块可以配合RTC芯片使用GPS负责校时RTC负责时间显示这个配合思路在车载和时钟类项目里很常用。3.4 完整解析流程架构把所有环节串起来主循环里的处理流程大概是这样的// GPS数据结构体 typedef struct { uint8_t pos_valid; // 定位有效标志 uint8_t fix_quality; // 定位质量 0/1/2 uint8_t satellite_num; // 卫星数 double lat; // 纬度十进制度 double lon; // 经度十进制度 double altitude; // 海拔米 double speed_kmh; // 速度公里/小时 double course; // 航向度 uint8_t day, month; // 日期 uint16_t year; // 年份 uint8_t hour, minute, second; // UTC时间 } GPS_Info_t; GPS_Info_t gps_info; // 主循环处理 void GPS_Process(void) { static char parse_buf[GPS_RX_BUF_SIZE]; uint16_t len; char *p_start, *p_end; if (gps_rx_complete) { gps_rx_complete 0; len gps_rx_len; // 拷贝数据到处理缓冲区 memcpy(parse_buf, gps_rx_buf, len); parse_buf[len] \0; // 逐条提取完整帧并解析 p_start parse_buf; while ((p_start strchr(p_start, $)) ! NULL) { p_end strstr(p_start, \r\n); if (p_end NULL) break; *p_end \0; // 校验和验证 if (GPS_CheckSum(p_start, strlen(p_start))) { // 根据语句类型分发解析 if (strncmp(p_start, $GNGGA, 6) 0 || strncmp(p_start, $GPGGA, 6) 0) { GPS_ParseGGA(p_start, gps_info); } else if (strncmp(p_start, $GNRMC, 6) 0 || strncmp(p_start, $GPRMC, 6) 0) { GPS_ParseRMC(p_start, gps_info); } } p_start p_end 2; // 跳过\r\n } } }注意我判断语句类型时同时匹配了GN和GP前缀。这是因为不同的GPS模块输出前缀可能不一样有的输出GP有的输出GN支持多星座的模块。程序写得通用一些以后换模块不用改代码。常见问题与排查技巧4.1 串口收不到数据怎么查这是最常见的故障。我一般按这个顺序排查先用USB-TTL模块直接连接GPS模块在电脑串口助手上看有没有数据输出。如果没数据说明GPS模块本身没工作检查模块供电和天线。模块正常但STM32收不到。用示波器或者万用表量GPS模块TX引脚有没有电平跳变有跳变但STM32收不到大概率是接错引脚或者串口时钟没配置对。如果GPS的TX和STM32的RX之间串了电阻检查电阻值太大比如10K以上可能拉低信号质量。调试技巧在GPS解析代码里加一个调试计数器每次串口空闲中断触发就加1最后通过调试串口打印出来。如果计数器不增长说明串口中断都没触发如果增长但解析不到有效语句说明数据格式和预期不一致。4.2 校验失败率高的原因如果你发现校验一直失败数据偶尔能解析出来大概率是波特率匹配没问题但数据在多帧传输中丢失了字节。常见原因有三个第一DMA缓冲区太小GPS模块一秒钟输出多帧你的缓冲区可能被覆盖。解决办法是增加缓冲区大小或者加环形缓冲区。第二主循环处理不及时。如果主循环里做了大量耗时操作比如驱动屏幕刷新、写SD卡空出来的时间不够DMA搬运数据数据就会丢。这种情况要学会拆分任务把GPS解析放到定时中断里做或者降低屏幕刷新频率。第三干扰导致数据错误。GPS模块的线是否和电机驱动线、电源线并行走线长距离传输时有没有用双绞线或者屏蔽线这几个问题处理不好校验失败是家常便饭。注意不要一上来就怀疑GPS模块坏了。我遇到过客户着急联系我说模块有问题后来发现是他用的ST-Link的虚拟串口驱动没装好串口助手打不开数据其实一直在输出。先检查电脑端再查硬件链路最后查代码。4.3 坐标漂移和精度问题解析出经纬度后你可能会发现坐标点会漂移静止时坐标也在小范围跳动。这是GPS本身的特性民用GPS精度本来就在2~5米左右加上多径效应、大气层影响跳动几米都算正常。如果漂移特别严重比如静止时坐标跳出去几百米检查天线是否靠近金属物体或者模块的定位状态是不是有效的A状态。如果RMC语句里的状态是V无效说明模块还没锁定足够的卫星这时候的坐标数据是不可信的代码里要根据pos_valid标志过滤掉无效数据。还有一点容易被忽略把经纬度直接打印出来用和在地图上显示是有区别的。很多地图SDK用GCJ-02坐标系火星坐标GPS原始输出是WGS-84坐标系两者之间有几米的偏差。如果项目要求地图上精确显示位置需要做坐标系转换这个在车载导航、共享定位类项目里几乎是必做的。4.4 冷启动时间长是不是程序的问题GPS模块冷启动完全断电后重新上电需要搜索卫星并下载星历时间通常在30秒到几分钟不等。如果你发现程序上电后很久才定位成功不一定是程序问题可能是天线位置不好收不到足够卫星信号模块进入省电模式搜索卫星的灵敏度降低周围环境遮挡严重室内、高楼密集区程序层面能做的是在上电后先输出调试信息说明当前GPS模块输出了几条有效语句、卫星数是多少这样能和正常情况做对比。另外有些模块支持掉电保存星历断电时用备用电池给模块供电下次上电热启动速度就快很多。程序优化与扩展建议5.1 缓冲区与内存管理的优化用DMA加空闲中断处理GPS数据时最容易出问题的就是缓冲区管理。上面代码用的是DMA直接接收定长缓冲区主循环来拷贝。这个方案简单可靠但有一个问题如果一帧数据长度超过缓冲区大小数据会被截断。我做的一个小优化是把固定缓冲区改成环形缓冲区ring buffer写指针由DMA更新读指针由主循环控制。这样GPS数据流可以持续不断地被缓冲主循环按需取数据不会丢帧。环形缓冲区的代码量不大但对系统的稳定性提升很明显。5.2 什么时候需要中断解析什么时候用轮询主循环轮询式的解析上面代码的做法适合大多数场景因为GPS数据的实时性要求不高——每秒几帧的刷新率人眼看起来已经很流畅了。但如果你的项目里主循环负载很重比如同时驱动TFT屏幕、处理按键、控制电机轮询的延迟就可能变大GPS数据在缓冲区里的等待时间变长很可能造成缓冲区溢出。这种情况下可以做一个折中方案串口空闲中断里只做数据拷贝和标志置位新增一个低优先级的定时中断比如100ms触发一次来做帧提取和解析。这样保证GPS数据本身不丢解析又不会挤占主循环太多时间。5.3 从GPS到更广的应用场景解析出经纬度、速度、时间后可以做的东西就多了。我做过的项目里这些数据被用来电子围栏判断设备是否离开设定区域触发告警轨迹回放定时记录经纬度到SD卡或Flash配合上位机画轨迹速度监测超速报警和加速度计配合判断急加速急减速时间校准用GPS的UTC时间校准RTC时钟代码设计时建议把GPS解析模块做成一个独立的功能模块对外提供统一的API接口void GPS_Init(void); void GPS_Process(void); GPS_Info_t* GPS_GetInfo(void); uint8_t GPS_IsValid(void);这样换平台或者换GPS模块时只需要改底层串口和协议解析上层调用逻辑不用动。5.4 STM32平台适配经验本文代码以STM32F103标准库为例但思路是通用的。如果你用的是HAL库串口空闲中断和DMA接收的配置略有不同核心逻辑一样。如果你用STM32CubeMX生成工程可以在图形化界面上直接把串口配置成DMA模式再到中断回调函数里添加自己的处理逻辑。换到小熊派、AT32、GD32这几类芯片时GPIO和串口的寄存器命名有差异但DMA加空闲中断的思路完全适用。移植时最花时间的往往不是数据解析逻辑而是底层驱动的适配。我个人的建议是先把解析代码写成纯C语言、不依赖任何硬件平台的部分只依赖标准库的字符串处理函数这样核心代码可以跨平台复用。底层串口接收和顶层应用各做各的分层清晰以后维护也方便。实测数据与效果分析6.1 验证环境与测试方法我用STM32F103C8T6作为主控外接一块ATGM336H GPS模块中科微的芯片性价比很高天线用无源陶瓷天线室外测试。程序烧录后通过调试串口每秒打印一次解析结果。测试时把模块放在车顶完全无遮挡冷启动到首次有效定位用了约35秒随后每秒刷新一次数据。测试数据样例[GPS] fix1 sat11 lat31.230508 lon121.473685 alt12.5m speed0.0km/h course0.0 [GPS] fix1 sat11 lat31.230507 lon121.473684 alt12.5m speed0.0km/h course0.0 [GPS] fix1 sat11 lat31.230509 lon121.473683 alt12.5m speed0.0km/h course0.0静止状态下经纬度小数点后第5位在跳动换算成距离大约是米级波动这是正常范围。6.2 资源占用与性能表现编译环境用Keil MDK 5优化等级-O2代码占用情况项目占用Flash约5.2KB含驱动层RAM约520字节含DMA缓冲区CPU负载解析一帧不到50微秒这个资源占用在STM32F103这种入门芯片上完全没有压力系统还能同时跑LCD显示、按键扫描和SD卡记录。实际上我用这套解析代码做过一个带2.8寸TFT屏幕的定位终端帧率稳定在10Hz以上屏幕刷新不受影响。6.3 实际项目中踩过的坑做一个定位记录仪时遇到过一个问题设备在车里用了两天第三天开始经纬度全部变成0。排查了很久发现是GPS模块供电不稳车辆点火瞬间电压跌落导致模块复位复位后模块重新冷启动而程序里又没有处理“模块复位后重新握手”的逻辑导致解析一直用旧数据。后来在代码里加了一个超时判断如果连续3秒没有收到有效定位数据就把设备状态切到“定位丢失”并尝试重新初始化GPS模块。这个逻辑加上后问题彻底解决。还遇到过一个很隐蔽的问题有的GPS模块输出的经纬度是0.000000但定位字段显示有效。这是模块冷启动未完的中间状态程序里单独判断经纬度是否为0无效才算真正解决。最后一个建议做GPS类项目时日志记录一定要做。不要求每次都记全量数据但至少保留最近一段时间的原始NMEA数据和解析结果定位异常的排查会省很多力气。我有一次做户外测试数据全部正确客户非说定位跳了后来靠日志回放才发现是他使用的第三方地图坐标系转换的问题GPS本身没有错。个人经验告诉我GPS数据提取这个环节看着简单但想做得稳、做得可靠协议细节、底层通信、异常处理一个都不能少。尤其是校验和经纬度格式转换这两个细节宁可多花十分钟写清楚也别等跑起来才发现数据不对。希望这篇文章对正在调试GPS模块的你有所帮助。
返回列表