You've connected your Python script to a Schneider M221, requested a holding
register pair where you know the temperature is 25.5°C, and you get back:
1.401298464324817e-45
Not 25.5. Not even close. Some weird denormal float so small it might as
well be zero.
Welcome to the Modbus float32 byte-swap trap. It's the single most
common reason engineers think pymodbus is broken when it isn't. The fix
is 5 lines of code once you know what's happening — and that's what this
article is about.
Why this happens
The Modbus specification defines registers as 16-bit unsigned integers.
Period. That's it. The spec was written in 1979 and at the time, that's
all anyone needed.
But modern PLCs need to send floats, 32-bit integers, doubles, strings.
The "solution" the industry adopted was: just split the larger value
across multiple 16-bit registers.
The problem? The Modbus spec says nothing about the order.
Each PLC vendor decided independently whether to put the high word first
or the low word first. Whether to swap bytes within a word. Whether to
flip everything. The result is four different ways to encode the same
float, and there's no in-band way to know which one you're getting.
This is why your PLC software shows 25.5°C while your Python script reads
1.4e-41 — same bytes, different decoding order.
The 4 byte orders you'll meet
A 32-bit float 25.5 in IEEE-754 is the byte sequence:
0x41 0xCC 0x00 0x00
Now watch what happens when different PLCs put these 4 bytes into 2 Modbus
registers:
| Order | Register N | Register N+1 | Common in |
|---|---|---|---|
| ABCD (big-endian, no swap) | 0x41CC |
0x0000 |
Allen-Bradley, ABB, "clean" implementations |
| CDAB (word-swap) | 0x0000 |
0x41CC |
Schneider M221/M241, some Siemens |
| BADC (byte-swap) | 0xCC41 |
0x0000 |
Rare — some old controllers |
| DCBA (byte + word swap) | 0x0000 |
0xCC41 |
Mostly legacy hardware |
If you decode bytes in ABCD order when the device sent CDAB, you get
a tiny denormal float (the 1.4e-41 you're seeing), or nan, or inf,
or just nonsense.
Schneider's CDAB (word-swap) is the trap that catches most people.
Code: decode all 4 orders in Python
Pure stdlib, no dependencies. Drop this into your project:
import struct
def decode_float32(reg_high: int, reg_low: int, byte_order: str = "ABCD") -> float:
"""Decode two 16-bit Modbus registers into a float32.
byte_order:
ABCD - big-endian, no swap (default IEEE-754)
CDAB - word-swap (Schneider, some Siemens)
BADC - byte-swap within each word
DCBA - byte + word swap
"""
# Pack the two registers into 4 bytes (big-endian)
raw = struct.pack(">HH", reg_high, reg_low)
if byte_order == "ABCD":
return struct.unpack(">f", raw)[0]
elif byte_order == "CDAB":
# Swap the two 16-bit words
return struct.unpack(">f", raw[2:4] + raw[0:2])[0]
elif byte_order == "BADC":
# Swap bytes within each word
return struct.unpack(">f", raw[1:2] + raw[0:1] + raw[3:4] + raw[2:3])[0]
elif byte_order == "DCBA":
# Reverse all 4 bytes
return struct.unpack(">f", raw[::-1])[0]
else:
raise ValueError(f"Unknown byte order: {byte_order}")
# Example: read a value with pymodbus and decode all 4 ways
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("192.168.1.10")
client.connect()
response = client.read_holding_registers(address=100, count=2)
reg_high, reg_low = response.registers[0], response.registers[1]
print(f"ABCD: {decode_float32(reg_high, reg_low, 'ABCD')}")
print(f"CDAB: {decode_float32(reg_high, reg_low, 'CDAB')}")
print(f"BADC: {decode_float32(reg_high, reg_low, 'BADC')}")
print(f"DCBA: {decode_float32(reg_high, reg_low, 'DCBA')}")
The trick: print all four when you're integrating a new device. The
one that gives a sensible value (e.g. 25.5 when you know the temperature
is 25.5) tells you the device's encoding. Lock it in for that device.
Real-world examples by vendor
Based on years of integration work, here's what I've seen most often:
- Schneider M221, M241, M340 → Almost always CDAB (word-swap). This is the #1 source of "my Python script is broken" tickets.
- Siemens S7-1200, S7-1500 → Depends on how the float is stored. If exposed via Modbus TCP wrapper, often CDAB too.
- Allen-Bradley CompactLogix → Usually ABCD when exposed through Prosoft Modbus modules. Clean.
- Mitsubishi FX series → ABCD in most configurations.
- Delta DVP → ABCD.
- Energy meters (Schneider PM5xxx) → CDAB typically.
- VFDs (ABB ACS, Schneider Altivar) → Mixed — always test all 4.
The lesson: never assume. Even within one vendor, different product
lines pick different orders. Always do the "print all 4" test on a new
device.
Common pitfalls
1. Some libraries do the swap silently — and silently wrong
pymodbus's built-in BinaryPayloadDecoder lets you specify
byteorder=Endian.BIG, wordorder=Endian.LITTLE (which is CDAB). But
the API is confusing enough that I've seen people set both to BIG and
spend hours debugging. Decode manually with struct — it's clearer.
2. The "negative number" red flag
If your decoded value is -1.5e+38 when you expect 25.5, you've
almost certainly got the byte order wrong. Single-digit positive
temperatures don't accidentally encode as huge negative numbers under any
normal scaling.
3. int32 has the same problem
This article focuses on float32 because it's the most painful, but all
multi-register types have this issue. int32, uint32, int64, double —
they all get split across registers and they all need the byte-order
treatment.
4. Some devices store the swap setting in a register
Annoying but real: a few high-end PLCs let the user configure
endianness. Always check the device manual for any "byte order" or "word
order" parameter before running test code.
Wrapping up
The Modbus float32 byte-swap is one of those things that looks like a
bug in your code but is actually a quirk of the protocol's history. Once
you know to test all 4 orders, you can integrate any device in a few
minutes instead of a few hours.
If this saved you time, a ⭐ on my open-source Modbus logger means a lot:
Python-Modbus-Serial-Logger-GUI —
it has all 4 decoding modes built in, plus a GUI so you don't need to
keep restarting Python while testing.
If you're using this in production work and want a more polished version
with a CSV recorder and Modbus TCP support, I sell a commercial license
at pokhts.gumroad.com.
I write about industrial Python and protocol internals at
dev.to/philyeh — new article every two
weeks. If you've got a specific PLC integration story you want to read
about, drop a comment.
Top comments (0)