· Industrial Protocols · 3 min read
Modbus RTU & TCP: The Definitive Guide (Protocol, Endianness & MBAP)
Beyond the wire. Master Byte Swapping, Function Codes, and the real structure of Modbus messages for 100% reliable integrations.

If you’ve succeeded in keeping your RS485 cable noise-free (check my RTU Survival Guide), the real challenge now begins: interpreting the data.
Modbus is an “Application Layer” protocol. It doesn’t care if it travels over a serial cable or fiber optics; the message logic is almost always the same. But that “almost always” is where engineers lose hours.
1. The 4 Tables: The Real Data Model
Don’t think of Modbus as variables. Think of it as 4 independent drawers.
| Model | Ref. | Access | Type | Industrial Usage |
|---|---|---|---|---|
| Coils | 0x | R/W | Bit | Actuating motors, lights, alarm resets. |
| Discretes | 1x | R | Bit | Sensor states, limit switches. |
| Input Reg. | 3x | R | 16-bit | Analog sensor readings (ADC). |
| Holding | 4x | R/W | 16-bit | Setpoints, configurations, VFD settings. |
The Offset Trap (0 vs 1)
This is error #1.
- PLC Manual: Says “Configure the setpoint in register 40101”.
- Real Protocol: On the wire, the message requests register 100.
Protocols are Zero-Based (start at 0). Most modern SCADAs allow you to write 40101 and they perform the subtraction, but if you use libraries like pymodbus, you must request 100.
2. Float Hell and Endianness
Modbus natively only understands 16-bit integers. How do we send a temperature of 25.43? We use two registers (32 bits).
Here is where the manufacturer can be creative. The Endianness (byte order) and Word Swapping (register order) vary.
If you read a value and it gives you something absurd like 1.43e-38, you have an Endianness problem. I’ve documented all combinations and how to fix them in Python here:
👉 Docs: Endianness Mastery Guide
Code Example (Fixing Swap)
import struct
# Read two registers [REG_HI, REG_LO]
regs = client.read_holding_registers(100, 2).registers
# Manual 'Swap' if the PLC is unusual (CD AB)
pack = struct.pack('>HH', regs[1], regs[0])
float_val = struct.unpack('>f', pack)[0]3. Modbus TCP: The MBAP Header
Unlike RTU (which uses CRC at the end), Modbus TCP wraps the message in a 7-byte header called the MBAP Header.
- Transaction ID (2 bytes): Allows the client to know which response corresponds to which question (essential in high-speed networks).
- Unit ID (1 byte): Crucial if you are using a TCP to RTU Gateway. This byte tells the gateway which serial ID to redirect the message to. If talking directly to a PLC, it’s usually 0 or 255.
4. Practical Cases: Error Troubleshooting
| Code | Meaning | Probable Cause |
|---|---|---|
| 01 | Illegal Function | The device doesn’t support that code (e.g., trying to write to an Input Register). |
| 02 | Illegal Address | The register doesn’t exist or is out of range. |
| 03 | Illegal Value | The data you’re sending is invalid (e.g., sending 500 to a 0-255 register). |
Conclusion
Integrating Modbus isn’t guessing. It’s reading the register map, understanding the offset, and mastering Endianness. To see full examples of production-ready asynchronous servers and clients, download my toolkit:
👉 Repo: modbus-troubleshooting-toolkit
Sources and References:



