ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32F407北斗GPS模块NMEA-0183报文解析实战

STM32F407北斗GPS模块NMEA-0183报文解析实战 简介面向嵌入式开发与单片机学习者的完整NMEA-0183协议解析工程以STM32F407ZG为验证平台实现北斗/GPS多模模块的报文解析覆盖GNGGA、GPGSA、BDGSA、GPGSV、BDGSV、GNRMC、GNVTG等常见语句。压缩包共95个文件以h/c源码头文件、o/crf等编译中间文件、uvprojx工程配置、sct链接脚本及hex烧录文件为主整体仅2.89MB便于直接打开、编译与烧录验证。相比零散代码其目录结构清晰外设配置USART、定时器与解析逻辑分离读者可快速定位GNSS数据提取、校验和计算、字段拆分等关键环节也能参考其工程组织方式方便迁移到其他STM32型号。目前已有3711人浏览学习适合刚接触STM32或GNSS解析的开发者作为入门参考也适合需要快速集成定位功能的项目复用。 北斗和GPS的报文解析在嵌入式里是个老生常谈但又绕不开的话题。只要你的板子要定位、要授时、要记录轨迹基本就绕不过NMEA-0183这套协议。最近我基于STM32F407把一套完整的解析工程整理了出来从串口数据接收到经纬度、时间、速度的字段提取全部跑通这篇文章就把整个思路和代码逻辑摊开来讲包括我踩过的几个坑。这套工程的核心并不复杂北斗GPS模块上电后会以固定的频率通过串口向外输出一行一行的NMEA语句我们只需要在STM32F407上用一个USART把数据收进来然后按行、按逗号切出关键字段再转换成我们需要的浮点数、整型或者是时间结构体。真正有点讲究的地方在于数据接收的可靠性、帧提取的边界处理、以及经纬度这种特殊格式的换算。如果你手里有STM32F407的开发板或者是探索者V2/V3这个不确定的话直接看板子上的丝印和主芯片旁边是否有V2/V3标识就行又恰好想给自己的项目加上定位功能那这篇文章适合你从头到尾看一遍。哪怕你用的是其他型号的STM32串口部分的逻辑也是通用的只需要改一下时钟配置和引脚映射。1. 项目总体思路与协议选型分析1.1 为什么选择STM32F407做主控STM32F407这颗芯片在定位类项目里其实有点“杀鸡用牛刀”的意思但恰恰因为它的资源足够丰富才让整个工程的调试变得省心。首先是串口资源多F407有6个USART/UART。GPS模块占用一个串口用于收数据我们还可以留出另一个串口做调试打印两个互不干扰。这在开发阶段特别重要——你可以一边在串口助手里查看原始NMEA语句一边在另一个串口看解析后的结构化结果方便对照排查。如果换一颗串口资源少的芯片比如很多小封装型号只有两三个串口调试起来就会束手束脚。其次是主频和浮点运算能力F407主频168MHz带FPU硬件浮点单元。虽然解析NMEA报文本身用不到太多浮点运算但在做经纬度格式转换时会有小数的乘除运算。比如把ddmm.mmmm这种度分格式转成十进制度数公式是“度 分/60”涉及浮点除法和乘法。有了硬件FPU这类运算都是几个周期完成毫无压力。再有就是F407的生态和资料非常全不管是寄存器版还是HAL库版网上能查到的参考资料非常多。探索者开发板V2和V3的区别主要在于板载外设和走线布局核心芯片逻辑是一样的本文的工程代码在V2/V3上都可以直接使用。如果用的不是探索者板子只要你的F407最小系统板引出了USART1和USART2同样可以跑。1.2 NMEA-0183协议里究竟都有什么NMEA-0183是美国国家海洋电子协会制定的串行通信协议标准最早用于海洋电子设备之间的通信后来被广泛应用在GPS、北斗、GLONASS等卫星导航接收机的数据输出上。它的格式很简单每一条语句以$开头以\r\n结尾中间用逗号分隔各个字段。北斗GPS双模模块输出的语句一般包括GGA、RMC、GSV、GSA、VTG等类型。其中对我们普通用户最有用的两条是$GNRMC推荐最小定位信息包含时间、定位状态、经纬度、速度、航向、日期。一条语句几乎覆盖了所有核心数据。$GNGGA全球定位系统固定数据包含时间、经纬度、定位质量、卫星数量、海拔高度。适合用来获取定位质量指标和海拔。其他语句比如$GNGSV是可见卫星信息$GNGSA是精度因子和活跃卫星编号$GNVTG是地面速度矢量。这些语句在工程里可以解析但优先级相对低一般日志类应用才会用到完整解析。需要特别注意的是不同模块输出的语句前缀可能不一样。有的模块输出$GPRMC纯GPS或$BDRMC纯北斗有的输出$GNRMC双模。北斗GPS双模模块通常在双模工作模式下输出$GN开头。所以代码里做帧头匹配时最好不要只匹配固定前缀可以做一个通用匹配凡是类型字段为RMC或GGA的都接收。2. 硬件连接与开发环境准备2.1 北斗GPS模块的选型建议市面上常见的北斗GPS模块有ATGM336H、NEO-M8N、中科微的GNS3308等。我个人用得比较多的是ATGM336H原因很简单便宜、双模、串口直接输出NMEA语句、功耗低。它和NEO-M8N的引脚基本兼容都是串口TTL电平输出可以直接接STM32F407的USART引脚。选型时要确认三件事模块输出的电平是TTL还是RS232。绝大多数北斗GPS模块是TTL电平可以直接连单片机串口。如果是RS232电平的老模块需要额外加MAX232做电平转换。模块的默认波特率。大部分默认9600也有部分默认115200。这个参数必须和STM32串口初始化配置一致否则收到的全是乱码。模块的上电时间。冷启动状态下模块可能需要几十秒才能完成首次定位所以工程里要设计一个“等待定位成功”的处理流程不能一上电就期望立刻有有效数据。ATGM336H的电路很简单VCC接3.3VGND接地TXD接STM32的RX引脚RXD接STM32的TX引脚如果需要向模块发送配置命令的话。如果板上没有走线把模块的TXD和STM32的USART1_RX连在一起就需要自己飞线。注意模块的TXD接单片机的RX交叉连接这个方向接反是新手最容易犯的错误。2.2 串口引脚映射与时钟配置要点我用的引脚分配是USART1TX PA9RX PA10用于连接北斗GPS模块。USART2TX PA2RX PA3用于调试打印解析结果。用USART1接GPS是因为它挂在APB2总线上时钟最高可以到84MHz波特率配置误差更小。USART2挂在APB1上时钟42MHz调试打印完全够用。时钟树配置时需要特别留意APB1和APB2的时钟频率这直接决定了串口波特率寄存器BRR的写入值。STM32F407的时钟树不算复杂外部晶振8MHz通过PLL倍频到168MHz系统主频AHB预分频为1则HCLK为168MHzAPB1预分频为4则APB1外设时钟为42MHzAPB2预分频为2则APB2外设时钟为84MHz。串口波特率计算公式是BRR 外设时钟 / 波特率如果外设时钟配置错了波特率就是错的。比如USART1若错误地按42MHz计算9600波特率实际BRR写入4375但真实时钟是84MHz实际波特率会变成19200自然收不到正常数据。所以遇到串口乱码、收不到数据时优先检查外设时钟配置是否正确。我用STM32CubeMX生成初始化代码时会直接确认这两路外设时钟的具体数值心里有底了再往下写。3. 报文接收与解析核心实现3.1 串口数据接收为什么必须用中断缓冲区很多人刚开始做GPS解析时习惯用阻塞式接收也就是在while循环里不断调用HAL_UART_Receive。这种做法在简单验证时没问题但一旦你的系统里还有别的任务要跑——比如刷新OLED屏幕、处理按键、控制LED闪烁——就会很痛苦。模块每秒输出好几条语句每条语句几十到上百字节如果主循环一直在等串口数据其他任务就全卡死了。正确做法是串口接收中断 环形缓冲区。中断每收到一个字节就把数据放进缓冲区里主循环负责从缓冲区取数据做解析。这样串口接收不占用主循环时间解析也不会丢数据。环形缓冲区的实现不复杂就是一个数组加读写索引读索引和写索引相等时缓冲区为空。比较关键的点是缓冲区大小必须足够容纳模块一帧最多长度。NMEA语句最长的一般是GSV语句可能超过80字节RMC和GGA也有70字节左右。为了保险缓冲区建议开256字节甚至512字节。串口中断接收的HAL库写法是// 使能USART1接收中断 HAL_UART_Receive_IT(huart1, rx_data, 1);然后在中断回调里把数据写入环形缓冲区void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { uint8_t byte rx_data; // 写入环形缓冲区的空闲位置 uint16_t next (rx_buf.tail 1) % RX_BUF_SIZE; if (next ! rx_buf.head) { rx_buf.data[rx_buf.tail] byte; rx_buf.tail next; } // 重新使能接收中断 HAL_UART_Receive_IT(huart1, rx_data, 1); } }这里有一个细节在回调函数里再次调用HAL_UART_Receive_IT是因为HAL库接收完指定字节数后会自动关闭中断必须重新使能才能继续接收。如果忘了这一步串口只会收到第一个字节就再也不进了。这是我见过最多人踩的坑。3.2 帧提取从字节流中切出完整的一句环形缓冲区里的数据是连续的字节流我们需要从中找到一个个完整的NMEA语句。NMEA语句以$开头以\r\n结尾。所以帧提取逻辑可以设计成状态机初始状态等待$字符。找到$后开始把后续字符暂存到行缓冲区。每收一个字符都检查是否为\n或\r\n组合如果是则一行结束把这一行交给解析函数。重复上述过程。这个状态机的处理位于主循环里每轮循环检查缓冲区是否有新数据有则按字节处理。关键在于行缓冲区的长度要足够长比如100字节并且要防止溢出——如果收到了超过100字节还没见到结束符说明这一帧有问题直接丢弃重置避免脏数据污染后续解析。帧提取的伪代码逻辑如下// 从环形缓冲区取一个字节送入状态机 uint8_t byte; while (ring_buffer_read(rx_buf, byte)) { if (state STATE_WAIT_START) { if (byte $) { line_len 0; line_buf[line_len] byte; state STATE_RECEIVING; } } else if (state STATE_RECEIVING) { if (byte \n line_len 0) { line_buf[line_len] \0; parse_nmea_line(line_buf); // 解析这一行 state STATE_WAIT_START; } else if (line_len LINE_BUF_SIZE - 1) { line_buf[line_len] byte; } else { // 行长度溢出丢弃并重置 state STATE_WAIT_START; } } }3.3 字段解析完整实现RMC和GGA的提取拿到一行完整的NMEA语句后首先要判断语句类型。简单的方法是用strstr在字符串里查找RMC或GGA但更严谨的做法是检查第4和第5个字符。比如$GNRMC的字符序列是$GN后跟RMC当然也有$GPRMC或$BDRMC。为了兼顾不同模块输出的前缀差异我的做法是先找到第一个逗号的位置然后看逗号前面的内容是否以RMC结尾。RMC语句的结构是$GNRMC,hhmmss.sss,A,ddmm.mmmm,N,dddmm.mmmm,E,速度,航向,日期,磁偏角,磁偏角方向,A*校验和具体字段含义字段0UTC时间格式为hhmmss.sss。字段1定位状态A表示有效定位V表示无效。字段2纬度格式为ddmm.mmmm度分格式。字段3纬度方向N或S。字段4经度格式为dddmm.mmmm。字段5经度方向E或W。字段6地面速度单位为节。字段7地面航向单位为度。字段8UTC日期格式为ddmmyy。解析时最方便的是用strtok按逗号分割字段但要注意strtok会修改原字符串所以要先拷贝一份行数据再操作。我封装了一个简单的获取字段函数每次定位到第n个逗号和下一个逗号之间拷贝出字段字符串。这样代码更可控也不用担心strtok在中断里被调用虽然我这里解析放在主循环。经纬度的度分格式转换是重点。比如RMC字段里的纬度是3109.5623表示31度09.5623分。转成十进制度数的公式是十进制纬度 31 09.5623 / 60 31.1593716667注意分的小数部分是60进制不是100进制。这一步做错的话定位结果会偏出去好几公里。方向字符N/S决定纬度正负号北纬为正南纬为负E/W决定经度正负号东经为正西经为负。时间字段的处理也要细心。RMC里的时间是UTC时间北京时间比UTC快8小时需要加8小时换算。但注意不能简单地把“时分秒”数值加8因为小时可能溢出到24小时以上。正确的处理是转成秒再转换uint32_t total_seconds hour * 3600 minute * 60 second; total_seconds 8 * 3600; // UTC转北京时间 total_seconds % 86400; // 超过24小时则取模 uint8_t bj_hour total_seconds / 3600; uint8_t bj_min (total_seconds % 3600) / 60; uint8_t bj_sec total_seconds % 60;日期字段的格式是ddmmyy分别提取日、月、年注意年份只有两位通常加上2000转换成完整年份。下面是RMC语句解析的核心实现int parse_rmc(char *line, gps_info_t *gps) { // 定位状态 char *p line; // 跳过 $GNRMC 前导部分 // 逐个字段提取 char *field[13]; int field_cnt 0; char *token p; while (field_cnt 13 token ! NULL) { if (field_cnt 0) { token strchr(token, ,); if (token) { token; field[field_cnt] token; } } else { field[field_cnt] token; token strchr(token, ,); if (token) { *token \0; // 把逗号替换为字符串结束符 token; } } } if (field_cnt 10) return -1; // 定位状态 if (field[1][0] ! A) { gps-valid 0; return -1; } // 解析时间 hhmmss.sss uint32_t hh (field[0][0]-0)*10 (field[0][1]-0); uint32_t mm (field[0][2]-0)*10 (field[0][3]-0); uint32_t ss (field[0][4]-0)*10 (field[0][5]-0); // 解析纬度 ddmm.mmmm double lat_deg (field[2][0]-0)*10 (field[2][1]-0); double lat_min atof(field[2] 2); double latitude lat_deg lat_min / 60.0; if (field[3][0] S) latitude -latitude; // 解析经度 dddmm.mmmm double lon_deg (field[4][0]-0)*100 (field[4][1]-0)*10 (field[4][2]-0); double lon_min atof(field[4] 3); double longitude lon_deg lon_min / 60.0; if (field[5][0] W) longitude -longitude; // 解析速度节转公里每小时 double speed_knot atof(field[6]); gps-speed_kmh speed_knot * 1.852; // 解析日期 ddmmyy gps-day (field[8][0]-0)*10 (field[8][1]-0); gps-month (field[8][2]-0)*10 (field[8][3]-0); gps-year 2000 (field[8][4]-0)*10 (field[8][5]-0); gps-valid 1; return 0; }3.4 避免浮点开销的整型解析方案上面的代码用了atof把字符串转成浮点数在F407的FPU帮助下其实也没问题。但如果你用的芯片不带FPU或者你觉得atof在库函数调用上有开销可以用整型来解析经纬度。思路是把ddmm.mmmm的整数部分拆成“度”和“分”两个整型小数部分按毫分1/1000分来处理。比如纬度3109.5623度是31分是09分的毫分部分是5623。存储时用int32_t存一个“毫度”值毫度 度 * 1000 分 * 1000 / 60这样处理后整个工程就不涉及浮点运算了在低端MCU上也能跑。但代价是代码可读性下降我个人在F407上还是倾向直接用浮点毕竟硬件FPU不是白给的。不过了解整型方案有个好处如果将来要移植到8位MCU或者不带FPU的Cortex-M0上可以直接切换过去。4. 常见问题与排查技巧实录4.1 串口收到的全是乱码这个问题90%是波特率不匹配。先确认模块的默认波特率再确认程序里USART1的初始化波特率。两者一致才会正常。我遇到过一种特殊情况模块默认9600但因为在调试别的功能时写过配置命令让模块改成了115200重新上电后模块又恢复成默认配置。这时不要盲目怀疑程序先把模块的TXD直接用USB转TTL工具接到电脑串口助手看原始输出是什么波特率再回头改程序。另一个原因是时钟树配置错误。USART1和USART2挂在不同的APB总线上若总线上外设时钟算错BRR就会算错。检查CubeMX生成的SystemClock_Config确认APB1和APB2的外设时钟频率。注意USART1在APB2上USART2/3/4/5/6在APB1上这点经常被忽略。4.2 能收到数据但解析出来是空的这种情况一般是帧匹配出了问题。检查两点行缓冲区的指针传递是否正确是否在回调函数里意外修改了共享变量。环形缓冲区的读写指针建议定义为文件级静态变量并且只在主循环和中断里分别访问各自的索引。如果中断里写指针主循环里读指针两个索引的修改要保证原子性——在Cortex-M4上单字节写入是原子的但多字节操作可能会有中断竞争所以处理时可以先关中断取指针取完再恢复。检查模块输出的是\r\n还是只有\n。大多数模块是\r\n但有些模块如果配置了“只输出LF”模式你的状态机专门等\n也没问题注意不要在处理时把\r当成有效数据带进去否则后续字段解析会多一个不可见字符导致数字转换失败。4.3 位置信息时有时无信号强度不高这通常是环境问题而不是代码问题。GPS/北斗信号在室内几乎不可用在窗户旁边也要看朝向。拿到模块后先在室外空旷处验证确认能定位后再搬到室内调试代码。不要一开始就在室内怀疑程序写错了。如果室外能定位但数据断断续续可以检查模块的供电是否稳定。北斗GPS模块的峰值电流在信号搜索时会较高如果用开发板的3.3V供电且板上有其他大功率外设可能出现瞬时压降导致模块重启。解决方法是模块单独用LDO供电或者在模块VCC处并联一个100uF电解电容和0.1uF瓷片电容。4.4 经纬度输出在某个方向上偏得离谱先检查方向字符解析有没有错误。北纬是N南纬是S东经是E西经是W。如果方向字符判断反了经纬度的符号就错了。尤其在国内测试时北纬N和东经E应该都保持正值。如果解析出来纬度是负数或者经度是负数基本就是方向字符处理的问题。再看度分转换是否正确。我先给个小测试用例验证你的解析代码已知RMC里的纬度为2238.87521上海附近手动计算22 38.87521/60 22.64792017。如果代码输出结果偏差在1以上多半是度分处理时把小数点位置搞错了。4.5 接收数据时偶发丢失排查思路首先要看环形缓冲区是否有覆盖。缓冲区的大小决定了能缓冲多少字节。NMEA模块一般每秒输出多条语句如果主循环处理不过来导致缓冲区写满新数据就会被丢掉。可以把环形缓冲区大小设到512字节同时打印缓冲区溢出的计数如果溢出计数持续增长说明主循环解析速度跟不上需要优化解析代码或提高主循环执行频率。另外注意如果开了多个串口中断要看中断优先级是否合理。USART1的接收中断优先级应该设为不低于其他外设中断否则数据可能在中断嵌套期间被延迟处理。F407的NVIC配置很简单把USART1中断优先级设为抢占优先级2即可。4.6 校验和要不要认真做NMEA-0183规范里每条语句在末尾有*xx格式的校验和是$和*之间所有字符的逐字节异或值。有些模块这个校验值是正确的有些模块则可能省略或算错。我的建议是在正式工程里最好做校验和验证因为卫星信号弱时可能出现误码误码会导致语句格式正确但字段值错误。如果解析时完全不检查校验和错误数据就会混进来。对于做精准定位、地图记录、自动驾驶这类应用校验是必须的。对于普通demo和教学演示可以先不关注校验和等系统稳定后再补上。校验和计算的代码很简单uint8_t calc_nmea_checksum(const char *line) { uint8_t xor 0; // 跳过起始的 $ const char *p line 1; // 逐字符异或直到 * 或字符串结束 while (*p *p ! *) { xor ^ (uint8_t)*p; } return xor; }然后把语句中*后面的两个十六进制字符转换成数字与计算结果比较。不一致就丢弃该帧。5. 工程扩展与实用建议5.1 从“能解析”到“能用”的优化路径解析出经纬度只是第一步。实际工程里很多人会在解析完成后立刻用printf打印。这在调试期没什么问题但如果你的系统需要长时间运行并记录轨迹printf本身的开销和时间抖动会影响整体节奏。建议把解析结果存到结构体里由应用层决定怎么使用。比如在结构体里加一个update_flag每次成功解析RMC后置1主循环检测到标志位后统一处理显示、存储等操作处理完清0。这样可以避免GPS数据更新和应用层处理之间互相干扰。此外GGA语句里的定位质量指示字段6很有用0表示无效1表示单点定位2表示差分定位。可以根据这个字段判断当前定位的可靠性。如果值长期为0说明模块还没有锁定足够的卫星。在车载或户外项目中这个字段还可以用来切换工作模式——差分定位时记录精度更高单点定位时可以提醒用户位置可能有偏差。5.2 深入做产品时值得关注的几个方向如果你想把这套工程做成真正可交付的产品可以考虑以下几点一是低功耗设计。北斗GPS模块常供电会持续消耗电流。在电池供电的场景中可以让模块进入备份模式只有需要定位时才唤醒。STM32F407自身也有多种低功耗模式但要注意唤醒后的串口重新配置问题。二是多系统融合。目前很多双模模块还支持GLONASS或Galileo。NMEA-0183会为每个系统输出独立的GSV语句也可以通过配置让模块输出合并后的语句。融合多个星座能明显提升定位速度和室外的抗遮挡能力。三是数据存储。加一张SD卡用SPI或SDIO接口把解析后的位置数据记录成CSV或GPX格式就能做一个完整的轨迹记录仪。F407的SDIO接口速度足以支撑普通的数据记录需求。5.3 个人实操中的几点总结这套工程跑通之后我有几点比较深的体会。第一GPS解析项目的难点从来不在“解析”本身而在于数据链路每一个环节的可靠性。从天线信号、模块供电、串口电平、波特率配置、中断处理、缓冲区管理任何一个环节出问题都会以“解析不到数据”的形式呈现。排查时不要只盯着代码看先从物理链路开始逐级检查往往效率更高。第二NMEA-0183协议虽然老但就是因为它简单稳定才在几十年后的今天依然是卫星导航模块的标准输出格式。理解它的行结构、字段含义、度分转换规则对阅读各种模块的手册非常有帮助。不同模块输出的小差异比如字段偶尔为空、小数点位数不同、前缀不同都是很正常的代码要有容忍这些变化的能力。第三在实际项目中建议把所有NMEA原始数据先存日志再在日志基础上调试解析算法。这样即使复现不了现场环境也能靠日志完整模拟。我以前做定位设备时就是把原始NMEA通过调试串口传到PC上存成文本然后用Python脚本离线验证解析逻辑验证稳定后才把逻辑搬回C代码里大大节约了开发时间。这个项目本身不复杂但把协议解析、串口处理、数据格式化这些基础功夫练好了后面不管是做无人小车、定位追踪器还是便携导航设备思路都是一样的。希望这篇文章能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表