为什么需要定位功能?
做物联网项目的同学肯定遇到过这种需求:设备在哪?怎么让它上报位置?共享单车、物流追踪、宠物定位、户外探险记录仪……这些场景都离不开 GNSS 定位。
今天咱们用国产的 ATGM336H 模块(北斗 + GPS 双模)做一个简易 GPS 追踪器,成本不到 50 元,还能把位置数据存到 SD 卡或者通过 4G 发送到服务器。
⚠️ 注意:GNSS 模块需要室外空旷环境才能正常定位,室内测试基本没信号。我一开始在办公室折腾了半小时没反应,还以为模块坏了,拿到楼下一分钟就搜到 8 颗卫星……
需要准备什么?
| 物品 | 型号/规格 | 价格 |
|---|---|---|
| GNSS 模块 | ATGM336H-5N | ¥25 |
| 开发板 | Arduino Nano / ESP32 | ¥15-25 |
| OLED 显示屏 | 0.96 寸 I2C | ¥8 |
| microSD 卡模块 | SPI 接口 | ¥5 |
| 锂电池 | 3.7V 500mAh | ¥10 |
| 充电模块 | TP4056 | ¥2 |
| 杜邦线 | 公对公/母对母 | ¥5 |
| 总计 | ¥70-80 |
ATGM336H 是中科微的国产北斗 + GPS 双模模块,灵敏度不错,冷启动 30 秒左右就能定位。相比进口的 NEO-6M,价格便宜一半,性能差距不大。
硬件连接
ATGM336H 使用 UART 串口通信,默认波特率 9600。接线非常简单:
| ATGM336H | Arduino Nano |
|---|---|
| VCC | 5V |
| GND | GND |
| TX | D10 (RX) |
| RX | D11 (TX) |
注意:模块的 TX 接开发板的 RX,RX 接开发板的 TX,别接反了。如果用 ESP32,可以在代码里自定义软串口引脚。
OLED 和 SD 卡模块用 I2C 和 SPI 接口:
| OLED | Arduino |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | A5 |
| SDA | A4 |
| SD 卡模块 | Arduino |
|---|---|
| VCC | 5V |
| GND | GND |
| CS | D4 |
| MOSI | D11 |
| MISO | D12 |
| SCK | D13 |
第一步:读取 NMEA 数据
GNSS 模块会持续输出 NMEA 0183 格式的数据,这是一套标准的航海电子设备通信协议。常见的数据行有:
$GPGGA– 定位信息(时间、经纬度、高度、卫星数)$GPRMC– 推荐最小定位信息(时间、状态、经纬度、速度)$GPGSV– 卫星状态(可见卫星列表)
我们主要解析 $GPGGA 和 $GPRMC。先写个最简单的读取程序:
#include
SoftwareSerial gpsSerial(10, 11); // RX, TX
void setup() {
Serial.begin(9600);
gpsSerial.begin(9600);
Serial.println("GPS Tracker Starting...");
}
void loop() {
if (gpsSerial.available()) {
String line = gpsSerial.readStringUntil('\n');
Serial.println(line);
}
}
上传代码后打开串口监视器,你会看到类似这样的输出:
$GPGGA,083522.00,3123.45678,N,12134.56789,E,1,08,1.2,50.5,M,0.0,M,,*6A
$GPRMC,083522.00,A,3123.45678,N,12134.56789,E,0.5,123.4,120326,,*1B
$GPGSV,3,1,12,01,45,123,45,02,30,234,40,03,60,345,42,04,20,056,38*78
如果全是空行或者 $GPGGA 里卫星数是 0,说明还没定位成功。拿到室外等 1-2 分钟,看到卫星数大于 4 就可以继续了。
第二步:解析经纬度
NMEA 的经纬度格式有点奇葩:DDMM.MMMMMM(度 + 分),不是我们熟悉的十进制度数。需要转换一下:
// 将 NMEA 格式转换为十进制度数
float convertToDecimalDegrees(String nmeaCoord, char direction) {
int degree = nmeaCoord.substring(0, 2).toInt();
float minute = nmeaCoord.substring(2).toFloat();
float decimal = degree + (minute / 60.0);
// 南纬或西经需要取负
if (direction == 'S' || direction == 'W') {
decimal = -decimal;
}
return decimal;
}
// 解析 GPGGA 语句
void parseGPGGA(String sentence) {
// $GPGGA,时间,纬度,N/S,经度,E/W,定位质量,卫星数,HDOP,高度...
int parts[20];
int idx = 0;
int start = 0;
for (int i = 0; i <= sentence.length(); i++) {
if (sentence[i] == ',' || sentence[i] == '*') {
String part = sentence.substring(start, i);
if (idx < 20) {
// 存储每个字段
}
start = i + 1;
idx++;
}
}
// 提取关键字段
String time = parts[0];
String lat = parts[1];
char latDir = parts[2].charAt(0);
String lon = parts[3];
char lonDir = parts[4].charAt(0);
int satCount = parts[6].toInt();
float altitude = parts[8].toFloat();
float latDec = convertToDecimalDegrees(lat, latDir);
float lonDec = convertToDecimalDegrees(lon, lonDir);
Serial.print("时间:"); Serial.println(time);
Serial.print("纬度:"); Serial.println(latDec, 6);
Serial.print("经度:"); Serial.println(lonDec, 6);
Serial.print("卫星数:"); Serial.println(satCount);
Serial.print("高度:"); Serial.print(altitude); Serial.println(" 米");
}
更简单的做法是用现成的库,比如 TinyGPSPlus,解析更可靠:
#include
#include
TinyGPSPlus gps;
SoftwareSerial gpsSerial(10, 11);
void setup() {
Serial.begin(9600);
gpsSerial.begin(9600);
}
void loop() {
while (gpsSerial.available()) {
gps.encode(gpsSerial.read());
}
if (gps.location.isUpdated()) {
Serial.print("纬度:");
Serial.println(gps.location.lat(), 6);
Serial.print("经度:");
Serial.println(gps.location.lng(), 6);
Serial.print("卫星数:");
Serial.println(gps.satellites.value());
Serial.print("高度:");
Serial.print(gps.altitude.meters());
Serial.println(" 米");
}
}
第三步:显示和存储
加上 OLED 显示屏,可以实时看到当前位置:
#include
#include
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
void displayGPS(float lat, float lon, int sats, float alt) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("GPS Tracker");
display.println("-----------");
display.print("Lat: ");
display.println(lat, 5);
display.print("Lon: ");
display.println(lon, 5);
display.print("Sats: ");
display.println(sats);
display.print("Alt: ");
display.print(alt);
display.println("m");
display.display();
}
如果要记录轨迹,可以把数据存到 SD 卡:
#include
#include
File logFile;
void setupSD() {
if (!SD.begin(4)) {
Serial.println("SD 卡初始化失败!");
return;
}
Serial.println("SD 卡就绪");
}
void logGPS(float lat, float lon, int sats, float alt) {
String fileName = "track" + String(millis()) + ".txt";
logFile = SD.open(fileName, FILE_WRITE);
if (logFile) {
logFile.print(millis());
logFile.print(",");
logFile.print(lat, 6);
logFile.print(",");
logFile.print(lon, 6);
logFile.print(",");
logFile.print(sats);
logFile.print(",");
logFile.println(alt, 1);
logFile.close();
}
}
数据格式是 CSV,方便后续用 Excel 或地图工具可视化。
第四步:低功耗设计
如果是电池供电的追踪器,功耗很重要。ATGM336H 工作电流约 20mA,ESP32 深度睡眠可以做到 10μA。
策略:每 5 分钟唤醒一次,定位 30 秒,记录位置,然后继续睡眠。
#include
#define uS_TO_S_FACTOR 1000000ULL
#define TIME_TO_SLEEP 300 // 5 分钟
void setup() {
// 初始化 GPS、SD 等
esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
Serial.println("准备进入深度睡眠...");
}
void loop() {
// 唤醒后执行
readAndLogGPS();
// 再次睡眠
esp_deep_sleep_start();
}
这样一节 1000mAh 的电池可以用好几个月。
常见问题排查
问题 1:模块一直输出空数据
- 原因:室内无信号或天线没接好
- 解决:拿到室外空旷处,检查天线是否拧紧(ATGM336H 需要外接有源天线)
问题 2:经纬度一直是 0
- 原因:还没完成首次定位(冷启动需要 30-60 秒)
- 解决:耐心等待,保持模块静止,确保天线朝上
问题 3:串口乱码
- 原因:波特率不匹配或接线错误
- 解决:确认波特率是 9600,检查 TX/RX 是否接反
问题 4:SD 卡写入失败
- 原因:供电不足或接触不良
- 解决:用独立 5V 供电给 SD 卡模块,检查 CS 引脚是否正确
问题 5:定位漂移严重
- 原因:多路径效应(高楼反射)或卫星数太少
- 解决:确保天空视野开阔,等待卫星数>8 再记录,软件上做滤波处理
扩展应用
有了基础定位功能,可以继续扩展:
- 4G 远程追踪:加上 EC200U 模块,把位置发送到服务器,手机随时查看
- 电子围栏:设定活动范围,超出范围发送报警
- 轨迹回放:把 SD 卡数据导入 Google Earth,回放运动轨迹
- 速度计算:根据位置变化和时间差计算移动速度
- 北斗短报文:用 RD 系列模块实现无信号区通信(需要 SIM 卡)
总结
GNSS 定位是物联网项目的常用功能,ATGM336H 作为国产模块性价比高,配合 Arduino 或 ESP32 可以快速实现定位追踪器。关键点:
- 室外测试,室内没信号
- 用 TinyGPSPlus 库简化解析
- 低功耗设计延长电池寿命
- CSV 格式存储方便后续处理
希望这篇博客文章对您有所帮助!
相关资源: