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 sentencettis talker ID (GP,GN,GL,GA, …)sssis sentence type (RMC,GGA,GSA,GSV,VTG, …),separates fields*hhis checksum in hex (XOR of bytes between$and*)
2) Talker IDs you will see
GP: GPSGL: GLONASSGA: GalileoGB: BeiDouGN: combined multi-GNSS output
In modern modules, GN is common.
3) Most useful sentence types
RMC (recommended minimum)
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:
- Read UART stream into a ring buffer
- Split by
\r\ninto full lines - Check
$prefix and checksum first - Whitelist sentence types (
RMC,GGA, maybeVTG) - Convert coordinates and publish normalized output
- 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) andA(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.