gps gnss uart firmware protocols

NMEA 0183 Checksum Validation in C

· Bob Peters

GPS modules output NMEA 0183 sentences continuously. If you’re not validating checksums, you’re silently accepting corrupted data — and position errors in GPS data tend to be large and obvious only after they’ve caused a problem.

How NMEA checksums work

An NMEA sentence looks like this:

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

The checksum is the two hex digits after the *. It’s calculated as the XOR of all bytes between $ and * (exclusive of both):

'G' ^ 'N' ^ 'G' ^ 'G' ^ 'A' ^ ',' ^ ... = 0x47

That’s the entire algorithm. XOR every byte in the sentence body. The result is a single byte, formatted as two uppercase hex digits.

Implementing validation in C

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

bool nmea_validate_checksum(const char *sentence) {
    // Find the $ at the start
    const char *start = strchr(sentence, '$');
    if (!start) return false;
    start++; // skip the $

    // Find the * before the checksum
    const char *star = strchr(start, '*');
    if (!star || strlen(star) < 3) return false;

    // Calculate XOR of all bytes between $ and *
    uint8_t calculated = 0;
    for (const char *p = start; p < star; p++) {
        calculated ^= (uint8_t)*p;
    }

    // Parse the two hex digits after *
    char hex[3] = { star[1], star[2], '\0' };
    uint8_t provided = (uint8_t)strtoul(hex, NULL, 16);

    return calculated == provided;
}

Use the NMEA checksum calculator to verify your implementation against known sentences.

Common mistakes

Not handling CRLF. NMEA sentences end with \r\n. If you include the carriage return in your XOR calculation, the checksum fails. Strip or stop at \r before computing.

Missing the $ exclusion. The $ itself is not included in the checksum calculation. Only bytes strictly between $ and * are XORed.

Case sensitivity. The two checksum hex digits in the sentence may be uppercase or lowercase. Parse them with strtoul(..., 16) or a function that handles both — don’t compare characters directly.

Partial sentences. GPS modules output at 1–10 Hz. If your UART buffer isn’t large enough, you can receive a partial sentence. Always buffer complete sentences (terminated by \n) before parsing. A simple circular buffer with newline detection works well.

Parsing GGA sentences robustly

NMEA sentences are comma-delimited but field count varies between receiver models and fix status. Never use fixed-offset parsing. Use strtok or a hand-rolled comma parser:

// Returns the nth field (0-indexed) from an NMEA sentence
// Writes into buf, returns false if field doesn't exist
bool nmea_get_field(const char *sentence, int n, char *buf, size_t buflen) {
    const char *p = sentence;
    int field = 0;
    while (*p && *p != '*') {
        if (field == n) {
            const char *end = strchrnul(p, ',');
            if (*end == '*') end = strchr(p, '*');
            size_t len = (size_t)(end - p);
            if (len >= buflen) return false;
            memcpy(buf, p, len);
            buf[len] = '\0';
            return true;
        }
        if (*p == ',') field++;
        p++;
    }
    return false;
}

u-blox UBX protocol

If you’re using a u-blox receiver (NEO-M8, M9, F9 series), you’ll often interact with UBX binary protocol in addition to NMEA. UBX uses a two-byte Fletcher checksum (CK_A, CK_B) over the message class, ID, length, and payload.

The algorithm is different from NMEA:

void ubx_checksum(const uint8_t *msg, size_t len, uint8_t *ck_a, uint8_t *ck_b) {
    *ck_a = 0;
    *ck_b = 0;
    for (size_t i = 0; i < len; i++) {
        *ck_a += msg[i];
        *ck_b += *ck_a;
    }
}

The checksum covers bytes 2 through N-2 of the frame (class, ID, length, payload — not sync bytes and not the checksum bytes themselves). Use the UBX checksum calculator to validate message construction.

GNSS data quality beyond checksum

Checksum validates that bytes weren’t corrupted in transit. It doesn’t validate the GPS fix quality. Always check:

  • fix_quality field in GGA (0 = no fix, 1 = GPS fix, 2 = DGPS)
  • hdop (horizontal dilution of precision) — values above 2–3 mean poor accuracy
  • satellites_used — fewer than 4 means marginal fix
  • Age of last fix — a checksum-valid sentence from 30 seconds ago may have a position that’s stale

Rejecting invalid checksums is the first filter. Checking fix status and DOP is the second.

Newsletter

The embedded engineer's weekly cheat sheet

Register tricks, timing gotchas, and tool updates. One email per week. No fluff.

No spam. Unsubscribe anytime.