This project builds a self-contained location display: a controller reads a real satellite fix from a dedicated GPS module and shows your current latitude and longitude on a small screen, entirely offline. No phone, no cell signal, and no internet connection are involved once the code is running -- the fix comes directly from GPS satellites.
What the Controller Does
The ESP32-C3 SuperMini reads a continuous stream of data from the GPS module over a serial (UART) connection, parses out the fields that matter -- latitude, longitude, and altitude -- and redraws the OLED display with the current position on a regular interval.
What the GPS Module Does
The NEO-6M GPS module does the actual satellite work: it tracks GPS satellites, computes a position fix, and streams the result out continuously as standard NMEA sentences over UART. NMEA is a well-established, widely supported text format, which is why mature parsing libraries exist for essentially every microcontroller platform -- you don't need to hand-parse the raw sentences yourself.
What the OLED Display Does
The 0.96" OLED simply renders whatever the controller has most recently parsed from the GPS -- latitude, longitude, and optionally altitude or fix status -- refreshed each cycle so the screen always reflects the latest data the module has reported.
UART Wiring Basics
The GPS module communicates over UART, a simple two-wire serial link: the module's TX (transmit) pin connects to the controller's RX (receive) pin, and vice versa. Getting TX and RX crossed -- connecting TX to TX -- is an extremely common beginner mistake and the first thing to check if nothing is coming through.
Software and Library Concepts
Start from a well-tested NMEA parsing library for your controller platform rather than writing sentence parsing from scratch -- NMEA has a defined checksum and field structure that's easy to get subtly wrong by hand. Once the library reports it's receiving valid sentences, extracting latitude and longitude is typically a couple of function calls.
Testing and Troubleshooting
No fix ever appears, even after waiting -- move to a location with a genuinely clear, unobstructed view of the sky. GPS reception does not work indoors or under heavy cover, and this is the single most common reason a GPS module 'doesn't work.'
Cold start takes a long time to get a first fix -- this is normal GPS behavior, not a defect. Time to first fix can reasonably take anywhere from about 30 seconds to several minutes on a cold start, depending on satellite visibility.
Serial data looks garbled or nothing appears at all on the UART connection -- double-check that TX and RX are not swapped between the module and the controller; this is the most common wiring mistake with any UART peripheral.
Display shows 0,0 as the coordinates -- this means no valid fix has been acquired yet, not a bug in your code. Wait longer and/or move somewhere with a clearer sky view before assuming something is broken.
Optional Future Upgrade: Logging a Track Over Time
Once a live fix is displaying reliably, a natural next step is logging a series of fixes over time -- whether to local storage or, combined with the concepts from a wireless data logger project, sent out over a network -- to build a simple track of movement rather than only showing the current position.
Wiring
NEO-6M VCC → ESP32-C3 SuperMini 5V (module has an onboard regulator, tolerant of 5V)
GPS needs a clear view of the sky — indoors, expect "no fix" and a satellite count of 0 for a long time, or forever; that's normal, not a wiring fault. First fix ("cold start") commonly takes 30 seconds to a few minutes; subsequent fixes after a brief power loss are usually much faster. The ESP32-C3's pin matrix lets `HardwareSerial` be remapped to almost any GPIO, so GPS_RX_PIN/GPS_TX_PIN aren't fixed hardware UART pins — adjust the `#define`s if you wire to different GPIOs.
Wiring Diagram
Starter Code
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <TinyGPSPlus.h>
#define SDA_PIN 8 // ESP32-C3 default I2C SDA - adjust if your board differs
#define SCL_PIN 9 // ESP32-C3 default I2C SCL - adjust if your board differs
#define GPS_RX_PIN 20 // to GPS module TX - any free GPIO (remapped via pin matrix)
#define GPS_TX_PIN 21 // to GPS module RX - adjust to match your wiring if different
#define GPS_BAUD 9600 // NEO-6M default NMEA output baud rate
#define OLED_ADDRESS 0x3C
Adafruit_SSD1306 display(128, 64, &Wire, -1);
TinyGPSPlus gps;
HardwareSerial gpsSerial(1); // use UART1
unsigned long lastFixTime = 0;
void setup() {
Serial.begin(115200);
gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
Wire.begin(SDA_PIN, SCL_PIN);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("SSD1306 init failed");
while (true) delay(1000);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Waiting for GPS fix...");
display.println("(needs clear sky view,");
display.println("can take a few minutes)");
display.display();
}
void loop() {
while (gpsSerial.available() > 0) {
gps.encode(gpsSerial.read());
}
if (gps.location.isUpdated() && gps.location.isValid()) {
lastFixTime = millis();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("GPS Fix");
display.setCursor(0, 14);
display.printf("Lat: %.6f\n", gps.location.lat());
display.printf("Lon: %.6f\n", gps.location.lng());
if (gps.altitude.isValid()) {
display.printf("Alt: %.1f m\n", gps.altitude.meters());
}
if (gps.satellites.isValid()) {
display.printf("Sats: %d\n", gps.satellites.value());
}
display.display();
Serial.printf("Lat: %.6f Lon: %.6f Sats: %d\n",
gps.location.lat(), gps.location.lng(), gps.satellites.value());
} else if (lastFixTime == 0 && millis() % 2000 < 20) {
// still searching for first fix - a normal wait indoors or under poor sky view
Serial.println("Searching for GPS fix...");
}
}