Skip to content
Back to Blog
Tutorials

CRC-16 Variants: Why MODBUS, CCITT and XMODEM Differ

Same bytes, four different CRC-16 results. See how poly, init, refin, refout and xorout separate MODBUS from CCITT-FALSE, XMODEM and KERMIT online.

13 min read

Why the Same Bytes Give Four Different CRC-16 Results

CRC-16 names a family rather than an algorithm, and the members of that family disagree with each other. Hand the same bytes to MODBUS, to CCITT-FALSE and to XMODEM, and you get back three 16-bit numbers with nothing in common.

Five constants decide which member you are running: poly, init, refin, refout and xorout. Change one and the output changes completely, with no partial resemblance to warn you. Most “why doesn’t my CRC match the device’s” questions end right there: the two sides are running different variants, and nobody wrote down which.

Every value in this guide came out of a parameterised CRC model run on Python 3.14.7 under macOS, cross-checked four ways: against zlib.crc32 and binascii.crc_hqx from the standard library, against the published check values in the RevEng CRC catalogue, and against vendor documentation for each parameter set.

1. Same frame, four CRC-16 results

Here is a real Modbus RTU request. Slave 01, function code 03 (read holding registers), starting address 0x0000, quantity 0x000A:

01 03 00 00 00 0A

The same six bytes through four variants:

VariantResultOn the wire (low byte first)
CRC-16/MODBUS0xCDC5C5 CD
CRC-16/IBM-3740 (CCITT-FALSE)0x042828 04
CRC-16/XMODEM0x0A3838 0A
CRC-16/ARC0xD6C5C5 D6

Nothing links those four values. They share no nibble pattern and they are not off by a constant, and no ordering trick turns one into another. Two of them happen to start with the same byte on the wire, which is a coincidence.

So the string “CRC-16” in a datasheet carries almost no information. It tells you the output is 16 bits wide and stops there. If you want to look at a result in another radix while comparing against a device that prints binary, the number base converter flips a 16-bit value between hex and binary without you having to think about padding.

2. What each of the five parameters changes

Underneath every variant is the same machine: a shift register that eats one bit at a time, and XORs in a constant whenever a 1 falls off the top. The parameters decide what enters the register and which direction the bits travel, plus one last XOR on the way out.

Here is the whole engine in fourteen lines of Python. It is a bit-at-a-time implementation, slow but readable, and it reproduces every value in this article:

def crc(data, width, poly, init, refin, refout, xorout):
    top = 1 << (width - 1)
    mask = (1 << width) - 1
    reg = init
    for byte in data:
        if refin:
            byte = int(f"{byte:08b}"[::-1], 2)
        reg ^= byte << (width - 8)
        for _ in range(8):
            reg = ((reg << 1) ^ poly) if reg & top else (reg << 1)
            reg &= mask
    if refout:
        reg = int(f"{reg:0{width}b}"[::-1], 2)
    return reg ^ xorout

The engine’s output can be cross-checked against the Python standard library. zlib.crc32 and binascii.crc_hqx each give an independent answer, and all three match:

zlib.crc32(b"123456789")               = 0xCBF43926   engine above = 0xCBF43926   MATCH
binascii.crc_hqx(b"123456789", 0x0000) = 0x31C3       CRC-16/XMODEM = 0x31C3      MATCH
binascii.crc_hqx(b"123456789", 0xFFFF) = 0x29B1       CCITT-FALSE   = 0x29B1      MATCH

poly: two camps, 0x8005 and 0x1021

The polynomial is the constant XORed back into the register. It is written in “normal” form, with the top bit implicit, so 0x8005 means x^16 + x^15 + x^2 + 1 and 0x1021 means x^16 + x^12 + x^5 + 1. The shift-and-XOR loop is polynomial long division over GF(2). The polynomial plays the divisor. The register holds the running remainder, and each bit of the message advances the division by one step.

Almost every CRC-16 you meet uses one of those two. 0x8005 covers ARC, MODBUS and USB. 0x1021 covers the whole CCITT tangle plus XMODEM, KERMIT and the RFID variants. Knowing the polynomial alone never identifies a variant.

init: the register’s starting value

Two values dominate, 0x0000 and 0xFFFF. The difference matters more than it looks. Start at zero and a zero byte leaves the register at zero, so 00 00 01 and 01 produce identical CRCs. A message that gains or loses leading zeros in transit passes the check. Starting at 0xFFFF removes that blind spot, which is why Modbus, CCITT-FALSE and USB all use it.

refin and refout: bit order, not byte order

refin reverses the eight bits of each input byte before it enters the register. refout reverses the final register before the last XOR. Hardware that shifts data out LSB-first gets these reflections for free, so the reflected variants tend to be the ones that came out of serial protocols.

This is the parameter pair that gets confused with endianness most often. It is a different thing at a different level, and section 6 pulls them apart.

xorout: the final XOR

This is the last step, applied to the register after refout. For CRC-16 it is usually 0x0000 or 0xFFFF; the CRC-32 family uses 0xFFFFFFFF. It is the cheapest parameter to get wrong, because a mismatch here looks exactly like a mismatch anywhere else.

width: 8, 16 or 32 bits

Width sets the detection ceiling. A CRC of width n catches every burst error up to n bits long, and misses a random corruption with probability about 2^-n. That is 1 in 65,536 for CRC-16 and 1 in 4.3 billion for CRC-32.

Starting from CRC-16/XMODEM and changing exactly one parameter at a time, against the standard probe input 123456789:

baseline CRC-16/XMODEM                        = 0x31C3
init 0x0000 -> 0xFFFF   (becomes CCITT-FALSE) = 0x29B1
refin/refout -> true    (becomes KERMIT)      = 0x2189
xorout 0x0000 -> 0xFFFF                       = 0xCE3C
poly 0x1021 -> 0x8005                         = 0xFEE8

One flipped flag gives a completely different output. CRC has no notion of “close”. Two results either match or they tell you nothing about how far apart the inputs were, so staring at a mismatch never localises the cause. The inner loop is nothing but XOR and shifts; the complete guide to bitwise operations covers those operators themselves, and this article assumes them.

3. “CCITT” is a bad name

The RevEng CRC catalogue lists 31 distinct 16-bit CRC definitions as of September 2026, and three of them carry the CCITT label. None of those three agree with each other. The four rows below share a polynomial and an input, and land on four unrelated results:

Common nameCatalogue namecheckinitrefin/refoutxorout
CCITT-FALSECRC-16/IBM-37400x29B10xFFFFfalse0x0000
XMODEMCRC-16/XMODEM0x31C30x0000false0x0000
“the real” CCITTCRC-16/KERMIT0x21890x0000true0x0000
CRC-16/MCRF4XX0x6F910xFFFFtrue0x0000

The history is short and unhelpful. The catalogue lists CRC-16/KERMIT with the aliases CRC-CCITT and CRC-16/CCITT-TRUE, and that one is reflected. An unreflected implementation with init 0xFFFF also circulated widely under the CCITT name, which is why the catalogue records “CCITT-FALSE” as an alias of CRC-16/IBM-3740. XMODEM sits between them: same polynomial, no reflection, init zero.

So when a datasheet says CCITT, it has told you the polynomial is 0x1021 and nothing else. You still have four candidates, and picking wrong gives you a value that looks just as plausible as the right one.

Quote parameters, not names. Writing poly=0x1021, init=0xFFFF, refin=false, refout=false, xorout=0x0000 in your protocol document takes one line and settles the question permanently. Writing “CRC-16/CCITT” settles nothing.

4. How to identify which CRC-16 variant you have

The catalogue solves this with a fingerprint. Every entry publishes a check value: the CRC of the nine ASCII bytes 123456789. Run that string through the implementation in front of you and look the result up.

Variantcheckpolyinitrefinrefoutxorout
CRC-16/ARC (IBM/LHA)0xBB3D0x80050x0000truetrue0x0000
CRC-16/MODBUS0x4B370x80050xFFFFtruetrue0x0000
CRC-16/USB0xB4C80x80050xFFFFtruetrue0xFFFF
CRC-16/IBM-3740 (CCITT-FALSE)0x29B10x10210xFFFFfalsefalse0x0000
CRC-16/XMODEM0x31C30x10210x0000falsefalse0x0000
CRC-16/KERMIT (the real CCITT)0x21890x10210x0000truetrue0x0000
CRC-16/GENIBUS0xD64E0x10210xFFFFfalsefalse0xFFFF
CRC-16/MCRF4XX0x6F910x10210xFFFFtruetrue0x0000
CRC-32/ISO-HDLC (zip, PNG, zlib)0xCBF439260x04C11DB70xFFFFFFFFtruetrue0xFFFFFFFF
CRC-32/BZIP20xFC8919180x04C11DB70xFFFFFFFFfalsefalse0xFFFFFFFF
CRC-32C (Castagnoli, iSCSI)0xE30692830x1EDC6F410xFFFFFFFFtruetrue0xFFFFFFFF
CRC-8/SMBUS0xF40x070x00falsefalse0x00
CRC-8/MAXIM-DOW (1-Wire)0xA10x310x00truetrue0x00

One detail sinks a surprising number of these comparisons: 123456789 means the nine bytes 31 32 33 34 35 36 37 38 39, not the number 123456789 and not a null-terminated string. If your language hands the function a wide string or appends a terminator, you are hashing different input and every row will miss. Non-ASCII payloads add a second trap: the same text encoded as UTF-8 and as Windows-1252 is two different byte sequences, and therefore two different CRCs. The ASCII table and converter shows the byte behind each character, which settles that question in a few seconds.

If the check value matches no row at all, rule out the dull causes before suspecting a custom polynomial:

  1. Byte order: you may be reading the result reversed off the wire.
  2. Field coverage: the sender includes or excludes an address byte, a length byte, or the CRC field itself.
  3. A vendor init that is neither 0x0000 nor 0xFFFF, which does happen in proprietary meter protocols and is usually buried in a footnote.

Brute-forcing the parameters

When the vendor will not tell you and you cannot read their firmware, ask for one thing instead: a captured frame plus the CRC their device produced for it. Then enumerate. Two polynomials, two init values, refin and refout independently and two xorout values come to 32 combinations, which is nothing:

target = 0x4B37                    # the value their implementation returned
for poly in (0x8005, 0x1021):
    for init in (0x0000, 0xFFFF):
        for refin in (False, True):
            for refout in (False, True):
                for xorout in (0x0000, 0xFFFF):
                    if crc(b"123456789", 16, poly, init, refin, refout, xorout) == target:
                        print(f"poly=0x{poly:04X} init=0x{init:04X} "
                              f"refin={refin} refout={refout} xorout=0x{xorout:04X}")
poly=0x8005 init=0xFFFF refin=True refout=True xorout=0x0000

One hit, and it is CRC-16/MODBUS. Feed it a real frame instead of 123456789 and the same 32-way sweep works on any message you have a known-good CRC for. If several combinations survive one sample, run a second frame and intersect the results.

5. Modbus RTU in practice

Modbus RTU uses CRC-16/MODBUS: polynomial 0x8005, init 0xFFFF, reflected in and out, no final XOR. For our six-byte request the CRC is 0xCDC5, and the completed frame is:

01 03 00 00 00 0A C5 CD

The wire order is the reverse of how you write the value

The value is 0xCDC5. The bytes appended to the frame are C5 CD. Modbus specifies the CRC low byte first, which is the opposite of the order you read the hex number in, and it catches people out constantly. Every other multi-byte field in the same frame, including that 00 0A register count, is big-endian. The CRC is the exception.

This has a useful side effect. Compute CRC-16/MODBUS over the whole frame, CRC bytes included, and the result is:

0x0000

That is the receiver’s entire validation routine. There is no need to split the trailing two bytes off, byte-swap them, and compare against a locally computed value. Run the CRC over everything that arrived and check for zero. Fewer steps means fewer places to reverse the bytes incorrectly.

Why Modbus documentation shows 0xA001

Open almost any Modbus implementation and the constant in the source is 0xA001, not 0x8005. Both are correct. 0xA001 is 0x8005 with its 16 bits reversed, and it belongs to the reflected form of the algorithm, where the register shifts right instead of left and the input bytes need no per-byte reversal. The two implementations produce identical output; only the internals differ. 0xA001 in source is a reliable marker of a reflected CRC-16 implementation.

One thing to watch for in a capture. Modbus ASCII is a separate framing that carries each byte as two hex characters between a leading 3A (:) and a trailing 0D 0A, and it uses an LRC rather than a CRC. If your trace shows 3A 30 31 30 33 where you expected 01 03, you are in ASCII mode and no CRC variant will ever match. The ASCII table maps those bytes straight back to characters.

The same algorithm in C

Modbus firmware almost never uses the Python model above. It uses the reflected form directly, shifting right and XOR-ing with 0xA001:

uint16_t crc16_modbus(const uint8_t *buf, size_t len) {
    uint16_t crc = 0xFFFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (int b = 0; b < 8; b++)
            crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
    }
    return crc;
}

Fed the frame 01 03 00 00 00 0A, this returns 0xCDC5, the same value the Python engine produced. The check string 123456789 gives 0x4B37. Both were run under Apple clang 21 and match the table in section 4 bit for bit.

6. refin/refout is not endianness

These two live at different levels and the confusion is expensive, because Modbus contains both at once.

Endianness is about bytes within a multi-byte value: whether 0xCDC5 is stored or transmitted as CD C5 or C5 CD. Nothing inside a byte moves. That is the level big-endian versus little-endian covers in full, and this article will not repeat it.

Reflection is about bits within a single byte. refin reverses the eight bits of each byte before the register sees it: bit 0 becomes bit 7. The byte keeps its position in the message. Neither setting has any effect on the other.

In a Modbus frame both are happening, independently:

  • Inside the algorithm, refin and refout are true, so bits get reversed within bytes.
  • On the wire, the finished CRC is appended low byte first, which is a byte-order decision made by the protocol.

Turning off the reflection to “fix” a byte-order problem produces a different variant entirely, and swapping the frame’s trailing bytes to compensate for a reflection mismatch produces a value that is wrong in a new way. Diagnose one at a time: get the check value right with the algorithm alone, then deal with the frame layout.

7. CRC-32 vs CRC-16: which one should you use?

CRC-32 has the same problem as CRC-16, just less visibly, because one variant dominates so completely that most developers never learn there are others.

Variantcheckpolyrefin/refout
CRC-32/ISO-HDLC0xCBF439260x04C11DB7true
CRC-32/BZIP20xFC8919180x04C11DB7false
CRC-32C (Castagnoli)0xE30692830x1EDC6F41true

ISO-HDLC is what zlib.crc32 computes, and it is everywhere on the web side of the stack. Zip stores it per entry in the central directory, and every PNG chunk ends with one covering the chunk type and data. Ethernet’s frame check sequence uses the same parameter set at the end of every frame your server sends. BZIP2 is that polynomial with the reflections turned off, which produces a value sharing nothing with ISO-HDLC.

CRC-16 shows up in the same neighbourhood too: Redis Cluster routes a key to one of its 16,384 slots with CRC16(key) mod 16384, which is why keys with the same hash tag land on the same node.

Why CRC-32C won the hardware lottery

CRC-32C uses a different polynomial with better error detection over the short blocks that storage and network protocols send. That earned it iSCSI, ext4 metadata checksums, SCTP and Btrfs. Then Intel put it in silicon: the SSE4.2 crc32 instruction computes CRC-32C directly, which turns it into an effectively free integrity check on modern x86. If you are choosing a CRC for new code today and have no compatibility constraint, this is the one to pick.

None of these is a hash for verifying a download. When you want a checksum to confirm a file arrived intact, the MD5 hash generator is the familiar tool, and MD5 vs SHA-256 covers which digest to trust for what. A CRC answers “did this get corrupted”; a cryptographic hash answers “is this the exact content I expect”.

8. A CRC does not stop tampering

Here are two JSON messages with opposite meanings and the same CRC-32:

message A = b'{"to":"alice","amount":100}  H\xf6\xc0E'
message B = b'{"to":"mallory","amount":999}\xb2\xb2\xc2\xc2'
CRC-32(A) = 0x12345678
CRC-32(B) = 0x12345678

SHA-256 over the same two:

A -> a83b90f5e3f21dd32039e0e693a8b15864eda83e258c115deeee50a60c09ae23
B -> f9319ee0507d719c7260535dff33df721ddd9c0e1857690f3d8e02f92601b742

Nothing here was brute-forced. The trailing bytes were solved for algebraically. CRC is a linear function, and appending four bytes to a message maps onto CRC-32 output as a bijection, so for any message and any target value there is exactly one four-byte suffix that gets you there, and finding it is arithmetic.

Appending the four solved bytes 46 C9 6E 0B to hello world:

CRC-32(b'hello world' + 46 C9 6E 0B) = 0xDEADBEEF   target 0xDEADBEEF   MATCH

An attacker who can modify your payload can therefore also fix up your CRC, in constant time, without needing to search for anything. Prefixing the message with a secret does not save it: for an edit that keeps the length, the correction applied to the CRC depends only on the bits that changed, so it can be computed without ever knowing the secret. That is the shape of the attack that broke WEP.

CRC defends against transmission noise, which is random and has no goal. An adversary is neither, and against one a CRC gives you nothing. When the requirement is authenticity rather than integrity, you need a keyed construction: the HMAC generator produces one over any payload and key, and why webhook signature verification fails walks through the parts that trip people up when they wire it into a real endpoint.

9. FAQ

Is CRC-16/CCITT the same as CRC-16/CCITT-FALSE?

No, CRC-16/CCITT-FALSE and CRC-16/CCITT are different variants. CCITT-FALSE is CRC-16/IBM-3740: init 0xFFFF, unreflected, check value 0x29B1. The variant usually meant by plain CCITT is CRC-16/KERMIT: init 0x0000, reflected, check 0x2189. They share the polynomial 0x1021 and agree on nothing else.

Why does an online calculator disagree with my device?

Different parameters, almost always. The tool defaults to one variant and the device implements another. Run the ASCII bytes 123456789 through both, compare the two check values against the table in section 4, and the mismatched parameter usually identifies itself in under a minute.

Why does Modbus put the CRC low byte first?

Because the specification says so, and it is the only field in the frame that behaves that way. Register addresses and counts are big-endian; the trailing CRC is not. Compute CRC-16/MODBUS over the whole frame including those two bytes and check for 0x0000 rather than reordering them yourself.

What exactly do refin and refout reverse?

Bits inside a byte, never bytes inside a message. refin reverses the eight bits of each input byte before processing; refout reverses the final register before the last XOR. Both are independent of endianness, which governs how a multi-byte value is laid out on the wire.

Should I use CRC-16 or CRC-32?

CRC-16 misses a random corruption roughly once in 65,536 times, CRC-32 once in 4.3 billion. For short serial frames of a few dozen bytes, CRC-16 is fine and is usually mandated by the protocol anyway. For files, network frames and anything over a few kilobytes, use CRC-32.

Can I use a CRC as an API signature?

No, a CRC cannot serve as an API signature. CRC is linear, so anyone who alters the payload can recompute a matching value, and appending four chosen bytes reaches any CRC-32 target you name. Signatures need a secret key and a non-linear construction. Use HMAC with SHA-256.

Can the original data be recovered from a CRC value?

No, the original data cannot be recovered from a CRC value. A CRC-16 compresses any input to 16 bits, so infinitely many messages share each value. The reverse direction is still useful to attackers, though: given a target value you can construct a message that produces it, which is exactly why a CRC authenticates nothing.

The vendor document only says “CRC-16”. How do I pin down the parameters?

Ask for one captured frame plus the CRC their device computed for it, then run the 32-combination sweep from section 4 against that pair. If more than one parameter set survives, repeat with a second frame and keep the intersection. Two samples are almost always decisive.

Tags: crc checksum modbus embedded data-integrity

Related Articles

View all articles