ESP32 实战教程:接入 GPS,解析 NMEA 并做位置记录器
本文由 AI 协助整理。GPS 首次定位时间、精度和可用卫星数受天空遮挡、天线、天气与模块状态影响,文中不虚构固定精度。位置数据属于敏感信息,发布日志或上传服务器前请先处理隐私。

GPS 模块究竟在输出什么
GPS 模块并不会直接把“我现在在某某地方”塞给 ESP32。它通常通过 UART 连续输出 NMEA 语句,例如 $GNRMC、$GNGGA。这些语句包含定位状态、时间、经纬度、速度、航向和卫星信息。
ESP32 的工作分为三层:UART 接收字节,NMEA 库解析语句,业务代码只使用有效字段。不要在业务代码里用 readStringUntil(',') 到处拆逗号,那会把校验、跨语句和半包问题全塞进一个循环。
硬件与接线
选择一个带 UART 输出的 GPS/GNSS 模块,例如常见的 u-blox 系列或兼容模块。接线:
| GPS | ESP32 |
|---|---|
| TX | GPIO16 / UART2 RX |
| RX | GPIO17 / UART2 TX,可选 |
| GND | GND |
| VCC | 按模块规格供电 |
GPS 模块冷启动时可能需要较长时间获得定位。把天线放到室外开阔处,先用 USB 转串口或 ESP32 串口打印原始语句,确认模块确实在输出,再开始研究经纬度。室内窗边拿不到有效定位,不一定是代码错,卫星也不太愿意穿墙参加会议。
安装解析库
Arduino IDE 可以安装 TinyGPSPlus。它负责从字节流中识别 NMEA 语句,并提供 location.isValid()、speed.kmph()、satellites.value() 等字段。
#include <TinyGPSPlus.h>
TinyGPSPlus gps;
HardwareSerial gpsSerial(2);
constexpr int GPS_RX = 16;
constexpr int GPS_TX = 17;
初始化 UART 与读取字节
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
Serial.println("GPS logger ready");
}
void loop() {
while (gpsSerial.available() > 0) {
char c = static_cast<char>(gpsSerial.read());
if (gps.encode(c)) {
handleSentence();
}
}
}
如果模块波特率不是 9600,应以配置为准。有些模块默认输出 9600,有些项目被改过 38400 或更高。TX/RX 接线要交叉:模块 TX 进入 ESP32 RX,模块 RX 才接 ESP32 TX。只想读取时可以暂时不接模块 RX。
只使用有效定位
void handleSentence() {
if (gps.location.isValid()) {
Serial.printf("lat=%.6f lon=%.6f age=%lu ms\n",
gps.location.lat(),
gps.location.lng(),
static_cast<unsigned long>(gps.location.age()));
} else {
Serial.println("Location not valid");
}
if (gps.time.isValid()) {
Serial.printf("UTC %02d:%02d:%02d\n",
gps.time.hour(), gps.time.minute(), gps.time.second());
}
}
isValid() 说明解析器收到了可用字段,age() 反映这条数据距离最近一次更新多久。设备从开阔处移到室内后,最后一个坐标可能仍然存在,但不代表当前位置还在不断更新。把时间和定位状态一起保存,不能只看坐标有没有数字。
记录速度、航向和卫星数
struct PositionSample {
uint32_t sequence;
bool valid;
double latitude;
double longitude;
double speedKmph;
double course;
uint32_t satellites;
uint32_t hdop100;
};
uint32_t positionSequence = 0;
PositionSample readPosition() {
PositionSample sample{};
sample.sequence = positionSequence++;
sample.valid = gps.location.isValid();
if (!sample.valid) return sample;
sample.latitude = gps.location.lat();
sample.longitude = gps.location.lng();
sample.speedKmph = gps.speed.kmph();
sample.course = gps.course.deg();
sample.satellites = gps.satellites.value();
sample.hdop100 = static_cast<uint32_t>(gps.hdop.hdop() * 100.0);
return sample;
}
实际项目中不要在每条 NMEA 语句到达时写 SD 或上报网络。GPS 可能每秒输出多条语句,业务层应该按固定周期读取“当前最新状态”。否则记录器会变成语句计数器,而不是位置采样器。
校验与异常数据
位置值需要业务范围检查:纬度应在 -90 到 90,经度在 -180 到 180,卫星数量不应是离谱的大数。速度、HDOP 和定位年龄也要与场景结合判断。
bool positionLooksReasonable(const PositionSample& sample) {
return sample.valid &&
sample.latitude >= -90.0 && sample.latitude <= 90.0 &&
sample.longitude >= -180.0 && sample.longitude <= 180.0 &&
sample.satellites > 0 && sample.hdop100 < 5000;
}
不要把无效定位写成 (0,0),这会让地图上出现一个看似真实的海上点。CSV 中应保留空坐标和 NO_FIX 状态,后续统计才不会把无效记录算进路线。
时间处理:GPS UTC 与本地时间
GPS 时间通常是 UTC。记录文件时要明确使用 UTC 还是北京时间,不要在字符串里写一个没有时区说明的 2026-09-28 08:00:00。
可以保存原始 UTC 字段,展示层再转换到本地时区:
utc_time,latitude,longitude,speed_kmph,satellites,status
2026-09-28T01:00:00Z,31.230400,121.473700,0.0,12,VALID
如果设备还有 RTC,可以在首次有效定位后用 GPS 时间校准 RTC,之后即使暂时没有卫星,也能给传感器记录提供相对连续的时间。校时要限制频率,避免每条语句都修改系统时间。
保存成 CSV
#include <SD.h>
bool appendPosition(const PositionSample& sample) {
File file = SD.open("/track.csv", FILE_APPEND);
if (!file) return false;
size_t written = 0;
if (positionLooksReasonable(sample)) {
written = file.printf("%lu,%.6f,%.6f,%.2f,%lu,VALID\n",
static_cast<unsigned long>(sample.sequence),
sample.latitude, sample.longitude,
sample.speedKmph,
static_cast<unsigned long>(sample.satellites));
} else {
written = file.printf("%lu,,,,,NO_FIX\n",
static_cast<unsigned long>(sample.sequence));
}
file.flush();
file.close();
return written > 0;
}
位置日志涉及隐私,文件名不要直接包含姓名、家庭地址或车牌。上传服务器时使用设备匿名 ID,并按照实际需求设置保存期限。
只在移动或距离变化时记录
如果设备静止放置,按秒记录会产生大量重复坐标。可以设置最小距离或最小时间间隔。初学阶段先用固定时间间隔,等数据格式与定位状态稳定后再加入距离过滤。
距离计算可使用 TinyGPSPlus 提供的工具,或者在服务器端处理。设备端如果需要低功耗,建议减少计算和写卡次数,把原始定位按较低频率保存。
GPS 状态机
一个可维护的程序至少区分:
NO_DATA:UART 没有收到有效语句。PARSING:有语句但尚未定位。VALID:坐标和时间有效。STALE:曾经定位,但数据长时间没有更新。ERROR:串口或存储发生异常。
网页、OLED、MQTT 和 CSV 都使用同一状态,不要每个输出模块自己猜“没有坐标代表什么”。状态统一以后,排错日志会比一串孤零零的经纬度更有用。
分阶段验证
- 只打印原始 NMEA,确认 UART 有连续输出。
- 加 TinyGPSPlus,观察
charsProcessed()、sentencesWithFix()。 - 把天线移到开阔处,等待首次定位。
- 打印卫星数、HDOP、坐标年龄和速度。
- 保存 CSV,检查 UTC、列数和无效行。
- 关闭 GPS 或遮挡天线,确认状态变成无定位而不是沿用旧值。
- 在移动中记录,检查速度和航向是否合理。
常见问题
| 现象 | 优先检查 |
|---|---|
| 串口完全没有字符 | VCC、GND、TX/RX、波特率 |
| 有 NMEA 但一直无定位 | 天线、天空视野、冷启动时间 |
| 坐标跳动很大 | 卫星数、HDOP、多路径反射、固定天线 |
| 速度偶尔很高 | 无效数据未过滤,或低速航向不稳定 |
| 时间差几个小时 | UTC 与本地时区混用 |
| 记录文件坐标为 0 | 把无效定位错误写成默认数值 |
总结
GPS 项目不是“串口读到一串数字”就完成了。要把 NMEA 解析成位置业务,还要处理定位状态、数据年龄、时间区域、无效值、隐私和记录频率。
先确认原始语句,再确认有效定位,最后把结构化状态写入 CSV 或上报网络。这样做出来的位置记录器,才能回答“这条坐标是什么时候、在什么状态下产生的”,而不是把一堆数字堆在文件里等未来的自己猜。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号