Checksums and CRC, Explained

What a checksum actually catches, why CRC beats a simple sum, the CRC variants used by NMEA, Modbus, CAN and MAVLink, and how to validate one yourself.

A frame of hex bytes with the final CRC byte highlighted, next to a valid-checksum reading and a corrupted one flagged as invalid

Every serious wire protocol ends its frames with a few extra bytes that look like noise but aren't. A checksum or CRC is a small number computed from the payload, sent alongside it, and recomputed by the receiver to catch bit-level corruption before it becomes a wrong reading on a dashboard. This post explains what those bytes actually protect against, why CRC beats a plain sum, and which variant shows up in which protocol.

What a checksum actually catches

A checksum catches corruption within a frame, a bit flipped by noise, a byte mangled in transit, a UART receive overrun. It does this by turning the payload into a short fingerprint that changes if even one bit changes. A mismatch between the sent fingerprint and the recomputed one means the bytes are not what the sender wrote (Peterson & Brown, "Cyclic Codes for Error Detection," Proceedings of the IRE, 1961).

What it cannot catch is a whole frame going missing. Picture ten frames leaving a sensor board and only nine arriving intact. Each of the nine passes its checksum with a clean bill of health, because each one really is byte-for-byte correct. The tenth simply never happened, as far as the checksum is concerned, and nothing about the nine survivors hints that a gap exists. The production-firmware habits post covers the fix for this blind spot: a rolling sequence counter the device increments every frame, so the host can tell "corrupted" apart from "absent" instead of only ever detecting the first one.

What a checksum flags What it never sees
A corrupted byte A dropped frame
A flipped bit A duplicated frame
A truncated frame A frame that never left the device

Why a plain sum misses errors that a CRC catches

A simple checksum, an XOR of every byte or an additive sum, is fast and cheap. But it is blind to entire classes of corruption, because addition and XOR are both commutative: swap two bytes, or flip the same bit in two different bytes, and the total comes out identical.

NMEA 0183, the sentence format GPS receivers speak, uses exactly this XOR scheme, and accepts the trade-off because the errors it misses are rare on a short, low-noise wire (National Marine Electronics Association, NMEA 0183 Standard).

A CRC (cyclic redundancy check) fixes this by treating the message as one giant binary number and dividing it by a fixed polynomial, keeping the remainder as the check value. Division does not commute the way addition does, so reordering bytes or flipping bits in a pattern that a sum would shrug off changes the remainder almost every time. The practical result: an n-bit CRC is guaranteed to catch every burst error shorter than n bits, not just probably catch it (Peterson & Brown, 1961, cited above). A CRC-16 catches every single- or double-bit error and every burst of corruption up to 16 bits wide, full stop, which is exactly the kind of damage a few nanoseconds of electrical noise on a wire tends to produce.

The gap between "usually works" and "provably works" is the whole reason CRC exists. It's tempting to treat a checksum's job as done once it flags most bad frames, but "most" is doing a lot of quiet work in that sentence. A sum-based checksum's blind spots are structured, not random: the exact kind of correlated bit flips a real cable produces (adjacent bits going down together from crosstalk, a whole byte clamped to one value from a stuck driver) are disproportionately the errors it misses.

A CRC's blind spots, by contrast, spread almost uniformly across all possible corruptions. That's why a 32-bit CRC still misses random corruption only about once in 4.3 billion frames, even in the worst realistic cases people have measured (Koopman, "32-Bit Cyclic Redundancy Codes for Internet Applications," Proceedings of DSN 2002, Carnegie Mellon University).

Which CRC variant is actually in your protocol

Every field you'll meet names its CRC by width, but "CRC-16" alone doesn't tell you enough. The polynomial, the initial value, and the bit order all have to match on both ends, or two implementations that both call themselves CRC-16 will disagree on the same bytes. A short field guide to the ones you'll actually run into:

  • CRC-8 shows up on short, low-stakes frames: single-wire sensor buses and small telemetry packets where one byte of overhead is affordable but two would sting.
  • CRC-16 is the workhorse. Modbus RTU appends a two-byte CRC-16, low byte first, to every frame on the wire, and a frame that fails the check is simply discarded by the receiver (Modbus.org, "MODBUS over Serial Line Specification and Implementation Guide V1.02"). A different CRC-16 variant, the CCITT flavor, turns up in MAVLink's own two-byte trailer, and MAVLink adds a twist worth knowing: it seeds the calculation with a CRC_EXTRA byte unique to each message type, so a receiver that misparses the payload length or field order gets a checksum mismatch instead of silently reading garbage as a valid message (MAVLink Developer Guide, "Packet Serialization").
  • CRC-32 is what you reach for when a payload is large or the stakes are higher: firmware images, log files, anything where you want the miss probability pushed down toward one in four billion rather than one in sixty-five thousand.
  • CAN skips an appended checksum entirely because the check is built into the hardware. Every classic CAN frame carries its own 15-bit CRC field, computed and verified by the controller silicon before the frame is even handed to software, and a node that sees a bad CRC refuses to acknowledge the frame at all (ISO 11898-1:2015). That's also why the CAN bus post doesn't spend any time on payload checksums. The protection is already there, underneath the arbitration ID, before your DBC file ever sees a byte.

Where the checksum actually lives in a frame

A checksum is only useful if the receiver knows exactly which bytes it covers and exactly where the check value itself sits, which is why framing and checksums are designed together rather than bolted on separately. In a COBS-framed binary protocol the payload is stuffed so that the delimiter byte can't appear inside it. The checksum then rides as ordinary payload bytes right before that delimiter: the COBS and MessagePack post walks through the exact byte layout, stuffing code and all, for a frame built this way.

Text protocols put the boundary in plain sight instead. NMEA sentences mark it with a literal * character. $GPGGA,...,545.4,M,46.9,M,,*47 reads as "everything between $ and * is covered, 47 is the hex checksum of that span." Binary protocols do the same job positionally: the checksum is always the last one, two, or four bytes before the frame's end delimiter, and both sides simply agree on the width up front.

Verifying a checksum on the host

On the receiving side, a frame's checksum only earns its keep if something actually recomputes it and acts on a mismatch. That job can happen at two different points in a parsing pipeline. Serial Studio's frame extraction stage has a built-in per-source checksum check, selectable from a list of common algorithms (XOR-8, MOD-256, CRC-8, three CRC-16 flavors, Fletcher-16, CRC-32, and Adler-32), and a frame that fails the check is dropped before the frame parser ever runs. Turning that on for a project that had been quietly plotting one bad spike every few thousand frames made the spikes disappear from the dashboard instead of the data.

If your protocol's checksum isn't one of the built-in choices, or the check has to happen alongside other custom parsing logic, a custom frame parser can recompute it in script and reject the frame itself. This is exactly what Serial Studio's shipped NMEA 0183 reference parser does: before it touches a single field, it recomputes the sentence's XOR checksum in Lua. If the check fails, it refuses to update any values, holding the last known-good reading instead of accepting a corrupted one. The same pattern, recompute and compare before you trust a byte, works for whatever algorithm your device actually uses; the parser just needs to know the frame's exact boundaries and the width of the check value it's looking for.

Try it yourself, no hardware required

NMEA is the easiest place to see a checksum with your own eyes, because it's plain text and the checksum is a single readable hex pair right at the end of the line. Serial Studio's TinyGPS example ships a simulated GPS feed of standard sentences like $GPGGA and $GPRMC, each one carrying a real XOR checksum computed the same way a receiver's firmware computes it. Load the example, watch the raw sentences in the console, and you can recompute the checksum on any line by hand: XOR every character between $ and * together and compare the result to the two hex digits that follow. Flip one character in a copy of the sentence and recompute; the checksum changes, which is the entire mechanism in one five-minute exercise.

Why this matters for telemetry you can trust

None of this is about paranoia for its own sake. A corrupted temperature reading that sails past a missing checksum doesn't look wrong, it looks like data, and a plot has no way to tell the difference between a real spike and eight flipped bits. Pairing a checksum with the sequence-counter habit from the production firmware post closes both halves of the problem: the checksum catches the frame that arrived wrong, the counter catches the frame that never arrived at all. Between the two, a dashboard's silence and its numbers both mean what they appear to mean.

Frequently asked questions

Do I need a CRC if I already have a sequence counter?

Yes, they solve different problems. A sequence counter tells you a frame is missing; it says nothing about whether the frames that did arrive are intact. A corrupted frame with an unbroken counter sequence sails through undetected unless something also checks its contents. Use both: a checksum for corruption, a counter for gaps, exactly as the telemetry habits post lays out.

Which CRC should I pick for a new project?

For most serial and network telemetry, CRC-16 is the sensible default: two bytes of overhead, a guaranteed catch on every error burst up to 16 bits, and wide library support on both microcontrollers and hosts. Reach for CRC-32 only when frames are large (kilobytes, not tens of bytes) or the cost of a missed corruption is genuinely high, since the extra two bytes barely register against a big payload but do add up on a short one.

Can a CRC be wrong on purpose (spoofed)?

Yes, trivially. A CRC has no secret key, so anyone who can see the algorithm, which is public for every standard variant, can recompute a valid CRC for data they've deliberately altered. It defends against accidental corruption from noise, timing glitches and dropped bits, not against a party who wants to forge a message. That job belongs to a cryptographic MAC or a signed hash, and a CRC should never be treated as a security control.

Is a checksum the same thing as a hash?

They overlap but aren't interchangeable. A checksum or CRC is built for speed and burst-error coverage on small frames, computed fresh on every message with almost no CPU cost. A cryptographic hash like SHA-256 is built to make forging a matching output computationally infeasible, at a much higher cost per byte, which is overkill for catching a flipped bit on a serial link and underkill for anything that needs to resist a deliberate attacker.

Comments

Copied to clipboard!