ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32与GPS/北斗模块:NMEA解析与DMA双缓冲TFT显示实战

2026/9/10 12:40:48 拓冰建站 浏览量
STM32与GPS/北斗模块:NMEA解析与DMA双缓冲TFT显示实战 简介面向STM32开发者这份DEMO例程源码演示了如何读写ATGM336H(GPS)模块并驱动3.5寸TFT液晶实时显示定位信息。代码基于HAL库完整覆盖NMEA协议解码、UTC转北京时间、经纬度格式换算等关键环节并采用DMA中断方式接收串口数据便于学习STM32外设与GPS模块的配合。压缩包共690个文件以C源码382个、头文件160个为主另含汇编文件、链接脚本、工程配置文件及hex固件等整体约7.04MB目录结构清晰可直接导入编译。源码涉及GPS初始化、NMEA协议解析、TFT液晶驱动与格式化显示注释较完整方便二次改动。目前已有3487人学习下载适合需要快速上手GPS数据解析与显示的中级嵌入式开发者作为设计参考。读者可从中获得一套可运行的工程模板、调试输出思路及液晶显示框架能有效缩短同类项目的开发周期。1. 从 ATGM336H 串口到 TFT 液晶:这套 demo 解决的核心问题把一块 ATGM336H 串口 GPS/北斗模块接到 STM32F4 开发板上,第一次上电最常见的结果不是“定位成功”,而是一整屏的 0.000000。别急着怀疑接线,先看这套 demo 在做什么:它通过串口 DMA 把模块输出的 NMEA 0183 原始语句收进缓冲区,再交给 nmealib 风格的解析器拆出经纬度、UTC 时间、速度和定位状态,最后同时推到 TFT 液晶与调试串口。对正在做车载定位、手持采集终端或者共享设备的人来说,这套代码的价值不只是“屏能亮”,而是把 UART DMA 双缓冲、字符串截断、时区偏移、经纬度度分转换这些容易翻车的细节提前趟平了。读完这一篇,你可以直接照着改出一个能用的定位显示节点。2. NMEA 0183 语句与硬件接线:先搞清楚数据长什么样,再谈解析2.1 从$GNGGA和$GNRMC拆出定位字段ATGM336H 是支持北斗与 GPS 的双模串口模块,上电后默认以 9600 波特率向外连续输出 NMEA 0183 语句。模块搜星后会同时输出$GNGGA、$GNRMC、$GNGSV等句子,前缀里的GN表示北斗与 GPS 联合定位;如果固件只搜到 GPS 卫星,也可能输出$GPGGA、$GPRMC。解析器不需要关心前缀,只需要按$开头、逗号分隔、*后跟校验和的格式逐条处理。以最常用的$GNRMC为例,一条有效定位语句大致长这样:$GNRMC,081536.000,A,3103.45678,N,12122.12345,E,0.86,42.30,030425,,,D*67从这条语句里能拿到:UTC 时间 08:15:36,定位状态 A(有效),纬度 3103.45678(N),经度 12122.12345(E),速度 0.86 节,航向 42.30 度,日期 2025 年 4 月 3 日。字段顺序固定,这正是解析库能工作的前提。下面的表格把这些关键字段列出来,方便对着调试串口逐一核对:字段序号示例值含义1081536.000UTC 时间,时:分:秒.毫秒2A定位状态,A有效,V无效33103.45678纬度,ddmm.mmmmm 格式4N北纬/南纬标识512122.12345经度,dddmm.mmmmm 格式6E东经/西经标识70.86地面速度,单位节842.30航迹角,单位度9030425UTC 日期:日/月/年在代码里,这些字段会解析进nmeaINFO结构体,经纬度以度分形式存放在info.lat和info.lon中,时间拆进info.utc的year、mon、day、hour、min、sec字段。2.2 引脚分配:3.3V 电平、交叉接线与共地ATGM336H 的串口电平是 3.3V 逻辑,而 STM32F4 的 USART 引脚同样工作在 3.3V,所以两者可以直接交叉连接:模块 TX 接 STM32 的 RX,模块 RX 接 STM32 的 TX。如果开发板上的串口引脚被板载 USB 转串口芯片占用,要先确认有没有冲突,否则会出现只能收不能发的现象。一个典型的接线表如下:ATGM336HSTM32F4说明VCC3.3V模块供电,不要接 5VGNDGND必须与 MCU 共地TXDPA10/USART1_RX模块发送给 MCURXDPA9/USART1_TXMCU 发送给模块,调试时可悬空PPSPB0(可选)秒脉冲输出,用于授时校准模块的 VCC 标称在 2.8V 到 3.6V 之间,直接挂到 STM32 的 3.3V 电源轨上没问题;如果开发板电源能力弱,建议单独用 LDO 给模块供电,避免 GPS 启动瞬间拉低 MCU 电压导致复位。天线接口通常是 IPEX 座或焊盘,接一块无源陶瓷天线放到窗户附近就能收到信号。2.3 为什么用 DMA 而不是串口中断逐字节接收GPS 模块虽然波特率只有 9600,但每秒会输出多行 NMEA 语句,如果每个字节都触发一次中断,在同时跑液晶刷新和浮点转换时会增加大量上下文切换开销。demo 的做法是把串口配置成 DMA 接收,让数据自行写入内存,等缓冲区填满一半或全部填满时再通知 CPU 处理。这样做还有一个好处:DMA 把一帧语句尽量完整地留在连续内存中,解析器可以直接扫描缓冲区,而不必用环形队列逐字节拼接。以半满/全满机制为例,缓冲区长度设为 512 字节时,前半段 256 字节可以被解析器先处理,同时 DMA 继续往后半段搬运数据,天然形成一种简化的双缓冲。一般建议把缓冲区设成 256 到 512 字节,因为一条完整的$GNGGA最长可能在 70 字节左右,256 字节足够容纳几条语句;如果后续还要解析$GNGSV这类卫星列表语句,512 字节更从容。缓冲区过大没有意义,只会浪费 SRAM。#define GPS_RBUFF_SIZE 512 #define HALF_GPS_RBUFF_SIZE 256 uint8_t gps_rbuff[GPS_RBUFF_SIZE]; volatile uint8_t GPS_HalfTransferEnd 0; volatile uint8_t GPS_TransferEnd 0; HAL_UART_Receive_DMA(husartx, gps_rbuff, GPS_RBUFF_SIZE);代码说明:husartx是接 GPS 模块的串口句柄,gps_rbuff是 DMA 搬运的目标缓冲区,GPS_RBUFF_SIZE是本次 DMA 搬运的总字节数。GPS_HalfTransferEnd和GPS_TransferEnd分别由半传输和全传输中断回调置位,主循环再消费。STM32F4 的 DMA 计数器是 16 位,单次传输最大长度 65535,512 字节完全在范围内;真正要留意的是缓冲区长度与回调里赋给nmea_parse的长度要保持一致,否则解析器会越界。3. STM32 端 NMEA 解析库的初始化与 DMA 双缓冲接收3.1 nmeaINFO 与 nmeaPARSER 各管什么demo 移植的是轻量级 nmealib 解析库。nmeaINFO info保存解析结果,包括经纬度、UTC 时间、速度、航向、定位状态等;nmeaPARSER parser保存解析器内部状态,例如当前正在拼接的语句、字段索引和校验和。解析过程就是不断把字符喂给parser,由它拆出字段并写入info。这个库的核心不是“有没有字符串匹配”,而是把一行 NMEA 拆成nmeaINFO能容纳的紧凑结构,便于后续直接使用。这里有一个容易被忽略的点:nmeaINFO声明后不会自动清零。demo 调用nmea_zero_INFO(info);把字段全部初始化为 0,这样未定位时屏上显示的是全零,而不是随机内存值。nmea_parser_init(parser);则重置解析器缓冲区,避免上一条被截断的语句影响下一条。nmeaINFO info; /* GPS 解码后得到的信息 */ nmeaPARSER parser; /* 解码时使用的数据结构 */ uint8_t new_parse 0; nmea_zero_INFO(info); nmea_parser_init(parser); nmea_property()-trace_func trace; nmea_property()-error_func error; nmea_property()-info_func gps_info;上面nmea_property()是解析库的全局属性接口,trace_func、error_func、info_func分别是指向跟踪、错误和原始信息输出的回调。正式发布时可以把这三个回调置空,减少通过 printf 输出带来的额外开销;但在调试阶段保留它们,能看到解析器对每条语句的处理结果。info中常用字段见下表:字段类型说明info.latdouble纬度,ddmm.mmmmm 度分格式info.londouble经度,dddmm.mmmmm 度分格式info.utcnmeaTIMEUTC 时间,含年月日时分秒info.speeddouble速度,单位节info.directiondouble航向,单位度info.sigint信号类型:无、普通定位、差分定位info.fixint定位状态:0 未定位,1 已定位,2 差分定位注意sig与fix在不同版本 nmealib 中定义不完全一致,工程中判断“能不能用”时最好同时看fix和当前可见卫星数,不要只依赖某一个字段。3.2 DMA 半满/全满回调只置标志,不做解析HAL_UART_Receive_DMA(husartx,gps_rbuff,GPS_RBUFF_SIZE);启动后,DMA 会持续把串口接收到的数据搬进gps_rbuff。传输一半时触发半传输中断,HAL 库回调HAL_UART_RxHalfCpltCallback;整个缓冲区填满时回调HAL_UART_RxCpltCallback。demo 没有在这两个回调里直接调用nmea_parse,原因是解析过程含浮点运算,放在中断里会阻塞后续串口接收,尤其在波特率提高后更容易丢字节。正确写法是只置标志:void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance GPS_USART_INSTANCE) { GPS_HalfTransferEnd 1; /* 标记前半段可解析 */ } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance GPS_USART_INSTANCE) { GPS_TransferEnd 1; /* 标记后半段可解析 */ } }这里通过huart-Instance判断中断来自哪个串口,避免与调试串口的收发回调相互干扰。标志位在中断里置 1、主循环消费后清 0,是一种典型的“中断通知、主循环处理”模式。这里有一个前提:gps_rbuff对应的 DMA 通道必须配置为循环模式(DMA_CIRCULAR),否则全满中断后 DMA 自动停止,串口后续数据不会再进入缓冲区。使用 STM32CubeMX 配置时,在 DMA Settings 里将 Mode 选为 Circular;如果使用寄存器或 HAL 手动初始化,也要把hdma_usartx_rx.Init.Mode设为DMA_CIRCULAR。这是这套数据流能连续工作的关键,若误配成 Normal 模式,只会收到最开始的一块数据,屏上时间刷新一次后就不动了。3.3 主循环消费标志位,把缓冲区两段交给解析器主循环while(1)里先查GPS_HalfTransferEnd,再查GPS_TransferEnd,两个分支都调用nmea_parse,只是数据指针和长度不同:if (GPS_HalfTransferEnd) { nmea_parse(parser, (const char*)gps_rbuff[0], HALF_GPS_RBUFF_SIZE, info); GPS_HalfTransferEnd 0; new_parse 1; } else if (GPS_TransferEnd) { nmea_parse(parser, (const char*)gps_rbuff[HALF_GPS_RBUFF_SIZE], HALF_GPS_RBUFF_SIZE, info); GPS_TransferEnd 0; new_parse 1; }nmea_parse的四个参数分别是解析器、缓冲区起始地址、本次要解析的字节数、结果写入的nmeaINFO。半满中断先触发时,前半段数据已经连续稳定,可以先解析;全满中断触发时,后半段同样完整。这样半个缓冲区的大小决定了每次解析的粒度。需要说明的是,nmea_parse是流式解析,如果一条语句恰好被缓冲区边界切断,库内部会暂存未完成的内容,等下一块数据补全,所以new_parse只表示“有新数据块”,不保证一定有新的定位结果。4. 经纬度转换、北京时间修正与液晶显示4.1 把度分格式转成十进制度:公式和代码NMEA 语句里的3103.45678表示 31 度 3.45678 分,而不是 31.0345678 度。如果要在地图或屏幕上显示十进制度,必须换算。demo 中deg_lat和deg_lon这两个double变量就是用来接收转换结果的。换算公式:度分值除以 100 取整数部分得到度,余下的小数部分是分,分再除以 60 变成度。实现如下:double nmea_to_degree(double nmea_coord) { int degree (int)(nmea_coord / 100); /* 取度 */ double minute nmea_coord - degree * 100; /* 剩余的分 */ return degree minute / 60.0; /* 十进制度 */ } deg_lat nmea_to_degree(info.lat); deg_lon nmea_to_degree(info.lon); /* 若解析库用负值表示南纬/西经,这里再补符号 */ if (info.lat 0) deg_lat -deg_lat; if (info.lon 0) deg_lon -deg_lon;代码说明:第一行强制把nmea_coord / 100转成int,是把度数部分截出来;第二行用原值减去度乘以 100,得到分;第三行把分转成度的小数。NMEA 标准中南北纬/东西经由字母标识,但 nmealib 的info.lat、info.lon在不同版本里可能用负号表示半球,所以最后两个if用于兼容这种约定。移植时最好先拿一条已知坐标的 NMEA 语句验证符号方向。4.2 UTC 转北京时间:GMTconvert 的时区参数GPS 模块内部用的是 UTC 时间,比北京时间晚 8 小时。demo 里调用GMTconvert(info.utc, beiJingTime, 8, 1);将解析出的 UTC 时间平移 8 小时,并写入beiJingTime。第 3 个参数8是时区偏移,第 4 个参数传1表示启用日期进位修正。如果跳过这个函数,或者只给hour加 8,晚上 20 点以后的数据会变成第二天 4 点,日期和星期全部错位。有一个打印细节:nmeaTIME结构体里的年份通常保存的是“相对于 1900 的偏移”,所以输出时需要写beiJingTime.year 1900,否则屏幕上会显示一个如 125 的年份。同理,info.utc.year也是同样的规则。4.3 TFT 液晶显示哪些内容demo 初始化 LCD 后先LCD_Clear清屏,再开背光,接着把解析结果按行显示。由于屏幕驱动库不同,显示函数名可能不一样,但核心是把字符串格式化好后写进坐标。一个常见写法是:char cStr[100]; sprintf(cStr, Time:%02d:%02d:%02d, beiJingTime.hour, beiJingTime.min, beiJingTime.sec); LCD_ShowString(20, 20, cStr, WHITE, BLACK); sprintf(cStr, Date:%04d-%02d-%02d, beiJingTime.year 1900, beiJingTime.mon, beiJingTime.day); LCD_ShowString(20, 50, cStr, WHITE, BLACK); sprintf(cStr, Lat:%0.6f, deg_lat); LCD_ShowString(20, 80, cStr, WHITE, BLACK); sprintf(cStr, Lon:%0.6f, deg_lon); LCD_ShowString(20, 110, cStr, WHITE, BLACK);LCD_ShowString的最后一个参数是背景色,如果清屏后是黑色,背景色传BLACK可以避免文字叠加形成阴影。%0.6f会输出 6 位小数,大约对应 0.1 米的精度级别,显示经纬度足够。需要注意的是,嵌入式sprintf如果启用浮点%f,程序体积会明显增大。Keil 环境下一般勾选 Use MicroLIB,或者自己把 double 放大10^6转成整数再拼接,否则 flash 不够时链接阶段会报错。TFT 上推荐的字段布局如下:显示行字段格式化示例第 1 行定位状态Fix: A或Fix: V第 2 行北京时间Time: 08-15-36第 3 行日期Date: 2025-04-03第 4 行纬度Lat: 31.05761第 5 行经度Lon: 121.37062第 6 行速度/航向Spd: 0.86kn 42.3deg4.4 定位无效时的显示策略模块冷启动阶段fix为 0,经纬度持续输出 0.000000,如果把 0 显示在屏幕上,用户会误以为解析坏了。实际项目里应该先判断info.fix:有效时把状态字母设成A,无效时显示V并把经纬度区域显示为--或留空。调试串口那边同样只在fix有效时打印详细值,无效时只打印一条简短的等待信息,避免刷屏。还要注意printf的中文输出问题。例程里printf(\r\n时间%d-%02d-%02d...)如果通过调试串口发送到串口助手,中文编码在部分工具里会乱码。建议把格式串改成英文Time:;若必须用中文,把串口助手设置为 UTF-8 解码,或改用UART_WriteString直接输出字节流。5. 定位精度、天线选型与几个常见坑:让 demo 真正可用5.1 冷启动等待时间与 GPS 周数翻转补丁ATGM336H 常温下冷启动首次定位一般需要 30 秒到 1 分钟,放在室内窗口附近可能更久。如果屏上长时间显示Fix: V,先确认天线是否在可见天空范围,而不是立刻怀疑代码。另一个隐藏问题是 GPS 周数翻转:部分旧固件在 2019 年 4 月后会把日期解析到 1999 年,因为 NMEA 协议中的周数只有 10 位,约 19.7 年翻转一次。判断方法很简单:看beiJingTime.year 1900是否明显小于当前年份,若是,说明模块固件需要打周数翻转补丁。5.2 无源陶瓷天线、有源天线与 ESD 防护模块上的 IPEX 天线座可以接无源陶瓷天线或有源天线。无源天线体积小、成本低,适合外壳开窗;有源天线内置低噪声放大器,增益更高,适用于金属外壳或线缆较长的场景。如果改用有源天线,要确认模块是否已有天线馈电引脚,没有的话需要从 3.3V 串一个磁珠或电感给天线座供电,同时注意电流不要超过模块手册的限制。不管用哪种天线,天线座都处于板边,RF 通路容易遭受静电放电冲击,建议在 IPEX 座附近预留 TVS 管;这与 TFT-LCD 液晶显示模组的 ESD 防护思路类似,液晶排线入口也要加钳位保护,否则在干燥环境下插拔排线可能损坏主控或显示驱动。5.3 用原始数据验证解析链路遇到“屏幕不动”的情况,不要急着改代码,先按顺序排除:把模块 TXD 断开,用 USB-TTL 直接连接电脑串口助手,看有没有$GNRMC输出;没有输出则问题在供电、天线或模块本身。如果模块有输出,再把 TXD 接回 STM32,在回调里增加一个计数器,每次GPS_HalfTransferEnd置位就加一,把计数值通过调试串口发出来。计数器不增长,说明 DMA 配置或中断优先级错误;计数器增长但new_parse始终为 0,则重点检查GPS_RBUFF_SIZE与宏定义是否一致,以及半满中断是否被无关代码意外关闭。5.4 把显示刷新频率限制在 1Hz我习惯在new_parse处理块里加一个gps_show_tick变量,只有距离上次刷新超过 1000ms 时才去刷新 TFT 和串口打印。这样即使模块更新频率从 1Hz 调到 5Hz,屏幕也不会闪烁,sprintf的浮点格式化开销也不会拖慢主循环。这套代码剩下的框架不用改,只把显示节奏放慢,整机的稳定性能上一个台阶。本文还有配套的精品资源点击获取