Also known as: CRC-16/CCITT, CRC-CCITT, CRC-16 0x1021
CRC-16/CCITT is not one checksum but a family of them — every member built on the same
16-bit generator polynomial 0x1021 (x¹⁶ + x¹² + x⁵ + 1).1 What makes two members
produce different check values on the same bytes is not the polynomial but three surrounding
parameters: the register’s initial value, whether input and output bits are reflected,
and a final XOR applied to the result. GopherTrunk reuses this one polynomial under several
parameter sets across its protocols, which is why getting the parameters right matters more than
the polynomial ever does.2
How it works
A CRC treats the message as a big binary polynomial and returns the remainder after dividing it
by the generator — computed with a 16-bit shift register that, on each step, shifts a message bit
in and XORs 0x1021 back whenever the register overflows the top bit. It is a
cyclic redundancy check in the textbook sense; the
“CCITT” label just fixes the polynomial. GopherTrunk’s CRCCCITTWithInit is the parametric core
— MSB-first, no reflection — with the initial value passed in, and the reflected and augmented
forms are separate functions with their own bit order.
The variants GopherTrunk uses
The same 0x1021 appears under these parameter sets. The check value is the CRC of the ASCII
string "123456789", the standard reference vector:
| Variant | Init | Reflect | Final XOR | Check | Used by |
|---|---|---|---|---|---|
| CCITT/FALSE | 0xFFFF |
no | 0x0000 |
0x29B1 |
P25 TSBK (stored complemented) |
| XMODEM | 0x0000 |
no | 0x0000 |
0x31C3 |
YSF FICH, M17, LoRa payload |
| Reflected | 0x0000 |
in/out | 0xFFFF |
— | MDC1200 |
| Augmented | 0x0000 |
no | 0xFFFF |
— | P25 TSBK trailer |
The MDC1200 reflected form runs the polynomial in its bit-reversed
representation 0x8408; the LoRa payload CRC is the plain XMODEM
member.
Init, reflection and final XOR
Each parameter changes the result independently:
- Initial value. Seeding the register with
0xFFFFinstead of0x0000makes the CRC sensitive to leading zero bytes — a run of zeros at the start of a message no longer leaves the register at zero, so prepended zeros are detected. - Reflection. Reflecting input and output bits processes each byte LSB-first, matching how UART/serial hardware clocks bits out. It changes the value but not the error-detection strength; it is a convention, chosen to match the wire.
- Final XOR. XORing the register with
0xFFFFat the end guards against trailing zero bytes the same way a non-zero init guards against leading ones.
Two implementations sharing the polynomial but differing in any of these will disagree on every input — which is exactly the failure mode below.
The augmented variant and issue #275
The P25 TSBK trailer uses the augmented-message form, which is a genuinely different
algorithm, not just different parameters: message bits are shifted into the low end of the
register and the polynomial is XORed when the register grows past 16 bits, with a final XOR of
0xFFFF. The encoder computes the trailer as the CRC of info ‖ 16 zero bits; a receiver checks
that the CRC of info ‖ trailer is zero. GopherTrunk’s original P25 code used the CCITT/FALSE
function here, and every on-air TSBK on the Mt Anakie capture failed verification even when the
trellis decoder reported a clean path — issue #275. Switching to CRCCCITTAugmented made the
on-air trailers verify. The lesson is the recurring one with this family: the polynomial is the
easy part; the init, shift direction and final XOR are where a decoder silently gets it wrong.
Sources
-
Cyclic redundancy check — Wikipedia, on CRC computation, generator polynomials and the init/reflect/xorout parameters. ↩
-
CRC catalogue (16-bit) — Greg Cook’s catalogue, the reference for CRC-16 parameter sets and their check values. ↩