GPS Modules for Student Projects: Reading NMEA Sentences with Arduino and ESP32

A GPS module streams NMEA 0183 sentences over serial, and reading two of them unlocks every tracker and logger project. This guide decodes GGA and RMC field by field, shows the degrees-minutes conversion, covers safe 3.3 V wiring, and gives a minimal ESP32 parser and a Python parser with checksum checking.

Written by Projectech14 min readPublished
For Electronics and IoT students building tracker, logger or navigation prototypes with a GPS module and an Arduino or ESP32 who need to decode NMEA data reliably. Topics: GPS module, NMEA 0183, ESP32, Arduino, UART serial
Blueprint-style technical illustration of a GPS receiver module on a breadboard wired to a microcontroller, with satellites overhead and a route trace.
Illustration generated for this guide.
In this guide

GPS Modules for Student Projects: Reading NMEA Sentences with Arduino and ESP32

Almost every student tracker, vehicle logger, field-survey device and delivery-robot prototype starts the same way: a small GPS module is wired to a microcontroller, and the microcontroller suddenly has to make sense of a stream of text arriving at 9600 baud. That text has a name — NMEA 0183 — and once you can read one sentence type properly, every GPS project you build afterwards gets easier, because they all speak the same language.

This guide explains what a GPS module actually sends, how to decode the two sentences that matter most, how to wire the module without damaging it, and how to parse the data on an ESP32 and in Python. It finishes with the failure modes students hit most, and an honest account of how accurate a hobby GPS module really is.

What a GPS module actually sends

A typical module, such as the widely used NEO-6M family, is a complete GPS receiver on a small board: antenna, receiver chip, and a serial port. It needs no commands to start working. Power it, give it a view of the sky, and after an initial acquisition period it begins transmitting position sentences once per second over its UART transmit pin, by default at 9600 baud, 8 data bits, no parity, one stop bit.

Each sentence is one line of ASCII text. It starts with a dollar sign, carries a talker identifier, a three-letter sentence type, a set of comma-separated fields, and ends with an asterisk followed by a two-digit hexadecimal checksum. A receiver with a clear sky view typically emits a burst of several sentence types each second: position, velocity, satellite status, and signal quality. The microcontroller's job is not to read all of them — it is to pick the one or two it needs and ignore the rest, because parsing everything at 9600 baud on a small microcontroller is a waste of effort and a source of bugs.

Two details matter before any wiring. First, the module transmits continuously whether or not it has a position fix; sentences with an empty fix field are not errors, they are the module saying it is still searching. Second, the module's serial output is at its own logic level — 3.3 V on most modern boards — and that level must be respected on the microcontroller side, as the wiring section explains.

The sentences that matter

Sentence What it gives you Use it when
GGA — Global Positioning System Fix Data Time, latitude, longitude, fix quality, satellite count, altitude You need a position and altitude, the standard logging choice
RMC — Recommended Minimum Specific Data Time, date, latitude, longitude, speed over ground, course You need speed, course, or the date as well as position
GSA — Dilution of Precision and active satellites Fix mode, which satellites are used, DOP values You want a quality indicator for your fix
GSV — Satellites in View Each visible satellite's elevation, azimuth and signal strength You are debugging why a fix is slow or weak
VTG — Course Over Ground and Ground Speed True and magnetic course, speed in knots and km/h You need speed without parsing RMC

Talker identifiers tell you which constellation produced the sentence: GP means GPS, GL means GLONASS, GA means Galileo, and GN means a combined GNSS fix. A modern module often emits GN sentences. The field layout of GGA and RMC is the same regardless of talker, so a parser written for GPGGA works on GNGGA if it checks the last three characters rather than the whole prefix.

Reading a GGA sentence field by field

Take this textbook example sentence, of the kind used in NMEA documentation to illustrate the format:

$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

Reading it left to right after the sentence type:

  • 123519 — UTC time, 12:35:19. GPS time is UTC, not local time; convert to your timezone in software, never by adjusting the module.
  • 4807.038,N — latitude in degrees and decimal minutes format: 48 degrees, 07.038 minutes, North. This is the format that trips up beginners, because it is not decimal degrees.
  • 01131.000,E — longitude: 11 degrees, 31.000 minutes, East.
  • 1 — fix quality: 0 means no fix, 1 means a standard GPS fix, 2 means differential (DGPS) fix. Never trust a position from a sentence whose fix quality is 0.
  • 08 — number of satellites used in the fix.
  • 0.9 — horizontal dilution of precision (HDOP). Lower is better; below 1 is excellent, above 5 is poor.
  • 545.4,M — altitude above mean sea level, in metres.
  • 46.9,M — geoid separation, the difference between the GPS ellipsoid and mean sea level. Log it if you compare altitudes across devices, otherwise ignore it.
  • The two empty fields before the asterisk are the age of differential data and the differential station ID, both blank without a DGPS source.

Converting the degrees-and-decimal-minutes format to the decimal degrees that maps and distance maths expect is one division:

latitude  = 48 + 07.038 / 60 = 48.1173 degrees
longitude = 11 + 31.000 / 60 = 11.5167 degrees

Apply a negative sign for South latitudes and West longitudes. The most common student bug in GPS projects is plotting the raw 4807.038 value as if it were decimal degrees, which puts the marker kilometres from the real location — if your logged track looks right in shape but wrong in place, check this conversion first.

The checksum after the asterisk is the XOR of all characters between the dollar sign and the asterisk, written as two hexadecimal digits. Checking it costs almost nothing and catches the most common serial corruption: a sentence damaged by noise fails the checksum and should be discarded, not plotted. A parser that skips the checksum will eventually log a wild position jump from a corrupted sentence, usually on demo day.

Wiring the module without damaging it

The wiring is only four connections, but two of them are routinely got wrong:

  1. Module TX goes to microcontroller RX, and module RX goes to microcontroller TX. Serial links cross over. Connecting TX to TX is the single most common wiring mistake, and its symptom — total silence — looks identical to a dead module, which sends students off replacing hardware that was never faulty.
  2. Match the logic level. Most GPS modules run on 3.3 V logic. An Arduino Uno's 5 V TX pin feeding a 3.3 V module RX pin can damage the module's input. If your microcontroller is a 5 V board, use a simple resistive divider or a level shifter on the microcontroller-TX-to-module-RX line, or simply leave the module RX unconnected if you never need to send it commands — the module transmits its sentences regardless. The module TX to microcontroller RX direction is safe on a 5 V board, since a 3.3 V high is read as logic high.
  3. Common ground is mandatory. The module and the microcontroller must share a ground connection, or the serial signal has no reference and the data is garbage.
  4. Antenna placement decides everything. A GPS antenna needs a view of the sky. Indoors, near a window, or under a metal roof, the module may never get a fix, or may take many minutes. Test outdoors or at a window with a wide sky view, and allow a cold start — the first fix after the module has been off for a long time or moved a long distance — several minutes. A warm start, with recent satellite data still stored, is typically under a minute.

On an ESP32, use a hardware serial port rather than a software serial emulation: Serial2 on GPIO 16 (RX) and 17 (TX) is the usual choice. Hardware serial at 9600 baud is effortless for the ESP32, leaves the USB serial free for debugging, and avoids the timing glitches that software serial introduces when the sketch is busy elsewhere.

Parsing on an ESP32: a minimal GGA reader

The sketch below reads Serial2, collects one line at a time, checks that it is a GGA sentence with a valid fix, converts the coordinates, and prints them. It deliberately avoids any GPS library so that the parsing logic is visible — libraries are excellent for finished projects, but they hide exactly the format this guide is teaching. Keep the line buffer small and discard overlong lines rather than letting them overflow.

#include <Arduino.h>

const int GPS_RX = 16;
const int GPS_TX = 17;
char line[128];
int pos = 0;

double toDecimalDegrees(const char *field, char hemi) {
  double raw = atof(field);
  int degrees = (int)(raw / 100);
  double minutes = raw - degrees * 100;
  double value = degrees + minutes / 60.0;
  if (hemi == 'S' || hemi == 'W') value = -value;
  return value;
}

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
}

void loop() {
  while (Serial2.available()) {
    char c = Serial2.read();
    if (c == '\n') {
      line[pos] = '\0';
      pos = 0;
      if (strncmp(line, "$GPGGA", 6) == 0 ||
          strncmp(line, "$GNGGA", 6) == 0) {
        char copy[128];
        strncpy(copy, line, sizeof(copy));
        char *field[15];
        int n = 0;
        char *token = strtok(copy, ",");
        while (token && n < 15) { field[n++] = token; token = strtok(NULL, ","); }
        if (n > 9 && field[6][0] != '0') {
          double lat = toDecimalDegrees(field[2], field[3][0]);
          double lon = toDecimalDegrees(field[4], field[5][0]);
          Serial.print("Fix: ");
          Serial.print(lat, 6);
          Serial.print(", ");
          Serial.println(lon, 6);
        }
      }
    } else if (c != '\r' && pos < 127) {
      line[pos++] = c;
    }
  }
}

Two habits in this sketch are worth copying into your own code. First, it checks the fix-quality field before trusting any coordinate — a zero there means the latitude and longitude fields are empty or stale. Second, it never blocks waiting for GPS data: the loop keeps running and processes characters as they arrive, so the rest of your sketch stays responsive.

Parsing in Python: GGA with a checksum check

The same logic on a computer, for logging or testing a module over a USB serial adapter, is shorter and adds the checksum verification the sketch omits for brevity:

def nmea_checksum_ok(sentence):
    body, _, given = sentence[1:].partition("*")
    calc = 0
    for ch in body:
        calc ^= ord(ch)
    return format(calc, "02X") == given.strip().upper()

def parse_gga(sentence):
    if not nmea_checksum_ok(sentence):
        return None
    fields = sentence.strip().split(",")
    if fields[6] == "0" or not fields[2]:
        return None
    lat_raw, lat_hemi = float(fields[2]), fields[3]
    lon_raw, lon_hemi = float(fields[4]), fields[5]
    lat = int(lat_raw // 100) + (lat_raw % 100) / 60.0
    lon = int(lon_raw // 100) + (lon_raw % 100) / 60.0
    if lat_hemi == "S":
        lat = -lat
    if lon_hemi == "W":
        lon = -lon
    return {"lat": round(lat, 6), "lon": round(lon, 6),
            "alt_m": fields[9], "sats": fields[7], "hdop": fields[8]}

Run this against a recorded log file before wiring it into a live project. A log-file test separates parsing bugs from wiring bugs: if the parser handles a recorded file correctly but live data fails, the problem is electrical or baud-related, not in the parser.

Common failures, and what each one actually means

Symptom Most likely cause, in order to check
No output at all on the serial monitor TX and RX not crossed; baud rate not 9600; module not powered; ground not shared
Output, but it is unreadable characters Baud mismatch — try 9600, then 38400 and 115200; check the logic level
Sentences arrive but fix quality stays 0 No sky view — move outdoors; antenna disconnected or covered; cold start still in progress, wait several minutes
Position jumps by kilometres between readings Raw degrees-minutes value plotted as decimal degrees; or a corrupted sentence accepted without a checksum check
Position wanders by tens of metres while stationary Normal for a hobby module: multipath reflections and satellite geometry shift the fix; see the accuracy section
Time is right but date is wrong or missing You parsed GGA, which carries no date — read RMC for the date field
Fix works on the bench but fails in the device Antenna now enclosed in a metal case or mounted under a PCB ground plane; reroute the antenna to a clear position

How accurate is a hobby GPS module, honestly?

Under an open sky, a typical hobby module reports positions within about 2 to 5 metres of the truth most of the time, with occasional excursions to 10 metres or more. That is good enough for vehicle tracking, route logging, geofencing a field, and speed display. It is not good enough for lane-level navigation, surveying, or any measurement that needs centimetre precision — those need survey-grade receivers or correction services that are outside the scope and budget of a student build.

Three factors move the error around. Satellite geometry, reported as DOP, matters more than satellite count alone: eight satellites bunched in one part of the sky give a worse fix than five spread widely. Multipath — signals bouncing off buildings, vehicles and wet ground before reaching the antenna — is the main error source in cities and the reason a stationary logger's track looks like a wandering scribble. And altitude from a GPS module is always noisier than horizontal position, typically two to three times worse, so do not use GPS altitude for any measurement that needs precision.

Design your project around these limits: average several readings for a stationary point, filter single wild jumps by checking the checksum and by rejecting steps that imply impossible speeds, and never present a GPS reading as more precise than the module can support. A tracker that honestly reports "within a few metres" is a better project than one that prints six decimal places and implies centimetre accuracy it does not have.

Project ideas that use this well

  • Route logger. Record GGA sentences to an SD card once per second on a walk or ride, then convert the log to a map track in Python. The whole project is parsing, logging, and one conversion formula.
  • Speed display. Parse RMC speed over ground and show it on a small OLED display, with a maximum-speed record. Speed from GPS is surprisingly stable and makes a satisfying bench demo.
  • Geofence alert. Store a centre point and a radius, compute the distance to each new fix with the haversine formula, and sound a buzzer or send a message when the device leaves the circle. Test the radius honestly: with a few metres of fix noise, a 10-metre fence will false-trigger.
  • Field survey helper. Log a fix at each sample point with a button press, including satellite count and HDOP alongside the coordinates, so the survey data carries its own quality record.

Frequently asked questions

Do I need an internet connection or a SIM card for GPS?
No. GPS is a receive-only system: the module listens to satellite broadcasts and computes its own position. Internet or mobile data only enters the picture if you want to send the position somewhere else, such as to a phone or a server.

Why does my module take so long to get its first fix?
A cold start needs the satellite almanac and ephemeris data, broadcast slowly by the satellites themselves, which takes minutes. Later starts reuse stored data and are much faster, as long as the module's backup battery or supercapacitor has kept its memory alive between sessions.

Can I use the module indoors for testing?
Only near a window with a wide sky view, and even then unreliably. For bench testing, record a real outdoor log once and replay it through your parser — that gives repeatable test data without depending on satellite reception at your desk.

Should I use a GPS library or parse the sentences myself?
For a finished project, use a well-tested library: it handles edge cases, checksums and multiple sentence types. For learning, parse GGA and RMC by hand once, as this guide does, so that when the library behaves unexpectedly you understand what it is doing underneath.

What is the difference between GPS, GNSS and the module's talker codes?
GPS is the American constellation; GNSS is the generic term covering GPS, GLONASS, Galileo and BeiDou together. A module that tracks several constellations reports combined fixes with the GN talker code and usually acquires faster and holds a fix better than a GPS-only receiver.

Does the module need a SIM card or an account to work?
No. The confusion comes from tracker products that combine a GPS module with a GSM module for sending positions; the GPS half works standalone, and the GSM half is a separate function with its own SIM requirement.

Limitations

This guide covers consumer GPS modules speaking NMEA 0183 over UART at hobby-project scale. It does not cover RTK or PPP correction, survey-grade receivers, assisted-GPS server protocols, or the binary protocols some modules offer alongside NMEA. Position accuracy figures here are typical values for open-sky hobby use, not guarantees, and they degrade indoors, in urban canyons, and under heavy tree cover. Never rely on a hobby GPS module alone for any safety-critical navigation, and check the current regulations before deploying a tracker on a vehicle you do not own.

More project guides

More in IoT & Embedded