news 2026/9/10 12:40:44

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.latinfo.lon中,时间拆进info.utcyearmondayhourminsec字段。

2.2 引脚分配:3.3V 电平、交叉接线与共地

ATGM336H 的串口电平是 3.3V 逻辑,而 STM32F4 的 USART 引脚同样工作在 3.3V,所以两者可以直接交叉连接:模块 TX 接 STM32 的 RX,模块 RX 接 STM32 的 TX。如果开发板上的串口引脚被板载 USB 转串口芯片占用,要先确认有没有冲突,否则会出现只能收不能发的现象。一个典型的接线表如下:

ATGM336HSTM32F4说明
VCC3.3V模块供电,不要接 5V
GNDGND必须与 MCU 共地
TXDPA10/USART1_RX模块发送给 MCU
RXDPA9/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_HalfTransferEndGPS_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_funcerror_funcinfo_func分别是指向跟踪、错误和原始信息输出的回调。正式发布时可以把这三个回调置空,减少通过 printf 输出带来的额外开销;但在调试阶段保留它们,能看到解析器对每条语句的处理结果。

info中常用字段见下表:

字段类型说明
info.latdouble纬度,ddmm.mmmmm 度分格式
info.londouble经度,dddmm.mmmmm 度分格式
info.utcnmeaTIMEUTC 时间,含年月日时分秒
info.speeddouble速度,单位节
info.directiondouble航向,单位度
info.sigint信号类型:无、普通定位、差分定位
info.fixint定位状态:0 未定位,1 已定位,2 差分定位

注意sigfix在不同版本 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_latdeg_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.latinfo.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: AFix: V
第 2 行北京时间Time: 08-15-36
第 3 行日期Date: 2025-04-03
第 4 行纬度Lat: 31.05761
第 5 行经度Lon: 121.37062
第 6 行速度/航向Spd: 0.86kn 42.3deg

4.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的浮点格式化开销也不会拖慢主循环。这套代码剩下的框架不用改,只把显示节奏放慢,整机的稳定性能上一个台阶。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 12:38:20

Cal.diy 部署在反向代理后面出现 SSL 证书错误怎么排查?

Cal.diy 部署在反向代理后面出现 SSL 证书错误怎么排查&#xff1f; 【免费下载链接】cal.diy Scheduling infrastructure for absolutely everyone. 项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy 当你把 Cal.diy&#xff08;一个自托管的日程安排应用&am…

作者头像 李华
网站建设 2026/9/10 12:37:40

收藏!5个面试题帮你找到真正懂AI落地的程序员(小白也能看懂)

本文通过作者在AI招聘中的经验&#xff0c;分享了五个关键的面试问题&#xff0c;帮助识别真正具备AI落地能力的候选人。文章强调&#xff0c;AI项目成功的关键在于解决真实业务问题&#xff0c;而非堆砌技术框架。作者提出的五个问题分别关注业务问题解决、个人项目责任、AI应…

作者头像 李华
网站建设 2026/9/10 12:34:30

Ruffle 桌面版使用指南:拖一个 SWF 进去,老 Flash 就跑了

Ruffle 桌面版使用指南&#xff1a;拖一个 SWF 进去&#xff0c;老 Flash 就跑了 【免费下载链接】ruffle A Flash Player emulator written in Rust 项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle 硬盘里存了多年的 .swf 游戏&#xff0c;在 2020 年底 Fla…

作者头像 李华