How it works
NMEA 0183 uses a simple XOR checksum over the sentence payload. The payload is everything between $ (exclusive) and * (exclusive). The result is formatted as two uppercase hex digits and appended after the *.
For $GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*43:
payload = "GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A"
checksum = 'G' ^ 'P' ^ 'R' ^ 'M' ^ 'C' ^ ',' ^ ... = 0x43
The $ and * themselves are never included. Everything in between is — commas, decimal points, all of it.
Implementation
This is the entire algorithm. No library needed:
uint8_t nmea_checksum(const char *sentence) {
uint8_t cksum = 0;
/* skip leading $ if present */
if (*sentence == '$') sentence++;
while (*sentence && *sentence != '*') {
cksum ^= (uint8_t)*sentence++;
}
return cksum;
}fn nmea_checksum(sentence: &[u8]) -> u8 {
let start = if sentence.first() == Some(&b'$') { 1 } else { 0 };
sentence[start..]
.iter()
.take_while(|&&b| b != b'*')
.fold(0u8, |acc, &b| acc ^ b)
}To validate an incoming sentence:
bool nmea_valid(const char *sentence) {
const char *star = strchr(sentence, '*');
if (!star || strlen(star) < 3) return false;
uint8_t expected = (uint8_t)strtol(star + 1, NULL, 16);
return nmea_checksum(sentence) == expected;
}fn nmea_valid(sentence: &[u8]) -> bool {
let Some(star_pos) = sentence.iter().position(|&b| b == b'*') else {
return false;
};
let suffix = &sentence[star_pos + 1..];
if suffix.len() < 2 { return false; }
let Ok(hex_str) = core::str::from_utf8(&suffix[..2]) else { return false; };
let Ok(expected) = u8::from_str_radix(hex_str, 16) else { return false; };
nmea_checksum(sentence) == expected
}Parsing rules
- Sentences start with
$, end with*HHwhereHHis the two-hex-digit checksum, followed by\r\n. - Some talkers (proprietary sentences) use
$Pas prefix — checksum algorithm is identical. - The checksum field is optional in NMEA 0183 v1.5 but mandatory in v2.0+. In practice, all modern GPS modules include it.
- The maximum sentence length is 82 characters including
$and\r\n.
Common mistakes
Including $ or * in the XOR. The checksum covers only the data between them. This is the most common off-by-one.
Case sensitivity. The hex checksum must be uppercase (*4B not *4b). Most parsers accept lowercase anyway, but the spec says uppercase.
CRLF vs LF. NMEA 0183 specifies \r\n. If your UART strips the \r, that’s fine — the checksum is computed before the line terminator.
Multi-sentence batches. If your parser buffers incoming data and processes line-by-line, ensure the \r doesn’t end up in the sentence string before you compute the checksum.
PUBX sentences. u-blox uses $PUBX for proprietary messages. Same checksum algorithm, but the parser must not confuse PUBX with standard talker+sentence-ID format.
Firmware considerations
For interrupt-driven UART with a DMA ring buffer, compute the checksum incrementally as bytes arrive — don’t wait for the full sentence. Start XORing from the first byte after $, stop when you see *, then compare the next two hex bytes to your running XOR. This avoids storing the entire sentence and works within tight RAM budgets on Cortex-M0 targets.