NMEA 0183 Practical Guide

NMEA 0183 is a text protocol used by GNSS/GPS modules, marine devices, and many embedded products. It is simple, stable, and still common in production systems.

If you work with UART logs from a GPS module, this is the format you will usually see.

1) NMEA 0183 sentence pattern

A standard sentence looks like:

$GPRMC,093000.00,A,5231.1234,N,01324.5678,E,2.1,84.4,240726,,,A*6C

Pattern:

$ttsss,data1,data2,...*hh
  • $ starts the sentence
  • tt is talker ID (GP, GN, GL, GA, …)
  • sss is sentence type (RMC, GGA, GSA, GSV, VTG, …)
  • , separates fields
  • *hh is checksum in hex (XOR of bytes between $ and *)

2) Talker IDs you will see

  • GP: GPS
  • GL: GLONASS
  • GA: Galileo
  • GB: BeiDou
  • GN: combined multi-GNSS output

In modern modules, GN is common.

3) Most useful sentence types

Gives time, status, position, speed over ground, course, date.

$GNRMC,093001.00,A,5231.1234,N,01324.5678,E,1.9,83.1,240726,,,A*6E

GGA (fix data)

Gives altitude, fix quality, satellite count, HDOP.

$GNGGA,093001.00,5231.1234,N,01324.5678,E,1,10,0.9,51.2,M,46.3,M,,*59

GSV (satellites in view)

Usually arrives as multi-part messages.

$GPGSV,3,1,10,02,67,123,40,05,15,045,35,12,45,300,42,17,30,210,37*70
$GPGSV,3,2,10,19,20,150,33,24,55,060,44,25,40,320,39,29,10,010,28*78
$GPGSV,3,3,10,31,75,250,41,36,25,100,34*4A

4) Checksum rule (critical)

Calculate XOR of all bytes between $ and *. Do not include $ or * or CR/LF.

Example command line check:

payload='GPRMC,093000.00,A,5231.1234,N,01324.5678,E,2.1,84.4,240726,,,A'
python3 - << 'PY'
payload = "GPRMC,093000.00,A,5231.1234,N,01324.5678,E,2.1,84.4,240726,,,A"
cs = 0
for ch in payload:
    cs ^= ord(ch)
print(f"{cs:02X}")
PY

5) Full practical parser example (Python)

This parser:

  • validates start and checksum
  • extracts talker and message type
  • keeps raw fields
  • converts latitude/longitude to decimal degrees
  • ignores malformed lines safely
import re
from dataclasses import dataclass
from typing import Optional, List

NMEA_RE = re.compile(r"^\$(?P<body>[^*]+)\*(?P<cs>[0-9A-Fa-f]{2})$")

@dataclass
class NMEASentence:
    raw: str
    talker: str
    msg_type: str
    fields: List[str]


def checksum_ok(raw_line: str) -> bool:
    m = NMEA_RE.match(raw_line.strip())
    if not m:
        return False

    body = m.group("body")
    expected = int(m.group("cs"), 16)
    actual = 0
    for ch in body:
        actual ^= ord(ch)
    return actual == expected


def parse_nmea(raw_line: str) -> Optional[NMEASentence]:
    line = raw_line.strip()
    if not checksum_ok(line):
        return None

    body = line[1:line.index("*")]
    parts = body.split(",")
    if not parts or len(parts[0]) < 5:
        return None

    header = parts[0]
    talker = header[:2]
    msg_type = header[2:]
    return NMEASentence(raw=line, talker=talker, msg_type=msg_type, fields=parts[1:])


def dm_to_decimal(dm: str, direction: str) -> Optional[float]:
    """
    Convert NMEA ddmm.mmmm or dddmm.mmmm to decimal degrees.
    Latitude uses 2 degree digits, longitude uses 3.
    """
    if not dm or not direction:
        return None

    if direction in ("N", "S"):
        deg_digits = 2
    elif direction in ("E", "W"):
        deg_digits = 3
    else:
        return None

    try:
        deg = float(dm[:deg_digits])
        minutes = float(dm[deg_digits:])
    except ValueError:
        return None

    value = deg + minutes / 60.0
    if direction in ("S", "W"):
        value = -value
    return value


def extract_position(sentence: NMEASentence):
    if sentence.msg_type == "RMC":
        # RMC: time,status,lat,NS,lon,EW,sog,cog,date,...
        f = sentence.fields
        if len(f) < 6 or f[1] != "A":
            return None
        lat = dm_to_decimal(f[2], f[3])
        lon = dm_to_decimal(f[4], f[5])
        return {"type": "RMC", "lat": lat, "lon": lon, "time": f[0], "date": f[8] if len(f) > 8 else ""}

    if sentence.msg_type == "GGA":
        # GGA: time,lat,NS,lon,EW,fix,sats,hdop,alt,...
        f = sentence.fields
        if len(f) < 9 or f[5] == "0":
            return None
        lat = dm_to_decimal(f[1], f[2])
        lon = dm_to_decimal(f[3], f[4])
        return {"type": "GGA", "lat": lat, "lon": lon, "time": f[0], "alt_m": f[8]}

    return None


if __name__ == "__main__":
    lines = [
        "$GNRMC,093001.00,A,5231.1234,N,01324.5678,E,1.9,83.1,240726,,,A*6E",
        "$GNGGA,093001.00,5231.1234,N,01324.5678,E,1,10,0.9,51.2,M,46.3,M,,*59",
        "$GPRMC,093000.00,V,,,,,,,240726,,,N*4F",  # invalid fix
        "$GNRMC,broken*00",  # malformed
    ]

    for line in lines:
        s = parse_nmea(line)
        if not s:
            print("DROP:", line)
            continue

        info = extract_position(s)
        if info:
            print("OK:", info)
        else:
            print("PASS:", s.msg_type)

Expected behavior:

  • valid RMC/GGA with checksum: parsed
  • invalid checksum or malformed line: dropped
  • no-fix lines: ignored for position output

6) Runtime pattern for real devices

Use this pattern in embedded Linux or MCU gateway software:

  1. Read UART stream into a ring buffer
  2. Split by \r\n into full lines
  3. Check $ prefix and checksum first
  4. Whitelist sentence types (RMC, GGA, maybe VTG)
  5. Convert coordinates and publish normalized output
  6. Keep raw line for trace/debug

Minimal serial reader loop (Linux, Python):

import serial

ser = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=1)

while True:
    raw = ser.readline().decode("ascii", errors="ignore").strip()
    if not raw:
        continue

    sentence = parse_nmea(raw)
    if not sentence:
        continue

    if sentence.msg_type not in ("RMC", "GGA"):
        continue

    data = extract_position(sentence)
    if data:
        print(data)

7) Practical pitfalls

  • Wrong baud rate (many modules default to 9600)
  • Mixing NMEA 0183 and vendor binary protocols on same port
  • Parsing without checksum validation
  • Confusing V (invalid) and A (active) status in RMC
  • Ignoring multi-part messages like GSV

Quick checklist

  • Validate checksum before parse
  • Use only needed sentence types
  • Handle no-fix data correctly
  • Convert lat/lon once in a shared utility
  • Log dropped lines for troubleshooting

That is enough to build a robust first version quickly.