If you've spent any time integrating PLCs with an IIoT platform, you've encountered the moment: you read a temperature register that should show 72.5°F, but instead you get 1,118,044,160. Or worse — NaN. Or a negative number that makes zero physical sense.
Welcome to the Modbus float encoding problem. It's the #1 source of confusion in industrial data integration, and it trips up experienced engineers just as often as beginners.
This guide goes deep on how 32-bit floating-point values are actually stored and transmitted over Modbus — covering register pairing, word-swap variants, byte ordering, and the practical techniques that production IIoT systems use to get correct readings from heterogeneous equipment fleets.
Why Modbus and Floats Don't Play Nicely Together
The original Modbus specification (1979) defined only 16-bit registers. Each holding register (4xxxx) or input register (3xxxx) stores exactly one unsigned 16-bit word — values from 0 to 65,535.
But modern PLCs need to represent temperatures like 215.7°F, flow rates like 3.847 GPM, and pressures like 127.42 PSI. A 16-bit integer can't hold these values with the precision operators need.
The solution: pack an IEEE 754 single-precision float (32 bits) across two consecutive Modbus registers. Simple enough in theory. In practice, it's a minefield.
The IEEE 754 Layout
A 32-bit float uses this bit structure:
Bit: 31 30..23 22..0
S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
│ │ └── Mantissa (23 bits)
│ └── Exponent (8 bits, biased by 127)
└── Sign (1 bit: 0=positive, 1=negative)
The float value 72.5 encodes as 0x42910000:
- Sign: 0 (positive)
- Exponent: 10000101 (133 - 127 = 6)
- Mantissa: 00100010000000000000000
That 32-bit value needs to be split across two 16-bit registers. Here's where the problems start.
The Four Word-Order Variants
Different PLC manufacturers split 32-bit floats into register pairs using different byte and word ordering. There are four possible arrangements, and encountering all four in a single plant is common:
Variant 1: Big-Endian (AB CD) — "Network Order"
The most intuitive layout. The high word occupies the lower register address.
Register N : 0x4291 (bytes A, B)
Register N+1: 0x0000 (bytes C, D)
Reconstruct: (Register_N << 16) | Register_N+1 → 0x42910000 → 72.5
Used by: Many Allen-Bradley/Rockwell PLCs, Schneider Modicon M340/M580, some Siemens devices.
Variant 2: Little-Endian Word Swap (CD AB)
The low word comes first. This is surprisingly common.
Register N : 0x0000 (bytes C, D)
Register N+1: 0x4291 (bytes A, B)
Reconstruct: (Register_N+1 << 16) | Register_N → 0x42910000 → 72.5
Used by: Many Modbus TCP devices, Conch controls, various Asian-manufactured PLCs.
Variant 3: Byte-Swapped Big-Endian (BA DC)
Each 16-bit word has its bytes reversed, but word order is normal.
Register N : 0x9142 (bytes B, A)
Register N+1: 0x0000 (bytes D, C)
This requires swapping bytes within each word before combining.
Used by: Some older Emerson/Fisher devices, certain Yokogawa controllers.
Variant 4: Byte-Swapped Little-Endian (DC BA)
The least intuitive: both word order and byte order are reversed.
Register N : 0x0000 (bytes D, C)
Register N+1: 0x9142 (bytes B, A)
Used by: Rare, but you'll find it in some legacy Fuji and Honeywell equipment.
How Production IIoT Systems Handle This
In a real manufacturing environment, you don't get to choose which word order your equipment uses. A single plant might have:
- TCU (Temperature Control Units) using Modbus RTU at 9600 baud, storing floats in registers 404000-404056 with big-endian word order
- Portable chillers on Modbus TCP port 502, using 16-bit integers (no float encoding needed)
- Batch blenders speaking EtherNet/IP natively, where float handling is built into the CIP protocol
- Dryers with Modbus TCP and CD-AB word swapping
A well-designed edge gateway handles this with per-device configuration. The key insight: float decoding is a device-level property, not a global setting. Each equipment type gets its own configuration that specifies:
- Protocol (Modbus RTU, Modbus TCP, or EtherNet/IP)
- Register address (which pair of registers holds the float)
- Element count — set to 2 for a 32-bit float spanning two registers
- Data type — explicitly declared as
float vs. int16 vs. uint32
Here's a generic configuration example for a temperature control unit reading float values over Modbus RTU:
{
"protocol": "modbus-rtu",
"tags": [
{
"name": "Delivery Temperature",
"register": 4002,
"type": "float",
"element_count": 2,
"poll_interval_sec": 60
},
{
"name": "Mold Temperature",
"register": 4004,
"type": "float",
"element_count": 2,
"poll_interval_sec": 60
},
{
"name": "Flow Rate",
"register": 4008,
"type": "float",
"element_count": 2,
"poll_interval_sec": 60
}
]
}
Notice the element_count: 2. This tells the gateway: "read two consecutive registers starting at this address, then combine them into a single 32-bit float." Getting this wrong is the most common source of incorrect readings.
The modbus_get_float() Trap
If you're using libmodbus (the most common C library for Modbus), you'll encounter modbus_get_float() and its variants:
modbus_get_float_abcd() — big-endian (most standard)
modbus_get_float_dcba() — fully reversed
modbus_get_float_badc() — byte-swapped, word-normal
modbus_get_float_cdab() — word-swapped, byte-normal
The default modbus_get_float() function uses CDAB ordering (word-swapped). This catches many engineers off guard — they read two registers, call modbus_get_float(), and get garbage because their PLC uses ABCD ordering.
Rule of thumb: Always test with a known value. Write 72.5 to a register pair in your PLC, read both registers as raw uint16 values, and observe which bytes are where. Then select the appropriate decode function.
Practical Decoding in C
Here's how you'd manually decode a float from two Modbus registers, handling the common big-endian case:
float decode_float_be(uint16_t reg_high, uint16_t reg_low) {
uint32_t combined = ((uint32_t)reg_high << 16) | (uint32_t)reg_low;
float result;
memcpy(&result, &combined, sizeof(float));
return result;
}
float decode_float_ws(uint16_t reg_low, uint16_t reg_high) {
uint32_t combined = ((uint32_t)reg_high << 16) | (uint32_t)reg_low;
float result;
memcpy(&result, &combined, sizeof(float));
return result;
}
Never use pointer casting (*(float*)&combined). It violates strict aliasing rules and can produce incorrect results on optimizing compilers. Always use memcpy.
Element Count and Register Math
One subtle but critical detail: when you configure a tag to read a float, the element count tells the gateway how many 16-bit registers to request in a single Modbus transaction.
For a single float:
- Element count = 2 (two 16-bit registers = 32 bits)
- Read function code 3 (holding registers) or 4 (input registers)
- The response contains 4 bytes of data
For an array of 8 floats (e.g., reading recipe values from a batch blender):
- Element count = 16 (8 floats × 2 registers each)
- Single Modbus read request for 16 consecutive registers
- Far more efficient than 8 separate read requests
This is where contiguous register optimization matters. If you have tags at registers 4000, 4002, 4004, 4006, 4008 — all 2-element floats — a smart gateway combines them into a single Modbus read of 10 registers instead of 5 separate reads. This reduces bus traffic by 60-80% on RTU networks where every transaction costs 5-20ms of serial turnaround time.
Modbus RTU vs TCP: Float Handling Differences
RTU (Serial)
Serial Modbus has strict timing requirements. The inter-frame gap (3.5 character times of silence) separates messages. At 9600 baud with 8N1 encoding:
- 1 character = 11 bits (start + 8 data + parity + stop)
- 1 character time = 11/9600 = 1.146ms
- 3.5 character silence = ~4ms
When reading float values over RTU, response timeout configuration matters. A typical setup:
Baud: 9600
Parity: None
Data bits: 8
Stop bits: 1
Byte timeout: 4ms (gap between consecutive bytes)
Response timeout: 100ms (total time to receive response)
If your byte timeout is too tight, the response may be split into two frames, and the second register of your float pair gets dropped. If you're seeing correct first-register values but garbage in the combined float, increase byte timeout to 5-8ms.
TCP (Ethernet)
Modbus TCP eliminates timing issues but introduces transaction ID management. Each request gets a transaction ID that the slave echoes back. For float reads, the process is identical — request 2 registers, get 4 bytes back — but the framing is handled by TCP, so there's no byte-timeout concern.
The default Modbus TCP port is 502. Some devices use non-standard ports; always verify with the equipment manual.
Common Pitfalls and Troubleshooting
1. Reading Zero Where You Expect a Float
Symptom: Register pair returns 0x0000 0x0000 → 0.0
Likely cause: Wrong register address. Remember the Modbus address convention:
- Addresses 400001-465536 use function code 3 (read holding registers)
- Addresses 300001-365536 use function code 4 (read input registers)
- The actual register number = address - 400001 (for holding) or address - 300001 (for input)
A tag configured at address 404000 maps to holding register 4000 (function code 3). If you accidentally use function code 4, you're reading input register 4000 instead — a completely different value.
2. Reading Extreme Values
Symptom: You get values like 4.5e+28 or -3.2e-15
Likely cause: Wrong word order. You're combining registers in the wrong sequence. Try swapping the two registers and recomputing.
3. Getting NaN or Inf
Symptom: NaN (0x7FC00000) or Inf (0x7F800000)
Likely causes:
- Word-order mismatch producing an exponent field of all 1s
- Reading a register that doesn't actually contain a float (it's a raw integer)
- Sensor disconnected — some PLCs write NaN to indicate a failed sensor
4. Values That Are Close But Off By a Factor
Symptom: You read 7250.0 instead of 72.5
Likely cause: The PLC stores values as scaled integers, not floats. Many older PLCs store temperature as an integer × 100 (so 72.5°F = 7250). Check the PLC documentation for scaling factors. This is especially common with Modbus devices that use single registers (element count = 1) for process values.
5. Intermittent Corrupt Readings
Symptom: 99% of readings are correct, but occasionally you get wild values.
Likely cause: On Modbus RTU, this is usually CRC errors that weren't caught, or electrical noise on the RS-485 bus. Add retry logic — read the registers, if the float value is outside physical bounds (e.g., temperature > 500°F for a plastics process), retry up to 3 times before logging an error.
Real-World Benchmarks
In production IIoT deployments monitoring plastics manufacturing equipment, typical float-read performance:
| Protocol | Float Read Time | Registers per Request | Effective Throughput |
|---|
| Modbus RTU @ 9600 | 15-25ms | 2 (single float) | ~40 floats/sec |
| Modbus RTU @ 9600 | 30-45ms | 50 (contiguous block) | ~1,000 values/sec |
| Modbus TCP | 2-5ms | 2 (single float) | ~200 floats/sec |
| Modbus TCP | 3-8ms | 125 (max block) | ~15,000 values/sec |
| EtherNet/IP | 1-3ms | N/A (native types) | ~5,000+ tags/sec |
The lesson: Modbus RTU float reads are slow individually but scale well with contiguous reads. If you have 30 float tags spread across non-contiguous addresses, it's 30 × 20ms = 600ms per polling cycle. Group your tags by contiguous address blocks to minimize transactions.
Best Practices for Production Systems
-
Declare types explicitly in configuration. Never auto-detect float vs. integer — always specify the data type per tag.
-
Use element count = 2 for floats. This is the most common source of misconfiguration. A float is 2 registers, always.
-
Test with known values during commissioning. Before going live, write a known float (like 123.456) to the PLC and verify the IIoT platform reads it correctly.
-
Document word order per device type. Build a device-specific configuration library. A TrueTemp TCU uses ABCD, a GP Chiller uses raw int16 — capture this per equipment model.
-
Implement bounds checking. If a temperature reading suddenly shows 10,000°F, that's not a process event — it's a decode error. Log it, don't alert on it.
-
Add retry logic for RTU reads. Serial networks are noisy. Retry failed reads up to 3 times before reporting an error status.
-
Batch contiguous registers. Instead of reading registers 4000-4001, then 4002-4003, then 4004-4005 as three separate transactions, read 4000-4005 as a single 6-register request.
How machineCDN Handles Float Encoding
machineCDN's edge gateway is built to handle the float encoding problem across heterogeneous equipment fleets. Each device type gets a configuration profile that explicitly declares register addresses, data types, element counts, and polling intervals — eliminating the guesswork that causes most float decoding failures.
The platform supports Modbus RTU, Modbus TCP, and EtherNet/IP natively, with automatic protocol detection during initial device discovery. When a new PLC is connected, the gateway attempts EtherNet/IP first (reading the device type tag directly), then falls back to Modbus TCP on port 502. This dual-protocol detection means a single gateway can service mixed equipment floors without manual protocol configuration.
For plastics manufacturers running TCUs, chillers, blenders, dryers, and conveying systems, machineCDN provides pre-built device profiles that include correct register maps, data types, and word-order settings — so the float encoding problem is solved before commissioning begins.
Getting float encoding right is the foundation of trustworthy IIoT data. Every OEE calculation, every alarm threshold, every predictive maintenance model depends on correct readings from the plant floor. Invest the time to verify your decoding — the downstream value is enormous.