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库的使用技巧

  1. 缓冲区机制clearBuffer() → 绘制 → sendBuffer(),避免屏幕闪烁
  2. 字体选择ncenB08_tr 是8像素高的字体,适合128x64屏幕
  3. 坐标系统:左上角为原点(0,0),向右向下递增
  4. 硬件I2C:比软件I2C更稳定,速度更快

6.2 GPS数据精度分析指标

指标 含义 推荐值
卫星数量 用于定位的卫星颗数 ≥ 4颗
HDOP 水平精度因子 < 2.0 (优秀)
海拔高度 海拔高度数据 -

6.3 距离参考点距离的应用场景

  • 🚶 步行导航:实时显示距离目标点的距离
  • 🚗 车辆追踪:记录车辆驶离起点的距离
  • 📍 位置记录:记录设备移动轨迹
  • 🏃 运动测距:简单的运动距离计算

七、完整代码

完整代码已在项目中实现,核心逻辑包括:

  1. GPS串口数据读取与解析
  2. 本地ENU坐标转换
  3. 距离参考点距离计算
  4. SSD1306 OLED显示更新

八、下一步计划

在接下来的博客中,我们将继续扩展功能:

  • 🎯 **计划三:添加LED状态指示与按键交互
  • 📦 **计划四:SD卡数据记录与轨迹回放
  • 🌐 **计划五:Web服务器远程监控
  • 📡 **计划六:MQTT数据上传到云平台

参考资源


总结:通过集成SSD1306 OLED显示屏,我们将原本只能在串口监视器中看到的数据,以更直观的方式展示出来。这为后续的便携式GPS追踪器、导航设备等项目打下了良好的基础。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐