Modbus Frame Decoder
Decode Modbus RTU/TCP frames: unit, function, registers, CRC check.
CRC valid
| Field | Bytes | Value | Meaning |
|---|---|---|---|
| Unit / slave address | 11 | 0x11 | 17 |
| Function | 03 | 0x03 | Read Holding Registers |
| Start address | 00 6B | 0x006B | 107 |
| Quantity | 00 03 | 0x0003 | 3 |
| CRC-16 (LE) | 76 87 | 0x8776 | valid |
This tool decodes Modbus RTU and Modbus TCP frames from raw hex bytes: unit address, function code, register addresses, quantities and data, with the RTU CRC-16 verified (or the MBAP header checked for TCP). Hover any field to highlight its bytes in the frame. Detection is automatic, and you can force RTU or TCP.
How it works
A Modbus RTU frame is address + PDU + CRC-16, where the PDU is the function code followed by its data. The CRC covers every byte before it and travels low byte first. A Modbus TCP frame replaces the address and CRC with a 7-byte MBAP header: transaction ID, protocol ID (always 0x0000), a length field counting the bytes that follow it, and the unit ID; the same PDU comes after.
Request and response PDUs share function codes, so the decoder infers direction from shape: a 4-byte body after a read function is an address + quantity request, while a body whose first byte equals the remaining length is a byte-count-prefixed response.
Worked example
→ unit 0x11 (17) · Read Holding Registers (0x03)
→ start address 0x006B (107) · quantity 3 · CRC valid
Same request over TCP: 00 01 00 00 00 06 11 03 00 6B 00 03
→ transaction 1 · protocol 0x0000 · length 6 · unit 0x11
Parameters
| Parameter | Value | Notes |
|---|---|---|
| RTU frame | addr(1) + PDU + CRC16(2, LE) | CRC-16/MODBUS over all preceding bytes |
| TCP frame | MBAP(7) + PDU | no CRC — TCP transport handles integrity |
| MBAP | txn(2) proto(2)=0 len(2) unit(1) | len = bytes after the length field |
| Exception | FC | 0x80, then code | e.g. 02 = Illegal Data Address |
Python implementation
# Verify a Modbus RTU frame's CRC (poly 0xA001 reflected form).
def crc16_modbus(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc
frame = bytes.fromhex("1103006B00037687")
assert crc16_modbus(frame[:-2]) == int.from_bytes(frame[-2:], "little")FAQ
▸How does auto-detect tell RTU from TCP?
A Modbus TCP frame starts with a 7-byte MBAP header whose Protocol ID is always 0x0000 and whose Length field must equal the number of bytes that follow it. If the header is consistent the frame is treated as TCP; otherwise the last two bytes are checked as an RTU CRC-16. You can also force the protocol manually.
▸Why does my frame show CRC FAIL?
The decoder recomputes CRC-16/MODBUS over every byte except the last two and compares with the received value (transmitted low byte first). For example, changing the valid frame 11 03 00 6B 00 03 76 87 to end in 88 fails: the computed CRC is still 0x8776 but the received one is now 0x8876.
▸What does a function code above 0x80 mean?
It is an exception response: the device echoes the request's function code with the top bit set, followed by an exception code. For example 01 83 02 C0 F1 is unit 1 answering function 0x03 with exception 02, Illegal Data Address.
▸Does Modbus TCP have a CRC?
No. TCP already guarantees integrity at the transport layer, so the RTU CRC-16 is dropped; instead validity is judged by the MBAP header consistency.
▸Is my frame data uploaded?
No — decoding happens entirely in your browser.