Mastering NMEA 0183: Why Your Device Needs to Read Between the Commas
TL;DR
- NMEA 0183 is the default readable output layer for most GNSS receivers. It carries position, time, velocity, satellite status, and fix quality as comma-separated ASCII sentences.
-
The checksum is simple but non-negotiable. XOR every byte between
$and*, then compare it with the two hex digits after the asterisk. - For RTK workflows, GGA is the first sentence to trust-check. Field 6 tells you whether the rover is Single, DGPS, RTK Float, or RTK Fixed.
- A production parser should count delimiters, handle null fields, verify checksums, and survive broken serial chunks.
NMEA 0183 is the ASCII sentence protocol most GNSS receivers use to expose position, time, velocity, satellite status, and RTK fix quality over simple serial links.
Wiring up a GNSS module mostly comes down to reading a serial stream. That stream usually relies on NMEA 0183: a comma-separated ASCII protocol from the early days of digital marine electronics, still widely used to expose position, time, and fix status.
NMEA 0183 survived because of its raw simplicity. It is human-readable and requires only a single UART pin to bring up. This guide walks through the parts engineers actually touch: sentence structure, checksum verification, parser design, common serial-stream failures, and RTK status decoding from GGA and RMC output. The examples are intentionally small — they show the parsing logic clearly; production code should still add logging, timeouts, and device-specific handling.
What Is NMEA 0183? (And Why It Outlived Marine Electronics)
The 1983 Marine Origin Story
NMEA stands for the National Marine Electronics Association, a US trade organization founded in 1957. In 1983 they published the NMEA 0183 standard to solve a specific problem: marine GPS receivers, fish finders, autopilots, and radars all spoke incompatible languages. A receiver from one vendor could not reliably share data with an autopilot or display from another.
The result was a one-way serial protocol, RS-422 at 4800 baud, with messages that any device could parse: ASCII text, comma-separated fields, line-terminated by <CR><LF>. No binary framing, no negotiation, no acknowledgment. Two wires, a serial terminal, and the receiver output was directly readable.
Why GNSS Engineers Inherited a Marine Protocol
Civilian GPS inherited NMEA 0183 because marine receivers were among the earliest commercial GPS users, and the protocol already solved the basic receiver-to-display problem. The protocol followed GPS into aviation, surveying, agriculture, robotics, and eventually any device that needs to know where it is.
NMEA 2000 later moved marine equipment onto CAN-bus, but embedded GNSS stayed with NMEA 0183 for simpler reasons: a UART pin is enough to receive it, the output is readable in a serial terminal, and forty years of vendor implementations means support is available on almost every GNSS receiver you are likely to integrate.
The Evolution Timeline: v3.01 → v4.10 → v4.11 → v4.30
The protocol evolved as constellations multiplied. Here is what actually matters when reading someone else's integration document or receiver datasheet:
| Version | Year | Key Addition | Engineering Relevance in 2026 |
|---|---|---|---|
v3.01 |
2002 | Last GPS-only era version | Still appears in legacy GPS-only devices and older integration examples |
v4.10 |
2012 | Nav Status field, Signal ID, GN Talker | Common reference point in GNSS module manuals and integration notes |
v4.11 |
2018 | BeiDou (GB), QZSS (GQ), NavIC (GI) Talker IDs | Important for BeiDou / QZSS / NavIC handling |
v4.30 |
2023 | Latest official revision (Dec 2023) | Adoption varies by receiver vendor and firmware generation |
This article does not replace the licensed NMEA specification. It focuses on the receiver behaviors GNSS developers usually need during integration.
Related Reference
For complete byte-level field tables and version-by-version differences, see AN-001: NMEA 0183 v4.10 Protocol Structure & Sentence Dictionary →
NMEA 0183 At a Glance — A 5-Fact Quick Card
If you only have 30 seconds, here is the protocol architecture:
# NMEA 0183 in 5 facts:
Format │ $TTSSS,field1,field2,...,fieldN*HH<CR><LF>
Encoding │ ASCII (printable + comma + asterisk)
Length │ ≤ 82 characters per sentence (incl. CR/LF)
Checksum │ XOR of all bytes between $ and *
Output │ UART, 4800 / 9600 / 38400 / 115200 baud common
That one mental model sets up the rest of this guide: parse by delimiters, verify the line, never assume the talker ID, and never treat serial reads as sentence boundaries. The next sections show why each of those matters.
Anatomy of an NMEA Sentence
A Real GGA Sentence, Field by Field
A GGA sentence is the easiest place to see how NMEA works: one address field, a sequence of comma-separated values, and a checksum at the end.
$GNGGA,072446.00,3114.562,N,12128.050,E,4,21,0.6,15.2,M,8.9,M,1.0,0031*50
| Field | Value | Meaning |
|---|---|---|
| Address | $GNGGA |
GN = combined GNSS talker; GGA = fix data sentence |
| 1 | 072446.00 |
UTC fix time, without date |
| 2-3 | 3114.562,N |
Latitude in DDMM.MMMM format + hemisphere |
| 4-5 | 12128.050,E |
Longitude in DDDMM.MMMM format + hemisphere |
| 6 | 4 |
Quality indicator; 4 means RTK Fixed |
| 7-8 | 21,0.6 |
Satellites used and HDOP |
| 9-12 | 15.2,M,8.9,M |
Altitude above MSL and geoid separation, both in meters |
| 13-14 | 1.0,0031 |
Differential data age and station ID |
| Checksum | *50 |
XOR checksum for line integrity |
Two critical details: First, the UTC time field carries only hhmmss.ss — there is no date. If your application logs timestamps or syncs to a system clock, combine this with date from RMC or ZDA, or you will hit the rollover trap discussed in the UTC rollover section below.
Second, coordinates use DDMM.MMMM format, not decimal degrees. The "3114.562" is 31 degrees, 14.562 minutes — not 3114.562 degrees. Map APIs (Google, Mapbox, Leaflet) all want decimal degrees, so every parser eventually needs a DDMM.MMMM → decimal conversion helper. Decimal degrees alone are not enough — the coordinates still depend on which geodetic datum the receiver uses, almost always WGS84. Beyond WGS84 explains why decoding the format right does not guarantee the point lands on the right map.
The 82-Character Convention (Not a Hard Rule)
The standard says each sentence — including the trailing <CR><LF> — should be at most 82 characters. The limit comes from the 1980s, when marine equipment shipped with a few KB of RAM and engineers wanted predictable buffer sizes.
In practice, vendor-specific NMEA extensions routinely break the limit. u-blox's $PUBX and other vendor-specific NMEA-style sentences can run longer than strict legacy parsers expect. Some receivers also expose separate proprietary binary protocols (UBX, BINR, OEM-style messages) — those should be handled as distinct formats, not as extended NMEA. Set a sane buffer ceiling (256 bytes is comfortable for everything except heavily-extended streams), but log overlength lines instead of crashing.
# Standard compliant:
$GNGGA,072446.00,3114.562,N,12128.050,E,4,21,...,0031*50<CR><LF>
└─ Standard sentence: ≤ 82 chars including CR/LF
# Vendor extension (often longer than 82):
$PUBX,00,072446.00,3114.5621234,N,12128.0501234,E,15.23,F3,...*XX<CR><LF>
(proprietary, ignore if you don't recognize the vendor code)
Talker IDs: GP, GL, GA, GB, GQ, GI, GN
The two characters right after $ tell you which constellation generated the sentence:
| Talker ID | Source | Notes |
|---|---|---|
GP |
GPS | Legacy GPS-only receivers, rare in 2026 |
GL |
GLONASS | Russian system, rarely standalone |
GA |
Galileo | European system, rarely standalone |
GB |
BeiDou | v4.11+, common in Asia-Pacific receivers |
GQ |
QZSS | v4.11+, Japanese regional |
GI |
NavIC / IRNSS | v4.11+, Indian regional |
GN |
Combined GNSS solution | What modern receivers actually output |
Engineer’s Takeaway
If your parser expects $GPGGA and the receiver outputs $GNGGA, every sentence will be silently dropped. This is a common source of "why is my parser broken after a firmware update?" failures. Match on the sentence ID (chars 3–5), not the talker ID (chars 1–2). The fix is small, but the failure mode is easy to miss.
Null Fields and the (0,0) Trap
The protocol has a strict rule for missing data: use empty fields, never zeros. A receiver that has not yet acquired a fix outputs commas with nothing between them. Some buggy receivers emit 0.000 instead — and that is how robots end up "teleporting" to the Gulf of Guinea (coordinates 0, 0).
# Correct: empty fields preserve "no data" semantic
$GNGGA,072446.00,,,,,0,00,99.9,,,,,,*XX ← no fix yet, 0 satellites used
# Wrong (some buggy receivers emit this):
$GNGGA,072446.00,0.000,N,0.000,E,0,00,... ← (0, 0) is the Gulf of Guinea
your robot just teleported to Africa
# Defensive parsing:
lat = float(fields[2]) if fields[2] else None
if lat is None or quality == 0:
skip_this_fix()
The NMEA Sentences You'll Actually See in GNSS Logs
NMEA 0183 defines dozens of sentence types, most of them for marine equipment that GNSS engineers will never touch (autopilots, radar, sonar, depth sounders). For positioning work, the same ten or so sentences show up everywhere. Here is the scannable map:
| What you need | Parse first | Why |
|---|---|---|
| Position + RTK state | GGA |
Fix quality, altitude, satellites, HDOP |
| Position + date | RMC |
Minimum navigation record, parses standalone |
| Lightweight position | GLL |
Latitude, longitude, and time in legacy logs |
| Position + per-constellation mode | GNS |
Multi-constellation alternative to GGA |
| Satellite geometry | GSA |
DOP values + satellites used in solution |
| Signal visibility | GSV |
Satellites in view with elevation, azimuth, SNR |
| Time and date only | ZDA |
Clean UTC date and time, no position payload |
| True heading, if available | HDT |
True heading from a heading-capable receiver, usually dual-antenna |
Field-by-field tables for each sentence are in AN-001. The H3 sections below cover the engineering context — when to use each one and why.
Position & Time Sentences: GGA, RMC, GLL, GNS, ZDA
GGA is the main RTK status sentence because Field 6 reports fix quality. RMC adds date and course-over-ground, which makes it useful for logging and timestamping. GLL is lighter: latitude, longitude, and time only, mostly in legacy logs.
GNS exposes per-constellation fix mode when the receiver supports it. For PPS-disciplined timing, such as IMU synchronization, ZDA gives UTC date and time without a position payload.
Velocity & Heading Sentences: VTG and HDT
VTG reports speed over ground and course. HDT appears only when the receiver or host can provide true heading, usually from a dual-antenna baseline or another heading-capable source.
Satellite Status Sentences: GSA, GSV, GST
GSA reports the satellites used in the current solution and DOP values. GSV lists satellites in view with elevation, azimuth, and signal-to-noise ratio; it is paginated when more than four satellites are visible. The pagination behavior is covered in the GSV pagination section below.
GST is less commonly parsed, but useful during diagnostics because it carries pseudorange error statistics.
What a Typical 1 Hz Output Stream Looks Like
A modern multi-constellation receiver covering GPS, BeiDou, and Galileo at 1 Hz outputs roughly 7-9 sentences per second:
# Typical 1 Hz output cycle from a multi-constellation receiver:
$GNRMC,072446.00,A,3114.562,N,12128.050,E,10.5,180.0,200625,0.0,E,A*1D
$GNVTG,180.0,T,180.0,M,10.5,N,19.4,K,A*35
$GNGGA,072446.00,3114.562,N,12128.050,E,4,21,0.6,15.2,M,8.9,M,1.0,0031*50
$GNGSA,A,3,03,07,11,14,,,,,,,,,1.5,0.6,1.2*2C # GPS satellites used in solution
$GBGSA,A,3,01,03,06,11,,,,,,,,,1.5,0.6,1.2*25 # BeiDou satellites used in solution
$GAGSA,A,3,02,05,09,11,,,,,,,,,1.5,0.6,1.2*2C # Galileo satellites used in solution
$GPGSV,3,1,12,03,45,150,42*48 # GPS in view (page 1 of 3)
$GBGSV,2,1,08,01,32,120,38*58 # BeiDou in view
$GAGSV,1,1,04,02,55,090,40*53 # Galileo in view
$GNGLL,3114.562,N,12128.050,E,072446.00,A,A*7F
That's roughly 760 characters per second, or about 7.6 kbps with framing overhead. Bandwidth becomes a constraint above 5 Hz on legacy 9600 baud links — see the baud rate guidance below.
Companion Code on GitHub
The Python implementations in the following sections — checksum validation, coordinate conversion, parsing, GSV pagination, serial reading, and RTK fix monitoring — are available as runnable examples in the Kalmix NMEA Toolkit. The article keeps the core logic inline; the repository contains implementation-ready helpers for developers.
Calculating the NMEA 0183 Checksum
What the Checksum Actually Protects (and Doesn't)
The two hex digits after * are an 8-bit XOR of every byte between $ and *. The receiver computes the same XOR over what it just received; if the two values match, the line was transmitted intact.
Engineer’s Takeaway
The checksum verifies line integrity. It does not prove the position is correct. A sentence with a valid checksum can still contain a wrong fix — multipath, weak signal, or a receiver bug will produce well-formed but inaccurate data. Trust the checksum to detect serial corruption. Trust GGA Quality, HDOP, and satellite count to assess the fix itself.
The XOR Range: After $, Before *
The XOR scope is exclusive on both ends: $ and * themselves are not included. The result is one byte, formatted as two uppercase hex characters. The reason for XOR instead of CRC is historical — early marine MCUs were 8-bit, and XOR is literally one instruction per byte. On a Cortex-M0 at 16 MHz, verifying an 80-character sentence takes about 5 microseconds.
The checksum workflow has four steps:
- Take the bytes between
$and*. - XOR them in order, starting from zero.
- Format the result as two uppercase hex digits.
- Compare it with the embedded checksum after
*.
For sentence:
$GNGGA,072446.00,3114.562,N,...,0031*50
Start after: $
Stop before: *
XOR scope: GNGGA,072446.00,3114.562,N,...,0031
Python Implementation for NMEA 0183 Checksum
The full Python version handles common edge cases (input with or without framing, trailing whitespace, missing asterisk):
def calculate_checksum(sentence: str) -> str:
"""
Compute NMEA 0183 checksum.
Accepts sentence with or without leading '$' and trailing '*XX'.
"""
# Strip framing if present
if sentence.startswith('$'):
sentence = sentence[1:]
if '*' in sentence:
sentence = sentence.split('*')[0]
checksum = 0
for char in sentence:
checksum ^= ord(char)
return f"{checksum:02X}"
def verify_checksum(sentence: str) -> bool:
"""Returns True if sentence's embedded *HH matches computed XOR."""
if '*' not in sentence:
return False
body, claimed = sentence.rstrip('\r\n').split('*')
return calculate_checksum(body) == claimed.upper()
Need to handle malformed sentences, append checksums, or run checksum validation as a standalone module? See the extended implementation on GitHub: NMEA/Parser/checksum.py →
C Implementation for Embedded Systems
For embedded MCUs, keep the checksum function small. This version accepts either a full sentence starting with $ or a pointer to the first byte after $:
#include <stdint.h>
uint8_t nmea_checksum(const char *s) {
uint8_t checksum = 0;
if (*s == '$') {
s++;
}
while (*s != '\0' && *s != '*') {
checksum ^= (uint8_t)(*s);
s++;
}
return checksum;
}
Manual Calculation for Debugging
When a parser refuses to accept a sentence and you suspect the checksum field, working it out by hand takes 30 seconds. Pick a short sentence, XOR each byte's ASCII value in turn:
# Verify $GPRMC,A*62 by hand:
Bytes between $ and *: G P R M C , A
ASCII (hex): 47 50 52 4D 43 2C 41
XOR step by step:
47 ⊕ 50 = 17
17 ⊕ 52 = 45
45 ⊕ 4D = 08
08 ⊕ 43 = 4B
4B ⊕ 2C = 67
67 ⊕ 41 = 26
Result: 0x26 ≠ 0x62 → Checksum mismatch (this sentence is fabricated)
Why Checksum Fails in Real Logs
Six things cause checksum mismatches in production. In order of how often they show up:
-
The XOR scope is strictly between
$and*, exclusive on both ends. Including either delimiter is the most common cause of checksum mismatches in custom implementations. -
Lowercase hex output. The standard requires uppercase. Some strict parsers reject
*5awhile accepting*5A. -
Missing zero-padding. The checksum must always be two characters.
0x0Amust be printed as*0A, not*A. Python'sf"{checksum:02X}"handles this; raw integer-to-hex conversions in C or LabVIEW often don't. - Including CR/LF. Line terminators are outside the XOR scope.
- EMI on long serial cables. Multi-meter UART runs without shielding will flip random bits. The first symptom is sporadic checksum failures that come and go with cable position.
- Mid-sentence truncation from serial buffer behavior — see the serial-stream section below.
Pro Tip
Treat checksum mismatch as a dropped packet. Never try to repair corrupted NMEA — partial truth is worse than known silence. Increment a counter, log the bad line, move on.
Parsing NMEA 0183 in Practice
Python: pynmea2 in 5 Lines
For most applications, the pynmea2 library handles parsing, type conversion, and DDMM.MMMM-to-decimal-degrees automatically:
# pip install pynmea2
import pynmea2
msg = pynmea2.parse(
"$GNGGA,072446.00,3114.562,N,12128.050,E,4,21,0.6,15.2,M,8.9,M,1.0,0031*50"
)
print(msg.timestamp) # 07:24:46
print(msg.latitude) # 31.2427 (decimal degrees, auto-converted)
print(msg.longitude) # 121.4675
print(msg.gps_qual) # 4 (RTK Fixed)
Python: Minimal Raw Parser (No Dependencies)
When pynmea2 is not available — MicroPython, CircuitPython, performance-sensitive paths, or just policy — the core logic is short. One coordinate helper, one parser:
def _ddmm_to_decimal(value: str, hemisphere: str):
"""
Convert NMEA DDMM.MMMM / DDDMM.MMMM format to decimal degrees.
Latitude has 2 degree digits (N/S); longitude has 3 (E/W).
Returns None if value or hemisphere is empty.
"""
if not value or not hemisphere:
return None
deg_digits = 2 if hemisphere in ("N", "S") else 3
degrees = int(value[:deg_digits])
minutes = float(value[deg_digits:])
decimal = degrees + minutes / 60.0
if hemisphere in ("S", "W"):
decimal = -decimal
return decimal
def parse_gga(sentence: str):
"""Minimal GGA parser. Returns None if invalid."""
if not sentence.startswith('$') or '*' not in sentence:
return None
body, checksum = sentence[1:].rstrip('\r\n').split('*')
fields = body.split(',')
# GGA must have at least 15 comma-separated fields
if len(fields) < 15:
return None
if not fields[0].endswith('GGA'): # Match on sentence ID, not talker
return None
if calculate_checksum(body) != checksum.upper():
return None
return {
'time': fields[1],
'latitude': _ddmm_to_decimal(fields[2], fields[3]),
'longitude': _ddmm_to_decimal(fields[4], fields[5]),
'quality': int(fields[6]) if fields[6] else 0,
'satellites': int(fields[7]) if fields[7] else 0,
}
Need handlers for RMC, VTG, GSA, and GSV in the same style? Runnable parser implementation is available on GitHub: NMEA/Parser/nmea_parser.py →
LabVIEW: How to Think About an NMEA Parser VI
LabVIEW shows up in lab automation, industrial test benches, and university research where engineers want a graphical wiring diagram instead of code. The NMEA parser pipeline in LabVIEW follows the same logic as any language, just wired left-to-right:
Serial Read
→ Match Pattern (CR/LF) to split into individual sentences
→ Verify XOR checksum reject corrupted lines
→ Split by comma produce field array
→ Dispatch by sentence ID chars 3–5, not full address
→ Parse fields per sentence type
LabVIEW and Python developers share a common trap: matching on $GPGGA instead of GGA. If you inherited a GPGGA Parser.vi from a 2015 forum post, it likely cannot read $GNGGA from a modern multi-constellation receiver. The fix is one block change: replace the literal-string match with a substring match on characters 3–5 of the address field.
Real-World Edge Cases
Real-world serial streams break simple parsers in predictable ways. The fixes are usually small; the hard part is recognizing the failure mode.
The Multi-Constellation Trap (GP vs GN)
Symptom: Your parser worked with old GPS logs but shows no data with a new multi-constellation receiver.
Why: The receiver outputs $GNGGA instead of $GPGGA, so a parser matching startswith('$GPGGA') silently drops every fix.
Fix: Match characters 3–5: if sentence[3:6] == 'GGA':. Working demo: NMEA/Examples/multi_constellation.py →
Serial Reads Are Not Sentence Boundaries
Symptom: Your parser fails randomly under load, especially at 5–10 Hz output rates, and checksum errors come and go.
Why: USB-serial drivers do not respect NMEA sentence boundaries. Common USB-serial bridges such as CP2102 and CH340 add hardware FIFO and driver buffering; at higher output rates, one OS read() can easily split an NMEA sentence across two calls.
Fix: Treat serial input as a stream, not as a line API. Use a persistent buffer that survives across reads:
# WRONG: each read() may truncate a sentence mid-byte
data = ser.read(1024)
for line in data.split(b'\n'):
parse(line) # some lines are partial → checksum fails
# RIGHT: persistent buffer, scan for \r\n boundaries
buffer = b''
while True:
buffer += ser.read(ser.in_waiting or 1)
while b'\r\n' in buffer:
line, buffer = buffer.split(b'\r\n', 1)
parse(line)
Avoid read_until('\n') for the same reason: a single corrupted byte can stall the loop while the parser waits for a line ending that may not arrive. Live serial demo: NMEA/Examples/read_serial.py →
The UTC Rollover Trap (Time Without Date)
Symptom: Your timestamps go backwards by 24 hours once a day, and sensor fusion stalls around UTC midnight.
Why: NMEA's time fields look complete but carry no date. A parser that combines GGA time with today's system date will eventually tear the timeline.
Fix: Combine time from GGA with date from RMC or ZDA. RMC carries both in the same sentence:
# $GNRMC,072446.00,A,3114.562,N,...,200625,...
# └─ time ┘ └─ date (DDMMYY)
from datetime import datetime, timezone
def parse_rmc_datetime(time_field: str, date_field: str) -> datetime:
"""Combine RMC time (hhmmss.ss) and date (ddmmyy) into UTC datetime."""
hh = int(time_field[:2])
mm = int(time_field[2:4])
ss = float(time_field[4:])
day = int(date_field[:2])
month = int(date_field[2:4])
year = 2000 + int(date_field[4:6])
return datetime(year, month, day, hh, mm, int(ss),
microsecond=int((ss % 1) * 1_000_000),
tzinfo=timezone.utc)
GSV Pagination Reassembly
Symptom: Your satellite-in-view count is consistently wrong, missing some satellites or showing the same satellite multiple times.
Why: GSV carries up to 4 satellites per sentence, so a receiver tracking 12 satellites sends three GSV pages in sequence. Reading the list before all pages arrive gives an incomplete view of the sky.
Fix: Buffer GSV pages until page == total, with a 300 ms timeout to discard incomplete cycles. Working example: NMEA/Examples/gsv_reassembly.py →
NMEA 0183 vs NMEA 2000 vs RTCM
The Physical Layer Wall You Can't Code Around
NMEA 0183 is UART / RS-422. You can wire it directly to a Raspberry Pi GPIO pin or an STM32 USART. NMEA 2000 is CAN-bus — you need a CAN-capable MCU plus a CAN transceiver, or a USB-CAN adapter, to read it at all. A common hobbyist setup uses MCP2515-based modules with a generic MCU; production systems usually use integrated CAN peripherals on automotive-grade MCUs with proper isolated transceivers. Either way, the hardware decision happens before any code is written.
| Property | NMEA 0183 | NMEA 2000 | RTCM 3.x |
|---|---|---|---|
| Year introduced | 1983 | 2001 | 2004 |
| Encoding | ASCII | Binary | Binary |
| Physical layer | RS-422 / UART | CAN-bus | UART / TCP / IP |
| Hardware needed | UART pin | CAN transceiver | UART / NTRIP client |
| Bandwidth | 4.8–115 kbps | 250 kbps | ~1–10 kbps |
| Direction | Receiver → host | Bidirectional | Base → Rover |
| Primary use | GNSS data output | Inter-device bus (marine) | RTK corrections |
Why RTK Workflows Mix NMEA + RTCM
If you are building a robot, drone, or tracker, you will read NMEA 0183 from the receiver and send RTCM 3.x corrections to it from a base station or an NTRIP service. NMEA 2000 only enters the picture when integrating with existing marine equipment networks. For everything else, NMEA 0183 + RTCM 3.x is the common pairing.
For a deep dive into the binary correction protocol, see RTCM Unpacked. For the network transport that delivers RTCM over IP, see Inside NTRIP.
Using NMEA 0183 in RTK Applications
For RTK systems, NMEA is not the correction stream — it is the status layer. RTCM improves the solution; NMEA tells your host whether the receiver considers that solution usable. The two sections below cover where to read that status, and how to confirm it.
Reading RTK Status from GGA Quality Indicator
Typical RTK status progression after corrections begin:
1 Autonomous → 2 DGPS → 5 RTK Float → 4 RTK Fixed
Side branches:
0 Invalid → no usable fix
6 Estimated/DR → receiver is falling back to inertial / dead reckoning
GGA Field 6 is where RTK status lives. Nine values, only a handful matter in practice:
| Value | Status | Practical Meaning |
|---|---|---|
0 |
Invalid | No fix — ignore the sentence |
1 |
GPS (autonomous) | Meter-class accuracy under open sky |
2 |
DGPS | Often sub-meter, depending on correction source |
3 |
PPS | Military precise positioning |
4 |
RTK Fixed | Centimeter-class under good conditions |
5 |
RTK Float | Often decimeter-class, still converging or degraded |
6 |
Estimated / DR | Inertial only, GPS lost |
7 |
Manual input | Operator-entered position |
8 |
Simulator | Test mode, never trust in production |
The engineering target in any RTK setup is 4 = Fixed. The receiver typically progresses 1 → 2 → 5 → 4 over 30-90 seconds after corrections start flowing. If it gets stuck at 5 = Float, start by checking the RTCM input, correction age, baseline distance, and antenna environment before changing parser logic. For the underlying correction format, see RTCM Unpacked.
Pro Tip
Indoors, near a window, you will almost never see Quality = 4. RTK needs open sky, stable carrier-phase tracking, and good satellite geometry. Go outside, place the rover where the antenna has a clear sky view, and give the ambiguity engine time to converge.
For a ready-to-run Python monitor that prints state transitions in real time: NMEA/Examples/rtk_fix_monitor.py →
Kalmix SCOUT PRO outputs standard multi-constellation NMEA 0183 over USB-C, parseable directly by the host without proprietary desktop software in the loop. RTK state transitions — Invalid, Autonomous, Float, Fixed, DR — are exposed to the application layer at parser-level granularity, so the diagnostics in this guide map one-to-one to receiver behavior.
Cross-Validating with RMC Mode Indicator
The RMC Mode Indicator appears in newer NMEA versions; older receivers may omit it entirely. Treat a missing mode field as unknown, not as invalid. When present, RMC encodes the same RTK state as GGA but as a single character in field 12:
| RMC Field 12 | Meaning | Matches GGA Quality |
|---|---|---|
A |
Autonomous | 1 |
D |
Differential | 2 |
E |
Estimated / DR | 6 |
F |
Float RTK | 5 |
M |
Manual input | 7 |
N |
No fix | 0 |
P |
Precise | 3 |
R |
RTK Fixed | 4 |
S |
Simulator | 8 |
Engineer’s Takeaway
Don't trust a single field for RTK state. When GGA reports 4 but RMC reports D, the receiver is in a transient — defer navigation decisions until the two agree.
Tuning Output Rate and Baud Rate for Real-Time Systems
At higher output rates, the bottleneck is usually not parsing logic but link budget: sentence count × sentence length × update rate must fit within the serial line's throughput. A 9600 baud link may be fine for a minimal 1 Hz stream, but it becomes fragile once you enable multi-constellation GSV, GSA, RMC, GGA, VTG, and RTK status at 5-10 Hz.
| Output profile | Recommended baud | Typical use |
|---|---|---|
| Minimal 1 Hz: GGA + RMC only | 4800 / 9600 | Legacy marine or slow logging |
| Typical 1 Hz: GGA + RMC + GSA/GSV | 9600 / 38400 | General GNSS logging |
| 5 Hz multi-constellation | 38400+ | Slow robotics / vehicle tracking |
| 10 Hz with RTK monitoring | 115200 preferred | Robotics, drones, control systems |
| 20 Hz+ | Vendor binary or high-speed UART | Tight sensor fusion / advanced systems |
NMEA Goes Both Ways: GGA Sent to NTRIP Casters
In standard tracking setups, NMEA is receiver-to-host. NTRIP networks flip this topology in one specific place: the rover may also send a GGA sentence upstream to the caster.
The outgoing GGA does not carry corrections — it tells the network roughly where the rover is, so the correction service can choose or generate location-aware corrections. Corrections still come back as RTCM in the opposite direction. This bidirectional use of NMEA over the same TCP socket is the foundation of Virtual Reference Station (VRS) and Network RTK services.
Most mature NTRIP clients — including RTKLIB, str2str, and many embedded receiver-side clients — handle this automatically when configured correctly. If you are writing your own NTRIP client, the GGA-upstream step is one of the easiest requirements to miss, and a common reason a freshly-built NTRIP setup receives no usable corrections.
Conclusion
NMEA 0183 looks deceptively simple — comma-separated ASCII, a one-line checksum, no negotiation. The real complexity sits in the serial stream itself: partial reads, vendor extensions, multi-constellation talker IDs, missing dates, and silent fix-quality degradation. Treat it as a byte stream, not a line API, and most production parsing bugs disappear.
The next entry in the GNSS Handbook covers how RTCM corrections actually reach the rover — which is where the GGA Quality field in this guide changes from 1 to 4.
Key Takeaway
NMEA 0183 is not difficult because the syntax is complex; it is difficult because real GNSS streams are continuous, partial, noisy, and vendor-specific. A reliable integration treats NMEA as a byte stream: validate the checksum, preserve null fields, parse by delimiters, and monitor RTK status through GGA rather than trusting a single position number.
Frequently Asked Questions
What is NMEA 0183?
NMEA 0183 is an ASCII-based serial communication protocol published in 1983 by the National Marine Electronics Association. GNSS receivers use it to output position, time, velocity, satellite status, and fix quality in readable text sentences.
What is the difference between NMEA 0183, NMEA 2000, and RTCM?
NMEA 0183 is ASCII output over RS-422 or UART. NMEA 2000 is a CAN-bus marine network. RTCM 3.x is a binary correction protocol used for RTK. In most RTK workflows, the host reads NMEA from the receiver and sends RTCM corrections into it.
How is the NMEA 0183 checksum calculated?
The NMEA 0183 checksum is the XOR of all bytes between $ and *, formatted as two uppercase hex digits. The $ and * characters are excluded from the XOR range.
Which NMEA sentences should I parse for GNSS or RTK applications?
For most GNSS applications, start with GGA for position and fix quality, RMC for date/time and minimum navigation data, and GSA/GSV for satellite status. For RTK, GGA Field 6 is the first field to monitor: 4 means RTK Fixed, 5 means RTK Float.
What is the difference between NMEA 0183 RMC and GGA sentences?
GGA carries position, altitude, fix quality, satellite count, HDOP, and RTK status, but no date. RMC carries position, date, time, speed, course, and validity, but no altitude or RTK quality indicator. For RTK status monitoring, parse GGA. For logging with reliable timestamps, parse both: GGA for fix quality, RMC for the date.
Why did my NMEA parser stop working after a GNSS receiver firmware update?
The most common cause is a talker ID change from GP to GN. A parser matching $GPGGA literally will drop every $GNGGA sentence from a multi-constellation receiver. Match on the sentence ID, such as GGA, instead of the full talker prefix.
Where can I find the official NMEA 0183 specification or PDF?
The official NMEA 0183 specification is sold by the National Marine Electronics Association at nmea.org under a paid license. Most free "NMEA 0183 protocol PDF" copies online are incomplete, outdated, or unauthorized. This guide and AN-001 cover the engineering content most GNSS developers need in daily integration.
You Might Also Like
AN-001: NMEA 0183 v4.10 Protocol Structure & Sentence Dictionary
Use the field-level dictionary when you need exact sentence structures and parsing rules.
LateralInside NTRIP: Why Seven Lines of Code Can Unlock Centimeter Accuracy
Understand how correction streams reach the rover before GGA reports RTK Float or Fixed.
ApplyKalmix SCOUT PRO — USB-C RTK GNSS Receiver
Evaluate NMEA output, RTK status, and receiver behavior on a field-ready GNSS device.