资讯动态

STM32实战:ATGM336H北斗GPS模块从串口解析到液晶显示全流程

发布时间:2026/9/10 12:51:04 来源:尧图企业网站定制
简介面向STM32开发者的ATGM336H模块读写DEMO基于HAL库实现完整NMEA协议解析、UTC转北京时间、串口DMA接收与TFT液晶显示。工程代码结构清晰主循环中通过半满/全满DMA中断轮询解析GPS数据并输出经纬度、时间等定位信息适合需要快速上手GPS模块与液晶联动开发的电子爱好者、嵌入式初级开发者。压缩包共690个文件以C源文件、H头文件、启动汇编文件和ICF链接脚本为主并含HEX、AXF等可直接烧录的固件及工程配置文件整体7.04MB目录完整便于移植到其他STM32型号。目前已有3487人学习参考可借助该例程理解GPS数据从串口接收、解码到屏幕显示的完整链路同时掌握HAL库外设初始化、DMA传输和NMEA库调用等实用技巧。1. 从ATGM336H的串口到液晶屏这条路比想象中长第一次接触ATGM336H大多数人会把它当成国产版的NEO-6M——接上USB-TTL看串口输出满屏滚$GNGGA、$GNRMC觉得模块已经工作了。等把模块真正接到STM32上才发现事情没那么顺模块默认9600波特率网上下载的解析例程却常常初始化成115200网上现成的代码只认$GPGGA而ATGM336H双模输出是$GNGGA进不了解析分支等这些都改完屏幕刚显示出经纬度又发现偶尔夹着半个字节的帧会把整条数据作废画面跳一下就不再刷新。这篇内容从ATGM336H的硬件连接、NMEA协议解析、液晶显示集成写到联调与进阶最终交付一条能在本地跑通的完整链路适合正在调车机屏、农业设备、便携定位终端的人直接复用。2. ATGM336H接线与STM32串口接收先把数据可靠接进来2.1 引脚定义与最小接线ATGM336H是基于中科微AT6558芯片的导航定位模块支持GPS/北斗双模默认通过UART输出NMEA 0183协议数据。模块不同批次的小板引脚排列略有差异但核心引脚是固定的先列一张最小接线表。模块引脚方向接STM32端说明VCC输入3.3V工作电压范围约2.8V~4.3V常规按3.3V供电GND输入GND与单片机共地TXD输出USART1_RX(PA10)模块发送接单片机接收RXD输入USART1_TX(PA9)只读定位数据时可不接PPS输出PA0(TIM2_CH1)秒脉冲输出接定时器输入捕获可做时间对齐V_BCKP输入可选接备份电池或超级电容保存星历加快热启动TXD和RXD交叉连接是串口的基本规则最容易犯的错是两头直连直接导致收不到数据。模块的RXD只在改配置、升级固件时才需要接跑DEMO只读定位数据时悬空即可。V_BCKP如果方便建议接一个1.5V到3V的纽扣电池或法拉电容模块掉电后仍能保存星历和RTC时间二次开机定位时间能明显缩短这个接口在大多数开发板上容易忽略。2.2 供电、电平与天线注意事项ATGM336H的逻辑电平是3.3V和STM32的USART直接连接没有问题。如果你的主控是5V供电的51或者树莓派电平不一致TXD输出方向需要做电平转换再进MCU不能图省事直接直连。模块板载LNA外部天线建议选用有源或无源陶瓷天线天线净空区不要被覆铜包围这点直接影响搜星数量。关于供电还有一条容易踩的坑多数小板没有反接保护和稳压5V直接接VCC存在风险除非模块PCB上明确标了5V输入支持否则不要赌。看丝印比看商品描述可靠。2.3 9600波特率下的UART中断接收ATGM336H出厂默认96008位数据位、1位停止位、无校验。STM32F103的USART外设配9600波特率时APB时钟48MHz或72MHz下分频都没有取整误差可以放心用。接收方式我建议用“单个字节中断收环形缓冲”而不是每行数据触发一次DMA空闲中断——9600波特率下每字节约1ms字节中断完全扛得住而DMA空闲中断在模块频繁输出GSV语句时需要考虑接收超时时间设置反而容易漏包。#define RX_BUF_SIZE 512 static uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint16_t rx_head 0; static volatile uint16_t rx_tail 0; static uint8_t rx_byte; void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 9600; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart1) ! HAL_OK) Error_Handler(); HAL_UART_Receive_IT(huart1, rx_byte, 1); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buf[rx_head] rx_byte; rx_head (rx_head 1) % RX_BUF_SIZE; HAL_UART_Receive_IT(huart1, rx_byte, 1); } }接收回调里把每字节压入环形缓冲然后立刻重新使能接收中断保证下一个字节到来前中断已经挂上。rx_head记录写入位置rx_tail由解析端消费后推进取模操作让缓冲区变成一个环任何一端的读写都不会越界。环形缓冲的好处是解析逻辑不需要逐字节等待可以在主循环里批量取数据互不阻塞。注意用这个方案时必须先在CubeMX里把USART1的全局中断打开否则HAL_UART_Receive_IT永远不会被触发。另外rx_byte必须是全局变量局部变量在中断回调返回后会失效。3. NMEA协议解析从$GNGGA里取经纬度与定位质量3.1 GPGGA与GNGGA到底差在哪NMEA 0183协议里GGA语句是“固定定位数据”包含时间、经纬度、定位状态、卫星数、HDOP和海拔。老一代GPS模块NEO-6M、VK2828等只输出GPGGA因为只认GPS星座ATGM336H在GPS北斗双模开启时输出GNGGA前缀GN表示多星座融合。很多移植失败的DEMO就是死在strncmp(line, $GPGGA)上判断永远不成立解析逻辑成了死代码。所以拿到ATGM336H第一步看原始语句开头是GPGGA还是GNGGA再决定匹配前缀。如果模块被配置成单GPS模式输出会回到GPGGA代码里两个前缀都匹配是最稳妥的写法。3.2 行缓冲与校验和避免半个帧破坏整包数据NMEA每行以$开头\r\n结尾行尾紧跟着*xx两位十六进制校验和。不做校验直接按逗号切字段一旦UART丢字节整条数据会错位轻则显示乱值重则把上一行的残留数据带进下一帧。常见做法是在串口接收回调里攒行遇到\n认为一行结束然后进解析。攒行过程要处理\r和缓存溢出。static char nmea_line[128]; static uint8_t nline_len 0; void NMEA_PushChar(char c) { if (c $) { nline_len 0; nmea_line[nline_len] c; } else if (c \n) { nmea_line[nline_len] \0; NMEA_ParseLine(nmea_line, nline_len); nline_len 0; } else if (nline_len sizeof(nmea_line) - 1) { if (c ! \r) nmea_line[nline_len] c; } } static const char *nmea_field(const char *line, int idx) { int i 0; while (line *line i idx) { if (*line ,) i; line; } return line; } static uint8_t nmea_checksum_ok(const char *line) { const char *star strchr(line, *); uint8_t calc 0, given 0; const char *p; if (!star || (star - line) 120) return 0; for (p line 1; p star; p) calc ^ (uint8_t)(*p); if (star[1] 0 star[1] 9) given (star[1] - 0) 4; else if (star[1] A star[1] F) given (star[1] - A 10) 4; if (star[2] 0 star[2] 9) given | (star[2] - 0); else if (star[2] A star[2] F) given | (star[2] - A 10); return calc given; }NMEA_PushChar每收到一个字节就调用一次由状态决定当前是哪一帧。nmea_field返回第idx个逗号之后的字段起始指针注意如果字段个数不足它会指向行尾的*之前调用方需要自己判断内容是否合法。nmea_checksum_ok遍历$与*之间的所有字符做异或再把*后的两位十六进制读数与计算结果比较不一致的帧直接丢弃。校验是成本极低、回报极高的一道过滤器GPS在车辆、无人机上使用时振动、干扰造成的串口误码率并不低省掉校验等于把错误数据放进显示层。3.3 GGA关键字段解析与经纬度换算GGA语句常见格式如下字段按逗号编号$GNGGA,080531.000,3113.2738,N,12122.6909,E,1,12,1.0,19.3,M,12.1,M,,*5E字段编号含义示例值1UTC时间时-分-秒.毫秒080531.0002纬度度分格式3113.27383北纬/南纬N4经度度分格式12122.69095东经/西经E6定位状态0未定位1单点定位2差分定位17使用卫星数128HDOP水平精度因子1.09海拔高度米19.3解析时时间、经纬度、状态、星数、HDOP这五个值是显示层最关心的。经纬度在NMEA里是度分格式比如3113.2738表示31度13.2738分要转成小数度数再参与计算static double parse_latlon(const char *field) { double v atof(field); int deg (int)(v / 100.0); double minutes v - deg * 100.0; return deg minutes / 60.0; } void GGA_Parse(const char *line) { const char *f; if (!nmea_checksum_ok(line)) return; if (strncmp(line, $GNGGA, 6) ! 0 strncmp(line, $GPGGA, 6) ! 0) return; f nmea_field(line, 1); sscanf(f, %2hhu%2hhu%lf, gps.utc_hh, gps.utc_mm, gps.utc_ss); f nmea_field(line, 2); gps.lat parse_latlon(f); f nmea_field(line, 3); if (*f S) gps.lat -gps.lat; f nmea_field(line, 4); gps.lon parse_latlon(f); f nmea_field(line, 5); if (*f W) gps.lon -gps.lon; f nmea_field(line, 6); gps.fix atoi(f); f nmea_field(line, 7); gps.sat_num atoi(f); f nmea_field(line, 8); gps.hdop atof(f); }s scanf里的%lf在STM32的MicroLib和标准C库下都能正常工作每秒解析一次的开销对F103来说可以忽略。数字转成double后显示用%.6f格式化计算距离时直接参与反正切、余弦运算。这里不建议用纯整数保存度分除非你要跑10Hz以上的高更新率否则浮点更直观调试也省时间。3.4 UTC时间、日期与GPS周数翻转GGA里只有时间没有日期日期要从RMC语句里取。RMC倒数第三个字段是日日月年年格式比如080531表示2024年5月31日“31.05.24”。void RMC_Parse(const char *line) { const char *f; if (strncmp(line, $GNRMC, 6) ! 0 strncmp(line, $GPRMC, 6) ! 0) return; f nmea_field(line, 9); if (strlen(f) 6) { gps.utc_day (f[0] - 0) * 10 (f[1] - 0); gps.utc_mon (f[2] - 0) * 10 (f[3] - 0); gps.utc_year (f[4] - 0) * 10 (f[5] - 0) 2000; } }中国地区的应用通常要把UTC时间加8小时。直接加8会碰到日期滚动简单处理是先转成绝对分钟再换算或者让时分秒各自加8后做进位回退。DEMO里我建议把“年月日时分秒”整体转成Unix时间戳再加28800秒再拆回本地时间这样逻辑最不容易出错。GPS周数翻转在2024年11月3日已经发生过一次GPS周计数器是10位到1024周会回零。ATGM336H的固件如果太老翻转后可能出现时间跳变、定位数据异常。遇到这种情况把模块固件升级到支持WNO周数翻转处理的版本或者直接联系供应商要最新固件。别等项目上线后时间突然错乱再排查这种故障非常隐蔽。4. 液晶显示把定位数据渲染成每秒刷新的一屏4.1 屏幕接口选择与驱动移植边界液晶屏这块标题里的DEMO通常用SPI接口的屏常见三种SSD1306驱动的0.96寸OLED、ST7735驱动的1.8寸TFT、ILI9341驱动的2.4寸或2.8寸TFT。它们之间的移植差异主要是初始化寄存器和坐标范围渲染接口可以抽象成同一套。SPI接线只有五根SCLK、MOSI、DC数据/命令、CS、RST算上背光和电源共七个引脚。STM32F103C8T6跑SPI1最大分频后时钟约18MHz刷128x64的OLED绰绰有余。ILI9341这类大屏建议用SPIDMA否则整屏刷新会占用MCU较多时间。显示驱动我建议只保留五个API上层逻辑完全不知道底层是什么屏void LCD_Init(void); void LCD_Clear(uint8_t color); void LCD_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t color); void LCD_DrawChar(uint16_t x, uint16_t y, char c, uint8_t color); void LCD_DrawString(uint16_t x, uint16_t y, const char *s, uint8_t color);SSD1306需要把整屏内容放在一块128x64的显存里调用DrawChar时修改显存对应区域再通过LCD_Refresh()把显存刷到屏幕ST7735和ILI9341自带GRAM画点函数直接写坐标不需要额外的Refresh。所以LCD_Refresh对OLED是必须动作对TFT是空函数移植时用宏控制即可。4.2 渲染层的抽象与字符/汉字显示英文字符可以用8x16点阵自己生成一个const uint8_t font8x16[95][16]数组每个字符占16字节。汉字常用16x16点阵一个汉字32字节。DEMO里如果只显示“定位中”“经度”“纬度”“卫星数”几个固定词把汉字按字模数组存就行不用整套字库省Flash。void LCD_DrawChar(uint16_t x, uint16_t y, char c, uint8_t color) { uint8_t i, j; const uint8_t *glyph font8x16[c - 32]; for (i 0; i 16; i) { for (j 0; j 8; j) { if (glyph[i] (0x80 j)) LCD_DrawPixel(x j, y i, color); } } }c - 32把ASCII映射到数组下标因为空格ASCII是32。点阵字模的每个bit对应一个像素高位在前。如果嫌自己提取字模麻烦也可以用PCtoLCD2002或Image2Lcd导出C数组注意设置纵向取模、字节正序否则字会左右颠倒或上下颠倒。汉字显示原理相同只是字符宽度从8像素变成16像素DrawString里遇到ASCII和汉字时按不同宽度推进光标位置。不想折腾字库的可以集成μGUI这类轻量GUI库它有全套控件但为了显示经纬度这种纯文本自绘反而更快。4.3 每秒局部刷新别整屏清空GPS模块每秒输出一组数据屏幕跟着每秒刷新一次。最容易犯的错是每帧先LCD_Clear再画全部文字清屏时屏幕会明显闪一下尤其在TFT上大面积的白色闪屏很刺眼。正确做法是画每个字段前只清掉这个字段所在的矩形区域。坐标值的变化集中在低位数字理论上可以只刷新变化区域但工程上按“字段矩形”清一次最省心。void Gps_Display(void) { char buf[32]; static uint8_t last_fix 0xFF; if (!gps.updated) return; gps.updated 0; snprintf(buf, sizeof(buf), %02d:%02d:%02d, gps.utc_hh, gps.utc_mm, (uint8_t)gps.utc_ss); LCD_FillRect(40, 8, 60, 16, 0); LCD_DrawString(40, 8, buf, 1); if (gps.fix ! last_fix) { LCD_FillRect(0, 32, 128, 16, 0); if (gps.fix) LCD_DrawString(0, 32, FIX OK, 1); else LCD_DrawString(0, 32, NO FIX, 1); last_fix gps.fix; } snprintf(buf, sizeof(buf), %d sat, gps.sat_num); LCD_FillRect(88, 56, 40, 8, 0); LCD_DrawString(88, 56, buf, 1); }这个函数的巧妙点在last_fix变量——定位状态从无到有、从有到无属于低频事件只在变化时重画状态行避免每秒都做无意义的清写。经纬度值即使每秒都在变也只清字段所在的小矩形不动其他区域。这里还有一个并发隐患UART中断在往gps结构体写数据主循环在读如果发生在赋值中间屏幕上可能出现半个旧值半个新值。DEMO里通常能容忍但如果要做严谨一些在解析函数尾部设置updated前用__disable_irq()包住几行快照复制或者把解析结果先写到临时结构体解析完再整体拷贝两种办法都能保证数据一致性。5. 联调排错从“no target found”到坐标纹丝不动5.1 下载、驱动与调试器问题STM32F103的DEMO工程最常见的错误是编译通过但下载失败Keil报error: no stm32 target found!。这个提示的意思是调试器和目标芯片没有建立连接排查顺序是先看ST-LINK的SWDIO、SWCLK、GND三根线有没有接对再看目标板是否上电。STM32F103板子如果连在别的系统上供电SWD接口地线没共地一样识别不到。还有一类情况是ST-LINK识别到目标但下载后没反应检查Keil的Debug设置里Flash Download里有没有勾选Reset and Run不勾选的话程序下载完不自动复位看起来像没烧进去。ST-LINK的虚拟串口在设备管理器里显示叹号去装STSW-LINK009驱动另外注意很多USB线只供电不通数据换线测试是最快的排除手段。5.2 原始NMEA流的五个检查点屏幕没数据时不要急着改解析代码先确认串口原始数据是不是真的在来。直接在STM32上通过printf往调试串口转发或者在PC上用USB-TTL接模块看原始输出。screen /dev/ttyUSB0 9600逐个检查下面五项$到\n的间隔是否接近一秒一条如果间隔不是整秒说明模块更新率被改过比如被配置成5Hz。语句前缀是不是GNGGA如果是GPGGA说明模块处于单GPS模式解析匹配要同时兼容。*后面的两位校验和是否与前面的异或结果一致如果不一致要么波特率不对导致字节错位要么信号线受到干扰。定位状态字段是否为1保持0说明还没固定跟解析无关是天线问题。时间字段是否每秒钟递增时间不动但语句在滚卫星数和HDOP也是满的那大概率模块时间基准卡死重启模块或检查PPS引脚状态。有一种隐蔽情况是模块曾经被配置成非9600波特率比如卖家发货前调成了115200。看到串口助手里乱码先试试115200、38400、4800这几个常见值确认后让STM32和它对齐或者在PC端发送恢复出厂配置指令。5.3 定位慢与GPS误差的现场判断ATGM336H冷启动通常几十秒静止在室内窗边时首次定位可能拖到几分钟这属于正常情况不是硬件坏了。拿到板子先别在办公室内测到窗边或者室外空旷处测试避免用搜索结果里的“室内定位”预期来怀疑模块。下表是几类典型现象的排查方向现象可能原因处理方式一直NO FIX天线没接好、屏蔽、模块背面焊盘虚焊换有源天线检查天线馈线搜星数多但坐标漂移大多径反射、天线离金属太近、HDOP过高移动天线位置输出HDOP并设阈值时间正常但经纬度全是0GGA解析字段索引错位对比原始语句检查nmea_field索引坐标在一定范围内来回跳静止环境下正常现象做位置平均或认为半径5米内变化正常断电重启后定位很慢备用电池没接星历丢失接V_BCKP测热启动重新定位GPS误差里水平定位精度标称2.5米CEP实际在城市峡谷、树荫下会劣化到5到10米这个数字在DEMO阶段不用纠结。重点看HDOP大于5时数据不建议用于导航或测量解析时可以把这个值的阈值判断加进去HDOP超标时显示层标注“定位质量差”比死等一个坐标有意义得多。6. PPS秒脉冲的时间对齐与定位精度评估6.1 用TIM输入捕获锁定PPS上升沿GPS模块的PPS引脚每秒输出一个精确的脉冲沿与UTC秒边界对齐精度远高于串口解析NMEA能拿到的秒信息。串口那条路经过字节接收、攒行、解析时间戳已经滞后几十毫秒对授时类应用不够用。把PPS接到STM32的定时器输入捕获通道每次上升沿产生捕获中断记录当前计数器值就能得到“本秒开始”的精确时间基准。static TIM_HandleTypeDef htim2; static volatile uint32_t pps_ticks 0; static volatile uint32_t pps_capture 0; void MX_TIM2_Init(void) { TIM_IC_InitTypeDef sConfigIC {0}; __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; HAL_TIM_IC_Init(htim2); sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_IC_SELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0x0F; HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); } void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { pps_capture HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); pps_ticks; } }定时器72分频后计数频率1MHz计数值直接对应微秒。pps_capture两次捕获值的差值理论上是1000000实际会因模块晶振误差有微小偏差大于±50微秒就该检查模块供电纹波和天线接口。当PPS沿来到时置一个标志位主循环里它的优先级高于NMEA刷新先校准本地软件秒再渲染显示这样屏幕上的秒数与真实UTC秒对齐误差控制在微秒级。6.2 静态采集五分钟评估实际误差判断一个定位方案能不能用把模块固定在一个位置采集五分钟静态数据看经纬度散点分布。计算所有点的平均坐标再统计每个点到平均点的距离水平方向上68%的点落在某半径内这个半径约等于该环境的实际CEP。如果在空旷室外这个半径小于3米说明天线和模块工作正常如果大于10米检查天线馈线是否靠近电源、板上是否有大面积覆铜遮挡、模块上方是否有金属壳体。国内常见地图平台使用的坐标与WGS84标准坐标系之间存在偏移把ATGM336H的原始经纬度直接标注在地图上会出现几百米的系统偏离需要在应用层做坐标转换后再上地图。这只是坐标基准差异不是模块故障做地图类应用时要提前规划。要快速验证PPS是否真的在和UTC对齐用另一台手机的时间参考对比PPS对应的整秒时刻或者用示波器看PPS和串口GGA语句第一个字节的时序关系——PPS沿应该落在GGA输出的整秒帧前后很窄的窗口内。6.3 有源天线与ESD防护的选型陶瓷无源天线成本低、体积小但它对地平面和净空要求苛刻天线正下方和周围一圈不允许铺铜天线馈线到模块天线焊盘的距离尽量控制在20毫米以内。如果产品结构决定天线必须远离主板比如金属外壳或长引线那就选有源天线内置LNA可以补偿线缆损耗但供电要按模块规格选取避免LNA工作电压不足导致灵敏度劣化。屏和模块外露的串口是静电进入的高危通道TFT-LCD液晶显示模组的FPC和触摸排线尤其容易积累电荷生产装配阶段建议在串口数据线上串33Ω电阻天线焊盘附近预留ESD保护器件焊盘整机过静电测试时再根据问题点补齐。GPS天线接口是射频通路不能用普通TVS管直接并联在馈线上需选用低结电容的射频专用ESD器件否则自身电容会吃掉天线信号。判断一个GPS系统是否可靠先看PPS抖动量级和静态点的散度这两个指标比屏幕上的卫星数更能说明问题。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价