【ESP32S3 + ATGM332D GPS模块实战二:SSD1306交互显示】
·
ESP32S3 + ATGM332D GPS模块实战二:SSD1306交互显示
一、项目背景与目标
上一篇博客《ESP32S3 + ATGM332D GPS模块实战一:TinyGPSPlus解析与本地墨卡托投影》中,我们已经实现了GPS数据的解析、经纬度到本地ENU坐标系的转换,以及速度和航向的计算。
**本文在此基础上,新增SSD1306 OLED显示屏的集成,实现:
- 📊 实时经纬度显示
- 📏 距离参考点距离显示
- 🚦 GPS信号状态指示
- 🔄 平滑的数据可视化交互
二、硬件连接图
ESP32S3开发板 ────────── ATGM332D GPS模块
GPIO18 (TX) ──> RX
GPIO17 (RX) <── TX
3.3V ── VCC
GND ── GND
ESP32S3开发板 ────────── SSD1306 OLED模块 (I2C)
GPIO12 (SCL) ── SCL
GPIO13 (SDA) ── SDA
3.3V ── VCC
GND ── GND
ESP32S3开发板
ATGM332D GPS模块
SSD1306 OLED模块
三、核心代码实现
完整代码
#include <Arduino.h>
#include <TinyGPS++.h>
#include <math.h>
#include <U8g2lib.h>
// ATGM332D GPS模块配置
#define GPS_UART Serial1 // 使用硬件串口1
#define GPS_BAUD_RATE 9600 // ATGM332D默认波特率为9600
#define GPS_TX_PIN 18 // UART1_TX
#define GPS_RX_PIN 17 // UART1_RX
// SSD1306 OLED配置
#define OLED_SCK_PIN 12 // SCL
#define OLED_SDA_PIN 13 // SDA
// 输出频率配置
#define PRINT_INTERVAL_MS 500 // 每秒5次 (1000/5=200ms)
// TinyGPSPlus对象
TinyGPSPlus gps;
// U8g2对象(I2C接口,SSD1306 128x64)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE, /* clock=*/OLED_SCK_PIN, /* data=*/OLED_SDA_PIN);
// 参考点(初始位置作为本地坐标系原点)
double refLat = 0.0;
double refLng = 0.0;
bool refSet = false;
// 上一时刻的位置和时间
double lastEast = 0.0;
double lastNorth = 0.0;
unsigned long lastTimeMs = 0;
// 计算得到的速度和航向
float calcSpeed = 0.0;
float calcHeading = 0.0;
// 函数声明
void latLngToENU(double lat, double lng, double refLat, double refLng, double& east, double& north);
void updateOLED(double lat, double lng, double distance);
void setup() {
// 初始化USB串口用于调试输出
Serial.begin(115200);
delay(500);
Serial.println("ATGM332D GPS Module Test Demo");
Serial.println("Using TinyGPSPlus Library with Local Mercator Projection");
Serial.println("Initializing...");
// 配置并初始化GPS串口
GPS_UART.begin(GPS_BAUD_RATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
// 等待GPS模块启动
delay(1000);
// 初始化OLED屏幕
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 20, "GPS Initializing...");
u8g2.sendBuffer();
Serial.println("GPS module initialized!");
Serial.printf("GPS UART: Serial1, TX=%d, RX=%d, Baud=%d\n", GPS_TX_PIN, GPS_RX_PIN, GPS_BAUD_RATE);
Serial.printf("OLED: SCK=%d, SDA=%d\n", OLED_SCK_PIN, OLED_SDA_PIN);
Serial.printf("Output frequency: %d times per second\n", 1000 / PRINT_INTERVAL_MS);
Serial.println("Listening for GPS NMEA sentences...");
Serial.println("----------------------------------------");
}
void loop() {
// 持续读取并解析GPS数据
while (GPS_UART.available() > 0) {
gps.encode(GPS_UART.read());
}
// 当接收到新的定位信息时输出
static unsigned long lastPrintTime = 0;
if (millis() - lastPrintTime >= PRINT_INTERVAL_MS) { // 每秒5次
lastPrintTime = millis();
Serial.println("========================================");
// 检查是否有有效的定位数据
if (gps.location.isValid()) {
double lat = gps.location.lat();
double lng = gps.location.lng();
// 设置参考点(首次有效定位)
if (!refSet) {
refLat = lat;
refLng = lng;
refSet = true;
Serial.println("参考点已设置");
}
// 将经纬度转换为本地ENU坐标
double east, north;
latLngToENU(lat, lng, refLat, refLng, east, north);
// 计算速度和航向(基于位移)
unsigned long currentTimeMs = millis();
if (lastTimeMs > 0 && refSet) {
double dt = (currentTimeMs - lastTimeMs) / 1000.0; // 时间差(秒)
if (dt > 0.01) { // 避免除零
double dx = east - lastEast;
double dy = north - lastNorth;
// 计算速度 (m/s -> km/h)
calcSpeed = sqrt(dx*dx + dy*dy) / dt * 3.6;
// 计算航向角度 (弧度转角度)
calcHeading = atan2(dx, dy) * 180.0 / M_PI;
if (calcHeading < 0) calcHeading += 360.0; // 转换为0-360度
}
}
// 更新上一时刻数据
lastEast = east;
lastNorth = north;
lastTimeMs = currentTimeMs;
// 输出定位状态
Serial.println("定位状态: 有效");
// 输出时间
if (gps.time.isValid()) {
Serial.printf("当前时间 (UTC): %02d:%02d:%06.3f\n",
gps.time.hour(),
gps.time.minute(),
gps.time.second() + gps.time.centisecond() / 100.0);
} else {
Serial.println("当前时间: 无效");
}
// 输出日期
if (gps.date.isValid()) {
Serial.printf("日期: %04d-%02d-%02d\n",
gps.date.year(),
gps.date.month(),
gps.date.day());
}
// 输出经纬度
Serial.printf("纬度: %.6f°\n", lat);
Serial.printf("经度: %.6f°\n", lng);
// 输出本地ENU坐标(相对于参考点)
Serial.printf("东向偏移: %.3f m\n", east);
Serial.printf("北向偏移: %.3f m\n", north);
// 计算到参考点的距离
double distance = sqrt(east * east + north * north);
Serial.printf("距参考点距离: %.3f m\n", distance);
// 更新OLED显示
updateOLED(lat, lng, distance);
// 输出计算得到的速度和航向(基于本地墨卡托投影)
Serial.printf("对地航速 (计算): %.6f km/h\n", calcSpeed);
Serial.printf("航向角度 (计算): %.6f°\n", calcHeading);
// 输出原始GPS数据(对比)
Serial.printf("原始航速: %.6f km/h\n", gps.speed.kmph());
Serial.printf("原始航向: %.6f°\n", gps.course.deg());
// 输出卫星数量
Serial.printf("卫星数量: %d\n", gps.satellites.value());
// 输出HDOP (水平精度因子)
if (gps.hdop.isValid()) {
Serial.printf("HDOP: %.2f\n", gps.hdop.hdop());
}
// 输出海拔高度
if (gps.altitude.isValid()) {
Serial.printf("海拔高度: %.2f m\n", gps.altitude.meters());
}
} else {
Serial.println("定位状态: 无效 (请移至开阔地带)");
Serial.println("当前时间: --:--:--.---");
Serial.println("纬度: ---.------°");
Serial.println("经度: ---.------°");
Serial.println("对地航速: ---.------ km/h");
Serial.println("航向角度: ---.------°");
// 显示接收到的字符数
Serial.printf("已接收字符数: %lu\n", gps.charsProcessed());
// 更新OLED显示(无有效定位)
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 20, "No GPS Signal");
u8g2.drawStr(0, 40, "Move to open area");
u8g2.sendBuffer();
}
Serial.println("========================================");
Serial.println();
}
// 检查是否有解析错误
if (gps.charsProcessed() < 10) {
Serial.println("等待GPS数据...");
delay(500);
}
}
// 将经纬度转换为本地ENU坐标(东-北-天坐标系)
void latLngToENU(double lat, double lng, double refLat, double refLng, double& east, double& north) {
// 地球半径(米)
const double R = 6378137.0;
// 将角度转换为弧度
double latRad = lat * M_PI / 180.0;
double lngRad = lng * M_PI / 180.0;
double refLatRad = refLat * M_PI / 180.0;
double refLngRad = refLng * M_PI / 180.0;
// 计算cos(lat)用于缩放
double cosLat = cos(refLatRad);
// 计算ENU坐标
east = R * (lngRad - refLngRad) * cosLat;
north = R * (latRad - refLatRad);
}
// 更新OLED显示
void updateOLED(double lat, double lng, double distance) {
char buf[32];
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
// 显示纬度
sprintf(buf, "Lat: %.6f", lat);
u8g2.drawStr(0, 12, buf);
// 显示经度
sprintf(buf, "Lng: %.6f", lng);
u8g2.drawStr(0, 28, buf);
// 显示距离参考点距离
sprintf(buf, "Dist: %.2fm", distance);
u8g2.drawStr(0, 44, buf);
// 显示定位状态
u8g2.drawStr(0, 60, "GPS: OK");
u8g2.sendBuffer();
}
3.1 依赖库配置
在 platformio.ini 中添加U8g2库:
lib_deps =
mikalhart/TinyGPSPlus@^1.1.0
olikraus/U8g2@^2.35.14
3.2 OLED初始化与配置
#include <U8g2lib.h>
// SSD1306 OLED配置
#define OLED_SCK_PIN 12 // SCL
#define OLED_SDA_PIN 13 // SDA
// U8g2对象(I2C接口,SSD1306 128x64)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R0,
/* reset=*/U8X8_PIN_NONE,
/* clock=*/OLED_SCK_PIN,
/* data=*/OLED_SDA_PIN
);
void setup() {
// 初始化OLED屏幕
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 20, "GPS Initializing...");
u8g2.sendBuffer();
// ... GPS串口初始化 ...
}
代码解析:
- **
U8G2_SSD1306_128X64_NONAME_F_HW_I2C:使用硬件I2C驱动128x64分辨率的SSD1306屏幕 - **
u8g2_font_ncenB08_tr:选择8像素高度的字体,适合在小屏幕上显示清晰 - **
drawStr(x, y, text):在指定坐标绘制字符串,OLED坐标系原点在左上角
3.3 距离参考点距离计算
// 将经纬度转换为本地ENU坐标(东-北-天坐标系)
void latLngToENU(double lat, double lng, double refLat, double refLng, double& east, double& north) {
const double R = 6378137.0; // 地球半径(米)
double latRad = lat * M_PI / 180.0;
double lngRad = lng * M_PI / 180.0;
double refLatRad = refLat * M_PI / 180.0;
double refLngRad = refLng * M_PI / 180.0;
double cosLat = cos(refLatRad);
east = R * (lngRad - refLngRad) * cosLat;
north = R * (latRad - refLatRad);
}
// 在loop中计算距离
double east, north;
latLngToENU(lat, lng, refLat, refLng, east, north);
double distance = sqrt(east * east + north * north);
原理说明:
- 参考点:首次有效定位时的位置作为本地坐标系原点
- ENU坐标系:East-North-Up(东-北-天)局部平面坐标系
- 距离公式:
distance = √(east² + north²)
3.4 OLED显示更新函数
void updateOLED(double lat, double lng, double distance) {
char buf[32];
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
// 显示纬度
sprintf(buf, "Lat: %.6f", lat);
u8g2.drawStr(0, 12, buf);
// 显示经度
sprintf(buf, "Lng: %.6f", lng);
u8g2.drawStr(0, 28, buf);
// 显示距离参考点距离
sprintf(buf, "Dist: %.2fm", distance);
u8g2.drawStr(0, 44, buf);
// 显示定位状态
u8g2.drawStr(0, 60, "GPS: OK");
u8g2.sendBuffer();
}
显示布局(128x64像素):
┌────────────────────────────┐
│ Lat: 38.869190 │ ← y=12
│ Lng: 121.532056 │ ← y=28
│ Dist: 9.71m │ ← y=44
│ GPS: OK │ ← y=60
└────────────────────────────┘
3.5 信号状态处理
if (gps.location.isValid()) {
// 有有效定位 → 显示详细数据
updateOLED(lat, lng, distance);
} else {
// 无有效定位 → 显示提示信息
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 20, "No GPS Signal");
u8g2.drawStr(0, 40, "Move to open area");
u8g2.sendBuffer();
}
四、实战日志分析
下面是从串口监视器中截取的实际运行数据,让我们分析其中的关键信息:
4.1 初始定位阶段
========================================
定位状态: 有效
纬度: 38.869190°
经度: 121.532056°
东向偏移: -9.086 m
北向偏移: -3.432 m
距参考点距离: 9.713 m
卫星数量: 0
HDOP: 25.50
========================================
分析要点:
- ✅ 虽然显示"卫星数量: 0"但竟然有定位结果 → 这说明模块实际上还处于冷启动阶段,模块可能使用了上次的星历数据
- ⚠️ HDOP=25.50(非常高,正常应小于2.0以下才是高精度定位
- 📍 参考点已设置,后续数据都以此为原点
4.2 定位稳定阶段
========================================
定位状态: 有效
纬度: 38.869191°
经度: 121.531948°
东向偏移: -18.461 m
北向偏移: -3.303 m
距参考点距离: 18.754 m
卫星数量: 4
HDOP: 4.00
========================================
分析要点:
- ✅ 卫星数量增加到4颗,HDOP降低到4.00
- 📍 位置在距离参考点约18米处,说明设备在移动
- 🚶 移动方向向西(东向偏移-18m表示向西移动)
4.3 高精度定位阶段
========================================
定位状态: 有效
纬度: 38.869194°
经度: 121.531933°
东向偏移: -19.747 m
北向偏移: -2.969 m
距参考点距离: 19.968 m
卫星数量: 4
HDOP: 4.00
海拔高度: 104.10 m
========================================

分析要点:
- ✅ HDOP=4.00,定位精度尚可
- 📍 距离参考点距离稳定在20米左右
- 🌍 经纬度小数点后第6位开始稳定
4.4 信号改善阶段
========================================
定位状态: 有效
纬度: 38.869193°
经度: 121.531955°
东向偏移: -17.912 m
北向偏移: -3.080 m
距参考点距离: 18.175 m
卫星数量: 5
HDOP: 2.30
========================================

分析要点:
- ✅ 卫星数量增加到5颗
- ✅ HDOP降低到2.30,定位精度显著提升
- 📊 这是一组比较理想的数据
五、OLED屏幕实际显示效果
有信号时显示:
Lat: 38.869190
Lng: 121.532056
Dist: 9.71m
GPS: OK
无信号时显示:
No GPS Signal
Move to open area

六、技术要点总结
6.1 U8g2库的使用技巧
- 缓冲区机制:
clearBuffer()→ 绘制 →sendBuffer(),避免屏幕闪烁 - 字体选择:
ncenB08_tr是8像素高的字体,适合128x64屏幕 - 坐标系统:左上角为原点(0,0),向右向下递增
- 硬件I2C:比软件I2C更稳定,速度更快
6.2 GPS数据精度分析指标
| 指标 | 含义 | 推荐值 |
|---|---|---|
| 卫星数量 | 用于定位的卫星颗数 | ≥ 4颗 |
| HDOP | 水平精度因子 | < 2.0 (优秀) |
| 海拔高度 | 海拔高度数据 | - |
6.3 距离参考点距离的应用场景
- 🚶 步行导航:实时显示距离目标点的距离
- 🚗 车辆追踪:记录车辆驶离起点的距离
- 📍 位置记录:记录设备移动轨迹
- 🏃 运动测距:简单的运动距离计算
七、完整代码
完整代码已在项目中实现,核心逻辑包括:
- GPS串口数据读取与解析
- 本地ENU坐标转换
- 距离参考点距离计算
- SSD1306 OLED显示更新
八、下一步计划
在接下来的博客中,我们将继续扩展功能:
- 🎯 **计划三:添加LED状态指示与按键交互
- 📦 **计划四:SD卡数据记录与轨迹回放
- 🌐 **计划五:Web服务器远程监控
- 📡 **计划六:MQTT数据上传到云平台
参考资源
总结:通过集成SSD1306 OLED显示屏,我们将原本只能在串口监视器中看到的数据,以更直观的方式展示出来。这为后续的便携式GPS追踪器、导航设备等项目打下了良好的基础。
更多推荐
所有评论(0)