CRC selection feels arbitrary until you get a bug. Picking the wrong polynomial, wrong bit order, or wrong initial value gives you a CRC that passes on your machine and fails on the target, or passes silently when data is corrupted.
What CRC actually does
A CRC divides the message (treated as a binary polynomial) by a fixed generator polynomial and uses the remainder as the check value. The receiver performs the same calculation and compares. Any burst error shorter than the polynomial’s degree is detected with probability 1.
The key property: a CRC-16 detects all burst errors ≤ 16 bits. A CRC-32 detects all bursts ≤ 32 bits. For random errors, a 16-bit CRC has a 1/2^16 = 0.0015% false-positive rate. That’s usually fine for embedded protocols where messages are short and you can retry.
The naming confusion
“CRC-16” is ambiguous. There are at least six variants in common use:
- CRC-16/IBM (Modbus): Poly 0x8005, init 0xFFFF, reflect in/out, no final XOR. Used in Modbus RTU.
- CRC-16/CCITT (False): Poly 0x1021, init 0xFFFF, no reflection. Used in XModem variants.
- CRC-16/CCITT-TRUE (Kermit): Poly 0x1021, init 0x0000, reflect in/out.
- CRC-16/USB: Poly 0x8005, init 0xFFFF, reflect in/out, final XOR 0xFFFF.
- CRC-16/DNP: Poly 0x3D65, reflect in/out, final XOR 0xFFFF. Used in DNP3.
If you tell a colleague “I’m using CRC-16” and they implement a different variant, your CRCs will never match even though both implementations are technically correct.
Always specify the full parameters: polynomial, initial value, input reflection, output reflection, final XOR value. Use the CRC calculator to verify your implementation against known test vectors.
Which to use
CRC-8 (poly 0x07 or 0x31 depending on variant): Use for short messages (< 256 bytes) where space is tight. Dallas/Maxim 1-Wire uses CRC-8/MAXIM (poly 0x31). Suitable for sensor data with infrequent transmission.
CRC-16/Modbus: Use when talking to industrial equipment. It’s the de facto standard for RS-485/Modbus RTU. The STM32 CRC peripheral supports this polynomial.
CRC-16/CCITT (0x1021): Widely used in serial protocols, SD cards, Bluetooth. Good general-purpose choice for custom protocols.
CRC-32/ISO-HDLC: The same CRC used in Ethernet, ZIP files, PNG. Hardware acceleration available on most Cortex-M MCUs. Use this for large data blocks (firmware updates, file transfers) where detection strength matters.
CRC-32C (Castagnoli): Better error detection properties than CRC-32/ISO-HDLC. Used in iSCSI, SCTP, and increasingly in storage applications. Some MCUs include hardware support.
Implementing in C without a lookup table
A fast CRC implementation for embedded systems:
// CRC-16/CCITT (poly 0x1021, init 0xFFFF)
uint16_t crc16_ccitt(const uint8_t *data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}
This is the bit-by-bit version. It’s slower than a lookup table but needs no RAM for the table — important on Cortex-M0 parts with 2–4 KB of RAM. For higher throughput, a 256-entry uint16_t table costs 512 bytes and gives 8–16× speedup.
Hardware CRC on STM32
STM32 has a CRC peripheral that computes CRC-32/ISO-HDLC in hardware:
// Enable CRC clock
__HAL_RCC_CRC_CLK_ENABLE();
// Reset and compute
CRC->CR = CRC_CR_RESET;
for (size_t i = 0; i < len; i += 4) {
CRC->DR = *(uint32_t *)(data + i);
}
uint32_t result = CRC->DR;
On STM32L4 and later, the polynomial and initial value are configurable. On older STM32F1/F2/F4, it’s fixed to the standard CRC-32 polynomial.
Verifying your implementation
Test vectors are the only reliable way to confirm your implementation is correct. For CRC-16/Modbus on the string "123456789" (bytes 0x31–0x39): the result should be 0x4B37.
The CRC calculator supports all common variants with correct test vectors — paste in your data, get the expected output, compare with your code.