The first Modbus device I talked to from an ESP32 ignored every request I sent. The wiring was right, the baud rate was right, the frame looked right in a logic analyser. The problem was two bytes at the end: the CRC. I’d computed a CRC-16 — just not the CRC-16 Modbus uses — and I’d appended it high byte first.
This guide explains the Modbus CRC properly, using the CRC calculator to check the numbers.
A Modbus RTU frame
A request to read 10 holding registers starting at address 0 from slave 1 is six bytes:
01 03 00 00 00 0A
Slave address 01, function code 03 (read holding registers), start address 00 00, count 00 0A. Then two CRC bytes are appended. For this frame the CRC value is 0xCDC5, and the bytes on the wire are:
01 03 00 00 00 0A C5 CD
Low byte first. That’s the second mistake I made, and it’s the most common one.
Which CRC-16?
“CRC-16” isn’t one algorithm. A CRC is defined by six parameters, and Modbus uses this set (called CRC-16/MODBUS in the RevEng catalogue):
| Parameter | Value |
|---|---|
| width | 16 |
| poly | 0x8005 |
| init | 0xFFFF |
| refin | true |
| refout | true |
| xorout | 0x0000 |
| check (“123456789”) | 0x4B37 |
Compare that with other CRC-16s you’ll find in libraries:
- CRC-16/ARC: same polynomial, but init
0x0000— check0xBB3D. - CRC-16/IBM-3740 (often mislabelled “CCITT”): poly
0x1021, init0xFFFF, not reflected — check0x29B1. - CRC-16/XMODEM: poly
0x1021, init0x0000— check0x31C3. - CRC-16/KERMIT: poly
0x1021, reflected — check0x2189.
If a library’s function doesn’t give 0x4B37 for the ASCII string 123456789, it isn’t the Modbus CRC. The calculator shows every preset for your input at once, and if you paste in a known-good CRC value from a working device, it tells you which presets produce it. That’s how I identify undocumented CRCs now.
What “reflected” means
refin = true means each input byte is processed least-significant bit first. refout = true means the final register is bit-reversed. In practice, a reflected CRC is implemented by shifting right instead of left and using the bit-reversed polynomial — for 0x8005, that’s 0xA001. You’ll see 0xA001 in almost every Modbus CRC snippet online; it’s the same polynomial written backwards.
Reflected CRCs are conventionally transmitted least-significant byte first, which is why Modbus puts C5 before CD. The calculator shows the value, both byte orders, and the conventional wire order, and appends the CRC to your frame so you can compare it directly with a capture.
A tested C function
The calculator generates C for whatever parameters you choose. This is the bitwise version for Modbus:
#include <stddef.h>
#include <stdint.h>
/* 16-bit CRC: poly=0x8005u init=0xFFFFu refin=true refout=true xorout=0x0000u */
uint16_t crc16_modbus(const uint8_t *data, size_t len) {
uint16_t crc = 0xFFFFu;
while (len--) {
crc ^= *data++;
for (int k = 0; k < 8; k++) crc = (crc & 1u) ? (uint16_t)((crc >> 1) ^ 0xA001u) : (uint16_t)(crc >> 1);
}
return (uint16_t)((crc ^ 0x0000u) & 0xFFFFu);
}
It’s 8 shift-and-XOR steps per byte and compiles to a few dozen bytes. There’s also a table-driven version that processes a byte per lookup — about 8× faster, at the cost of a 512-byte table, which on an Uno you’d want in PROGMEM. For Modbus at 9600 baud the bitwise version is more than fast enough.
The ByteKiln test suite compiles both versions for every preset with gcc -Wall -Werror and checks them against the catalogue check values, so you can paste them with some confidence.
Appending and checking on Arduino
Sending a request from an ESP32 over RS-485:
uint8_t frame[8] = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A };
uint16_t c = crc16_modbus(frame, 6);
frame[6] = c & 0xFF; // low byte first
frame[7] = c >> 8;
Serial2.write(frame, 8);
Checking a response has a neat shortcut. If you run the CRC over the whole received frame, including its two CRC bytes, a correct frame gives zero:
bool valid = crc16_modbus(response, length) == 0;
That works for Modbus because of how the CRC’s remainder behaves when the CRC is appended in the right byte order — another reason the order matters.
Debugging checklist
- Check
crc16_modbus("123456789")returns0x4B37. If not, it’s the wrong algorithm. - Put the low byte first on the wire.
- Don’t include the CRC bytes when calculating the CRC to send.
- Make sure you’re computing over raw bytes, not a hex string. “01 03” as text is five ASCII characters, not two bytes — the calculator’s hex mode parses
01 03 00 00 00 0A,0x01,0x03and010300into the same six bytes. - RS-485 half-duplex: switch DE/RE back to receive only after the last byte has left the UART (
Serial2.flush()), or you’ll clip the CRC.
The calculator’s frame line gives you the exact bytes to send. If you’re watching the bus from your laptop, the number base converter helps decode register values once the CRC is right.
Beyond Modbus
The same approach works for any protocol that uses a CRC: SMBus PEC (CRC-8/SMBUS), Dallas 1-Wire ROM codes (CRC-8/MAXIM-DOW), XMODEM file transfer (CRC-16/XMODEM), and CRC-32 in Ethernet and ZIP files. For large firmware images, the CRC calculator hashes files in a background worker, so you can compare a CRC-32 against a bootloader’s without freezing the page. For cryptographic integrity instead of error detection, use a real hash — the hash generator covers SHA-256 and friends.