简介:面向嵌入式与物联网开发者的北斗定位ATGM336H-5N模块软硬件资料包,覆盖参考设计原理图、STM32/C51工程源码、Arduino例程及相关调试工具,兼顾硬件电路理解与多平台代码迁移,适合从入门到进阶的定位系统开发学习与实际项目移植。压缩包共224个文件,以C/H源码、uvproj工程、hex固件、ino例程、PDF文档等类型为主,另有编译链接产生的obj/axf等中间文件,整体体积仅6.31MB,便于快速下载部署;其中原理图PDF可用于核对电源管理、信号处理与接口连接等设计细节。目前已有1766人学习查看,资源内容组织清晰,既包含原理图与设计说明帮助理解硬件连接,也提供多平台工程代码便于直接验证;对希望将北斗定位能力融入智能交通、物流跟踪、农业自动化等场景的开发者,是一套务实的一站式参考资料。
1. 拿到ATGM336H-5N先看懂这颗北斗定位模块在解决什么问题
很多项目要北斗定位数据,但用不到厘米级,于是有人选了高精度RTK,接线复杂、成本翻几倍;有人退而求其次用手机基站定位,精度差得没法用。ATGM336H-5N是中间那个务实的选项——一颗以中科微AT6558为主控的北斗/GPS双模模块,3.3V供电,UART输出,默认9600波特率,单点定位精度在2.5米这个量级,室内外切换、轨迹记录、资产追踪、农业机械作业统计这类场景都够用。标题里那份资料的价值,不在某一张原理图本身,而在于它把硬件参考设计和STM32、C51、Arduino三套例程源码放在了一起,意味着不管你的项目主控是哪一种,都有一条能直接照做的路径。下面按硬件设计、STM32实现、C51与Arduino移植、调试验证的顺序,把该看的引脚、该调的参数和容易踩的坑一次说清。
2. 硬件参考设计:ATGM336H-5N引脚、电源与天线处理
先别急着写代码,模块能不能输出有效定位,一半决定在原理图上。ATGM336H-5N的封装不大,引脚也不算多,但电源、天线和串口这三块的接法决定了后续调试要花多长时间。
2.1 先从引脚定义确认最小连接关系
拿到模块第一件事是对照丝印和参考设计图纸把每个引脚的作用确认一遍。常见引脚功能如下表:
| 引脚 | 方向 | 说明 | 典型连接 |
|---|---|---|---|
| VCC | 输入 | 主电源,3.0V~3.6V,典型3.3V | 接LDO输出 |
| V_BCKP | 输入 | 备份电源,用于RTC和星历保持 | 接3.3V或电池/法拉电容 |
| GND | — | 系统地 | 就近接完整地平面 |
| TXD | 输出 | NMEA数据输出,接主控RX | 串口交叉连接 |
| RXD | 输入 | 配置指令输入,接主控TX | 串口交叉连接 |
| 1PPS | 输出 | 秒脉冲,定位后每秒输出一个脉冲 | 留测试点或接MCU捕获 |
| NRST | 输入 | 复位,低有效 | 上拉到3.3V |
连接时最容易犯的错是TXD和RXD不交叉,模块TXD必须进主控的RX引脚。另外RXD这一路建议串一个1kΩ电阻再进模块,主控侧上电瞬间如果有噪声或电平不确定,这个电阻能起到一定的保护作用。1PPS在功能上不是必须的,但调试时很有用——判断“模块到底有没有定位”比看串口快得多,后面第5章会用到。
2.2 电源设计:VCC和V_BCKP要分开对待
模块的参考设计里,电源部分通常不会复杂,但有两个细节值得注意。第一,VCC引脚旁边要放10μF和100nF两级退耦电容,100nF尽量贴近模块引脚,不要超过3mm走线距离,否则模块在接收灵敏度上会吃亏。第二,V_BCKP不能随手悬空。很多工程师觉得这个引脚是“可选”的,直接不接,结果每次上电模块都等于冷启动,在室内或地库场景下首次定位时间会被拉长到几分钟,还会被误判为模块故障。我一般会把V_BCKP和VCC接到同一个3.3V网络,至少保证模块断电后星历还能靠备份电容维持一小段时间。如果产品有常备电,V_BCKP单独接一个小容量电池或法拉电容,热启动体验会明显好很多。
供电来源方面,用AMS1117-3.3这类LDO就可以满足需求,但要注意模块启动瞬间电流并不大,真正该关注的是电源纹波。纹波偏大时,定位模块在弱信号场景下的表现会变差,所以PCB布局上LDO输出电容要靠近模块这一侧,地线走星形或铺铜,不要让电机驱动、继电器这类大电流回路跟模块共用一段细地线。
2.3 天线方案:陶瓷天线与有源天线走线差别
ATGM336H-5N的硬件参考设计里,天线部分一般给两种选择。一种是直接焊陶瓷贴片天线,适合产品外壳小、集成度高的场景;另一种是通过IPEX座外接有源天线,适合车载或需要把天线引到更高位置的场景。两者对电路的要求不一样:有源天线需要在射频输入线上叠一个馈电网络,常见做法是用一个小阻值电感(百纳亨量级)把3.3V引到天线供电脚,同时用地和电容做射频隔离;陶瓷天线则不需要馈电,但更讲究净空——天线正下方不要铺铜、不要走线,模块最好放在板边,让天线朝外悬空。
不管用哪种天线,射频走线越短越好,中间不要打过孔,不要跟晶振、DCDC电感、PWM电机线贴在一起。有条件的板子把射频线两侧打一排地过孔做包地,线宽按50欧姆阻抗去调,两层板控制不精确也关系不大,关键是走线两侧的“地”要连续。另外,天线区域周围不要放金属外壳或大块铺铜,这一点在结构设计阶段就要跟结构工程师确认,否则等板子回来才发现天线被屏蔽了,搜星性能会大幅下降。
2.4 原理图评审阶段的4个检查点
画完原理图别急着投板,按下面这张表过一遍,能省掉不少返工:
| 检查点 | 合格标准 | 常见出错现象 |
|---|---|---|
| VCC与V_BCKP | 两个引脚都有3.3V来源 | 上电后串口完全无输出 |
| TXD/RXD | 交叉连接,RXD串1k电阻 | 数据乱码或收不到任何NMEA语句 |
| 1PPS测试点 | 留出可测量焊盘 | 无法判断是否已定位 |
| 天线净空区 | 天线正下方无铺铜无走线 | 搜星慢,弱信号下反复失锁 |
PCB阶段还要加一条:模块尽量靠近板边放置,天线一侧朝外,不要在模块正上方叠加排针或其他金属器件。
3. STM32例程:串口接收与GNGGA经纬度解析
STM32的例程是这套资料里最常用的部分。很多人拿到例程直接编译下载,发现串口能收到数据但解析不对,问题大多出在对NMEA语句的理解上。这一章以STM32F103为例,从串口配置讲到GNGGA语句解析。
3.1 串口初始化:9600 8N1不是随便定的
ATGM336H-5N出厂默认波特率是9600,数据位8位,无校验,1位停止位。NMEA-0183协议本身就是按这个低速率设计的,一条GGA语句最长也就80多个字符,也不需要高波特率。在CubeMX里把USART1配成异步模式,参数按出厂默认填好,然后只需要开一个串口接收中断,不需要DMA。
为什么不推荐DMA?因为NMEA语句不定长,DMA要预先设定接收长度,处理不定长数据要么加空闲中断,要么设超时,对例程来说复杂度反而上去了。串口中断逐字节接收,每行结束以换行符为边界,逻辑非常直观。
3.2 逐字节中断接收:完整接收一行NMEA
下面这段代码是接收部分的骨架,可以直接跑在STM32F103标准库或HAL库工程里:
/* 全局接收变量 */ uint8_t rx_byte; /* 每个接收字节 */ char nmea_buf[100]; /* 一行NMEA的缓冲 */ uint16_t nmea_idx = 0; volatile uint8_t gga_ready = 0; /* 置1表示有一行完整的GGA */ /* 初始化时启动单字节接收 */ HAL_UART_Receive_IT(&huart1, &rx_byte, 1); /* 串口接收中断回调 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { char c = (char)rx_byte; if (c == '\n') { /* 一条语句结束 */ nmea_buf[nmea_idx] = '\0'; if (strstr(nmea_buf, "$GNGGA")) gga_ready = 1; /* 只触发GGA解析 */ nmea_idx = 0; } else if (nmea_idx < sizeof(nmea_buf) - 1) { nmea_buf[nmea_idx++] = c; /* 超出缓冲的字节丢弃 */ } HAL_UART_Receive_IT(&huart1, &rx_byte, 1); /* 重新使能 */ } }这段代码有一个容易被忽略的要点:HAL_UART_Receive_IT每次只接收一个字节,在回调处理完你必须再次调用它,否则中断只会触发一次,后面什么数据都收不到。主循环里只需要检查gga_ready标志位,置位后把nmea_buf交给解析函数再清除标志。这个结构换到STM32F407、H743上只需改huart实例名,没有平台相关性。
3.3 GNGGA解析:为什么前缀是GN而不是GP
AT6558同时跟踪北斗和GPS两个星座,输出的语句前缀会根据实际参与定位的星座组合变化。常见的是$GNGGA,代表北斗和GPS混合定位;如果看到$GPGGA,说明当前只有GPS星座在工作。解析时不能用strstr(buf, "$GPGGA")去匹配,否则北斗数据会被漏掉。
GGA语句的字段顺序是:UTC时间、纬度(ddmm.mmmm格式)、N/S、经度(dddmm.mmmm格式)、E/W、定位质量、卫星数、水平精度因子、海拔高度、高度单位、大地水准面差…… 解析代码:
void parse_gngga(char *buf, double *lat, double *lon, int *quality, int *sat) { char tmp[100]; char *tok; int field = 0; double lat_raw = 0, lon_raw = 0; char ns = 0, ew = 0; strncpy(tmp, buf, sizeof(tmp) - 1); tmp[sizeof(tmp) - 1] = '\0'; tok = strtok(tmp, ","); while (tok != NULL) { switch (field) { case 2: /* 纬度字段 ddmm.mmmm */ lat_raw = atof(tok); break; case 3: /* N/S */ ns = tok[0]; break; case 4: /* 经度字段 dddmm.mmmm */ lon_raw = atof(tok); break; case 5: /* E/W */ ew = tok[0]; break; case 6: /* 定位质量 */ *quality = atoi(tok); break; case 7: /* 可见卫星数 */ *sat = atoi(tok); break; } field++; tok = strtok(NULL, ","); } /* 将 ddmm.mmmm 转成十进制度 */ int lat_deg = (int)(lat_raw / 100); double lat_min = lat_raw - lat_deg * 100; *lat = lat_deg + lat_min / 60.0; if (ns == 'S') *lat = -*lat; int lon_deg = (int)(lon_raw / 100); double lon_min = lon_raw - lon_deg * 100; *lon = lon_deg + lon_min / 60.0; if (ew == 'W') *lon = -*lon; }strtok会修改原始缓冲区,所以先用strncpy拷贝一份再解析。经纬度转换是这类例程最容易写错的地方,很多源码直接把ddmm.mmmm当十进制度用了,结果坐标偏移几公里。判断定位有效性时,quality为0表示未定位,1是单点定位,2是差分定位,只有大于0的数据才值得记录。
3.4 STM32例程里需要调整的3个参数
| 参数 | 常见设置 | 备注 |
|---|---|---|
| 串口波特率 | 9600 | 与模块出厂配置一致 |
| 接收缓冲区长度 | 100~128字节 | 超过后丢弃,不要覆盖旧数据 |
| 定位质量使能 | quality > 0 | 保证轨迹数据有效 |
如果你的板子用USB转串口芯片跟模块通信,PC端看到的波特率要和MCU串口保持一致,不要误把USB转串口的115200当成模块通信波特率。
4. 把例程移植到C51和Arduino:两个受限平台的取舍
STM32资源充足,整行缓存再解析没有问题。但C51只有256字节RAM,Arduino Uno也只有2KB,直接搬过来就会遇到内存问题。这一章说清楚两种平台各自该怎么做。
4.1 C51为什么非用11.0592MHz晶振不可
STC89C52这类51单片机的串口波特率由定时器1产生,公式是:波特率 = 晶振频率 / 12 / 32 / (256 - TH1)。用11.0592MHz晶振时,取TH1 = 0xFD,算下来正好是9600,误差为0%。如果板子已经用了12MHz晶振,算出来的9600会有约8.5%的误差,串口通信在长帧数据下就容易出错。所以C51例程里一律按11.0592MHz设计,串口初始化代码:
void UART_Init(void) { TMOD &= 0x0F; /* 只改定时器1 */ TMOD |= 0x20; /* 定时器1,模式2:8位自动重装 */ TH1 = 0xFD; /* 11.0592MHz下波特率9600 */ TL1 = 0xFD; SCON = 0x50; /* 8位UART,允许接收 */ TR1 = 1; /* 启动定时器1 */ ES = 1; /* 开串口中断 */ EA = 1; /* 开总中断 */ }TMOD &= 0x0F这行很关键,它只清零高4位,不影响低4位定时器0的配置,避免初始化完串口把PWM或系统节拍弄坏了。
4.2 C51内存受限:用状态机边收边解析
一条完整的GGA语句最长接近100字节,C51的256字节RAM存一条还行,但还要留变量、堆栈和显示缓冲,内存会非常紧张。更稳妥的做法是不缓存整行,在串口中断里直接识别字段。
void UART_ISR(void) interrupt 4 { unsigned char c; if (RI) { RI = 0; c = SBUF; if (c == '$') { state = 0; /* 等待GGA语句名 */ field = 0; idx = 0; } if (state == 0) { ggabuf[ggaidx++] = c; if (ggaidx >= 6) { /* 已收集$GNGGA */ if (ggabuf[1] == 'G' && ggabuf[3] == 'G') state = 1; /* 确认是GGA,开始数逗号 */ else state = 0; } } else if (c == ',') { field++; idx = 0; } else if (field == 2 && idx < 8) { latbuf[idx++] = c; /* 直接存纬度ASCII */ } } }上面只是思路,真实例程里还需要对经度、质量、卫星数做同样的分支。这里的核心思想是:数据在中断里按字段切好,主循环只负责把ASCII转成数值。另外C51上最好不用浮点数,把经纬度转成整数保存,比如用度乘以10的6次方后的整数,既能保证精度又避免printf和浮点库拖慢速度。
4.3 Arduino:整行缓冲加字符串解析
Arduino Uno的RAM有2KB,整行缓存100字节绰绰有余,但要注意别用String类做拼接,容易产生堆碎片。模块接软串口时用SoftwareSerial,接在UNO的D10、D11上:
#include <SoftwareSerial.h> SoftwareSerial ss(10, 11); char line[100]; unsigned char idx = 0; void setup() { Serial.begin(115200); /* 调试输出口 */ ss.begin(9600); /* 模块连接口 */ } void loop() { while (ss.available()) { char c = ss.read(); if (c == '\n') { line[idx] = 0; if (strstr(line, "$GNGGA")) { double lat = 0, lon = 0; int q = 0, sat = 0; parse_gngga(line, &lat, &lon, &q, &sat); if (q > 0) { /* 只输出有效定位 */ Serial.print(lat, 6); Serial.print(","); Serial.println(lon, 6); } } idx = 0; } else if (idx < sizeof(line) - 1) { line[idx++] = c; } } }parse_gngga可以用第3章的strtok版本,Arduino的编译器对标准C函数支持得很好,不用自己逐字符找逗号。用两块开发板调试时容易混淆串口:模块接软串口,调试打印走硬件串口,两个初值一个9600一个115200,不要设成一样。如果是ESP32或ESP8266这类多硬件串口的平台,直接用Serial1接模块,不再需要SoftwareSerial。
两种平台的思路差异可以用这张表概括:
| 对比项 | C51/STC89C52 | Arduino Uno |
|---|---|---|
| RAM | 256字节 | 2KB |
| 串口资源 | 1个硬件串口 | 硬件串口+软串口 |
| 推荐解析方式 | 中断内状态机 | 整行缓冲后解析 |
| 浮点运算 | 尽量避免 | 可用,避免String拼接 |
5. 定位调试技巧:先看输出,再算离散度
5.1 用USB转TTL线先把模块单独验证出来
模块上电后第一步不是接单片机,而是拿一根USB转TTL线直接连模块:模块TXD接转换器RXD,模块RXD接转换器TXD,GND共地,VCC必须接3.3V。打开串口助手设为9600,能看到NMEA语句持续输出。如果没有任何输出,先查VCC和V_BCKP的电压,再看焊接有没有连锡。如果能看到$GNGGA但quality一直为0,属于正常现象——模块还没完成定位,把它放到窗台或室外,通常一二十秒内quality会变为1。在这个阶段可以用串口助手发AT指令调整模块输出内容,格式是$PCASxx加参数再跟校验和,校验和是$到*之间字符的逐字节异或结果。指令不对模块不会应答,重新核对字段序号再发即可。
5.2 用Python统计标准差,判断定位质量
只看quality为1还不够,模块可能锁定在误差较大的状态下。把串口输出保存成uart.log,用下面这段Python统计连续200帧定位结果的离散程度:
import re import math lats, lons = [], [] pat = re.compile( r"\$GNGGA,\d+\.\d+,(\d+\.\d+),([NS]),(\d+\.\d+),([EW]),([0-9]),(\d+)" ) with open("uart.log", "r") as f: for line in f: m = pat.search(line) if not m: continue lat_raw, ns = m.group(1), m.group(2) lon_raw, ew = m.group(3), m.group(4) q = int(m.group(5)) if q == 0: continue lat_deg = int(lat_raw) // 100 + (float(lat_raw) % 100) / 60.0 lon_deg = int(lon_raw) // 100 + (float(lon_raw) % 100) / 60.0 if ns == 'S': lat_deg = -lat_deg if ew == 'W': lon_deg = -lon_deg lats.append(lat_deg) lons.append(lon_deg) if len(lats) >= 200: break n = len(lats) if n < 10: print("有效样本不足") else: lat_mean = sum(lats) / n lon_mean = sum(lons) / n lat_sigma = math.sqrt(sum((x - lat_mean) ** 2 for x in lats) / (n - 1)) lon_sigma = math.sqrt(sum((y - lon_mean) ** 2 for y in lons) / (n - 1)) print(f"样本数 {n}") print(f"经度标准差 {lon_sigma * 1e5:.1f} (约米)") print(f"纬度标准差 {lat_sigma * 1e5:.1f} (约米)")模块固定不动时,标准差通常应在数米以内;200帧中如果quality频繁跳0,说明信号环境或天线位置有问题,优先调整天线净空和供电走线。确认定位质量没问题后,再把这套判断逻辑写进STM32或Arduino的主循环里,把quality大于0且标准差正常的点存入轨迹。
本文还有配套的精品资源,点击获取