Overview
Modbus RTU was defined as a 16-bit register protocol, but most analog process values from PLCs, drives, flow meters, and power meters are 32-bit IEEE 754 single-precision floating-point numbers. A float occupies two adjacent Modbus registers, and the way those two 16-bit words (and the two bytes inside each word) are placed on the wire depends entirely on the byte-order convention of the slave device. When the SCADA or HMI master interprets the register pair with the wrong convention, the decoded value is garbage: typically a sign-flipped, exponent-corrupted number that may appear to fluctuate across positive and negative ranges, or simply as a small value that does not track the process variable.
This reference explains the IEEE 754 single-precision bit layout, the four possible word/byte permutations over Modbus, how to identify which permutation a specific slave uses by reading raw registers, and the concrete configuration changes required in WinCC OA, WinCC Professional, TIA Portal, Ignition, FactoryTalk View SE/ME, and Codesys-based controllers. Verification procedures and a symptom-decoding matrix are included.
IEEE 754 Single-Precision Bit Layout
An IEEE 754 single-precision float is laid out as follows:
| Bit Range | Width | Field | Description |
|---|---|---|---|
| 31 | 1 bit | Sign (S) | 0 = positive, 1 = negative |
| 30 - 23 | 8 bits | Exponent (E) | Biased by +127, range -126 to +127 |
| 22 - 0 | 23 bits | Mantissa (M) | Fractional part with implicit leading 1 |
Total width: 32 bits = 4 bytes = exactly two Modbus 16-bit registers. The value of the float is computed as:
value = (-1)^S × 2^(E-127) × (1.M)
When the mantissa is all zeros and the exponent is 0x7F8 (E = 128), the value is ±infinity. When E = 0 and M = 0, the value is ±0. When E = 255 and M ≠ 0, the value is NaN.
Modbus PDU Register Model
Modbus function codes that carry 16-bit register data:
| Function Code | Name | Access |
|---|---|---|
| 01 | Read Coils | Read discrete bits |
| 02 | Read Discrete Inputs | Read discrete bits |
| 03 | Read Holding Registers | Read 16-bit words |
| 04 | Read Input Registers | Read 16-bit words |
| 05 | Write Single Coil | Write one bit |
| 06 | Write Single Register | Write one 16-bit word |
| 15 (0x0F) | Write Multiple Coils | Write N bits |
| 16 (0x10) | Write Multiple Registers | Write N 16-bit words |
A 32-bit float requires two consecutive registers in either the holding or input register space, addressed as N and N+1. See the Modbus Tools protocol reference and the Wikipedia Modbus entry for the full PDU specification. See also Schneider Electric FAQ FA168406 on proper Modbus installation, and the National Instruments in-depth Modbus protocol article for additional framing on master/slave roles and transaction timing.
Endianness Fundamentals
Endianness is the order in which a multi-byte scalar is decomposed into bytes for serial transmission. There are two independent axes:
- Word order (which 16-bit register holds the high half of the 32-bit value): big-endian places the high word in the lower-numbered register; little-endian places the high word in the higher-numbered register.
- Byte order within each word (which byte of the 16-bit word is transmitted first over the UART): big-endian transmits the high byte of the word first; little-endian transmits the low byte of the word first.
These two axes can be combined independently, producing four possible permutations of the four bytes of a float. The permutations are conventionally named by the order in which the four bytes appear in consecutive Modbus registers, byte-by-byte:
| Permutation | Register N bytes | Register N+1 bytes | Also called | Typical vendors |
|---|---|---|---|---|
| ABCD | [A][B] | [C][D] | Big-endian, big-endian words | Modicon, Schneider M340, ABB AC500, most legacy PLCs |
| BADC | [B][A] | [D][C] | Byte-swap, big-endian words | Many Chinese instruments, some Wago 750 couplers, certain Schneider PM5000 series |
| CDAB | [C][D] | [A][B] | Word-swap, big-endian bytes | Wago 750-xxxx with float, Beckhoff EL/ELM terminals, some ABB drives, Phoenix Contact Axioline |
| DCBA | [D][C] | [B][A] | Little-endian, little-endian | Some Codesys v3 controllers, some MicroLogix/CompactLogix native Modbus, certain sensor heads |
where A = bits 31-24, B = bits 23-16, C = bits 15-8, D = bits 7-0 of the IEEE 754 value.
Inline Diagram: Byte Mapping
Symptom-Decoding Matrix
When a float reads incorrectly, the symptom is highly diagnostic. Force a known reference value (1.0, 100.0, 273.15) into the slave and observe how the master interprets it:
| Observed master value (slave = 1.0) | Observed master value (slave = 100.0) | Likely permutation | Action |
|---|---|---|---|
| 1.0 | 100.0 | ABCD (correct) | No swap, this is the natural decode |
| Very small ~1e-39 or 0 | Very small ~7e-43 | CDAB | Swap the two 16-bit words (word-swap) |
| Large negative ~ -1.7e+38 or NaN | Large negative ~ -1.5e+34 | BADC | Swap bytes within each word (byte-swap) |
| Different small value, no obvious pattern | Different small value | DCBA | Swap words and bytes (full reversal) |
| Sign flips when sign bit of high word changes | Same | ABCD with sign bit in wrong word | CDAB most likely |
| Value reads correctly but trend is jittery near zero | Same | Denormal or scale mismatch | Check engineering units and scaling factor |
Manual Hex Decoding Procedure
Use this procedure on any Modbus master to identify the slave's permutation. It does not require any float conversion—only a Modbus poll that returns the two 16-bit register values in hexadecimal.
- Force a known float into the slave. The canonical reference is 1.0 =
0x3F800000, because the byte pattern is easy to recognize. - Read the two consecutive Modbus registers and write down their values in hex.
- Concatenate the four bytes in the order received, treating each 16-bit register as high-byte, low-byte.
- Match the resulting 32-bit hex pattern against the table below.
| Reg N | Reg N+1 | Byte order | Decode as |
|---|---|---|---|
| 0x3F80 | 0x0000 | ABCD | 1.0 (correct, no swap) |
| 0x0000 | 0x3F80 | CDAB | 1.0 after word-swap |
| 0x803F | 0x0000 | BADC | 1.0 after byte-swap within each word |
| 0x0000 | 0x803F | DCBA | 1.0 after full reversal |
Repeat the test with 2.0 = 0x40000000, -1.0 = 0xBF800000, and 100.0 = 0x42C80000 to confirm. Negative values are particularly diagnostic: the sign bit (bit 31) of -1.0 is set in byte A (= 0xBF); if you observe the sign in the wrong word, the permutation is CDAB or DCBA.
Step-by-Step Fix in WinCC OA (Siemens SCADA)
The original symptom in this scenario was reading tag address 578 as float in WinCC OA, receiving a nonsensical value spanning negative to positive while integer reads worked correctly. The fix is to add the appropriate word-swap transformation on the SCADA side.
Prerequisites
- WinCC OA version 3.16 or later (3.18+ recommended for the Modbus driver improvements).
- The Modbus RTU driver
ModDrvinstalled and licensed. - A known reference value writable on the slave (or a Modbus simulator such as
modbus-server,diagslave, or a Pythonpymodbusserver).
Configuration
- Open the PARA module and navigate to the RTU device driver configuration.
- Locate the Driver panel for the affected connection. Confirm the COM port, baud rate, parity, stop bits, and slave (unit) ID.
- Open the DP (Data Point) editor for the analog input tag at address 578.
- Set the data point type to FLOAT (32-bit IEEE 754). Do not select INT or WORD; that will truncate the 32-bit value and produce the integer-looks-correct-but-float-looks-wrong symptom.
- On the conversion tab, set the Swap attribute:
| Observed Reg N / Reg N+1 pattern | WinCC OA swap selector |
|---|---|
| 0x3F80 / 0x0000 (ABCD) | No swap (default) |
| 0x0000 / 0x3F80 (CDAB) | WORD_SWAP (swap the two 16-bit words) |
| 0x803F / 0x0000 (BADC) | BYTE_SWAP (swap bytes within each word) |
| 0x0000 / 0x803F (DCBA) | BOTH (full 32-bit reversal) |
- Save the DP and re-activate the driver. The change should take effect within one poll cycle.
- Verify by reading the value back in the para value list or by adding the tag to a graphical view with a numeric widget.
Step-by-Step Fix in TIA Portal (Siemens S7-1200 / S7-1500)
For a Siemens S7-1200/1500 acting as a Modbus RTU master via the MB_CLIENT or MB_MASTER instruction, or as a Modbus TCP master via MB_CLIENT in TCP mode, the same four permutations apply.
- Place the raw two-register read into a
WORDarrayData[0..1]usingMB_MASTERwith mode 0 (read holding registers) or 4 (read input registers). - Combine the two words into a
DWORDwith explicit byte assembly. The S7 byte order is big-endian.
For CDAB slaves (word-swap):
// Data[0] is the LOW word, Data[1] is the HIGH word
// Reassemble as big-endian DWORD then interpret as REAL
#rawDword := SHL(IN := WORD_TO_DWORD(#Data[1]), N := 16)
OR WORD_TO_DWORD(#Data[0]);
#realValue := DWORD_TO_REAL(#rawDword);
For BADC slaves (byte-swap within each word):
// Each word's bytes are swapped
#swapped0 := SHL(IN := WORD_TO_DWORD(#Data[0]), N := 8)
OR SHR(IN := WORD_TO_DWORD(#Data[0]), N := 8) & 16#00FF;
// Or use the ROR/WORD swap pattern
#realValue := DWORD_TO_REAL(SHL(IN := WORD_TO_DWORD(#swapped0), N := 16)
OR WORD_TO_DWORD(#swapped0));
A more compact SCL approach is to use the standard library SWAP function or the IEC function SWAP_DWORD (only available in S7-1500) to perform the full 32-bit reversal in one call.
Step-by-Step Fix in Studio 5000 / Logix Designer (Allen-Bradley)
For CompactLogix or ControlLogix acting as a Modbus master using the MODBUS AOI (Add-On Instruction) or a MESSAGE instruction with CIP Modbus, the byte order is the Logix-native little-endian. The CIP REAL data type is stored as IEEE 754 in the tag memory.
- Read the two 16-bit registers into a
SINT[4]array using a Modbus message with anAtomicdata type ofINT[2]. - Copy the four bytes into the
REALtag in the correct order usingCOPand aSINToverlay.
For CDAB slaves:
// Source: Modbus response in ModbusWords[0..1] (INT)
// Destination: ModbusReal (REAL)
ModbusBytes[0] := ModbusWords[1].0; // high byte of high word
ModbusBytes[1] := ModbusWords[1].1; // low byte of high word
ModbusBytes[2] := ModbusWords[0].0; // high byte of low word
ModbusBytes[3] := ModbusWords[0].1; // low byte of low word
COP(ModbusBytes[0], ModbusReal, 4);
Alternatively, use the SWAP_FILE AOI from the Rockwell Sample Code library, or write a small routine that performs the byte reordering explicitly.
Step-by-Step Fix in Ignition (Inductive Automation)
For Ignition 8.x with the Modbus TCP / RTU driver, the byte order is configured per-tag using the Tag Scaling or the Modbus Address Mapping property sheet.
- Open the tag configuration in the Designer.
- Set the data type to Float4.
- Set the Byte Order selector. Ignition exposes four presets:
Big-endian (ABCD),Little-endian byte swap (BADC),Little-endian word swap (CDAB),Little-endian (DCBA). - Apply and verify with a known reference value.
Step-by-Step Fix in FactoryTalk View SE / ME
FactoryTalk View uses the RSLinx OPC server for Modbus communication. The byte order is set in RSLinx Classic's Modbus driver configuration:
- Open RSLinx Classic, launch RSWho.
- Right-click the Modbus device and select Device Properties.
- On the Data Table tab, set the Word Order to one of: High word first (big-endian), Low word first (little-endian / word-swap), High word first, byte-swap, Low word first, byte-swap.
- Save and restart the RSLinx driver for the change to take effect.
Step-by-Step Fix in Codesys v3
Codesys uses the ModbusTCPSlave / ModbusRTUSlave library for slave and ModbusTCPClient / ModbusRTUClient for master. To exchange floats:
- Declare two adjacent
WORDvariables at the holding register address. - Use the
__MEMCMPand__MEMCPYutilities, or theDWORD_TO_REALwith a manual word swap, to assemble the float.
// CDAB slave example
wHigh := pModbusRTU^.wRegs[1]; // N+1 is high word
wLow := pModbusRTU^.wRegs[0]; // N is low word
dwRaw := SHL(WORD_TO_DWORD(wHigh), 16) OR WORD_TO_DWORD(wLow);
rValue := DWORD_TO_REAL(dwRaw);
PLC-Side Considerations (Float Source Encoding)
The endianness problem originates at the slave, not the master. The slave's PLC must write the float into its Modbus holding register area in a way that the master can interpret. Common patterns:
Siemens S7-300 / S7-400 / S7-1200 / S7-1500 (STEP 7 / TIA Portal)
Siemens stores REAL in big-endian byte order. The standard block move from a DB REAL tag to a Modbus holding register area using BLKMOV or MOVE_BLK preserves the byte order. Therefore, the default Siemens encoding is ABCD (big-endian, big-endian words). To produce CDAB output, swap the words manually before writing to the Modbus area.
Allen-Bradley ControlLogix / CompactLogix
Logix stores REAL in little-endian (the LSB is at the lowest memory address). When the MODBUS AOI or a CIP-Modbus gateway exposes a Logix REAL to a Modbus master, the default encoding depends on the gateway. Most CIP-to-Modbus gateways (ProSoft, Spectrum, Moxa MGate) default to ABCD but can be configured for CDAB. Always verify with the gateway datasheet.
Schneider Electric Modicon M340 / M580
Modicon uses big-endian natively. Floats are stored in %MW holding register pairs as ABCD. The Unity Pro / EcoStruxure Control Expert READ_VAR and WRITE_VAR block can be configured to swap words (CDAB) or bytes (BADC) on per-request basis, controlled by the SWAP input parameter.
Vendor-Specific Float Handling Quick Reference
| Vendor / Device | Default float encoding | How to change |
|---|---|---|
| Modicon M340 / M580 (Unity Pro) | ABCD | SWAP input on READ_VAR / WRITE_VAR |
| Siemens S7-1200 / S7-1500 | ABCD (native) | Manual word-swap in SCL |
| Allen-Bradley CompactLogix | ABCD via CIP gateway default | Gateway byte-order setting |
| Wago 750-xxx (e!COCKPIT / Codesys) | CDAB | Library FB swap parameter |
| Beckhoff EL/ELM Modbus TCP | CDAB | TcModbusSrv FB configuration |
| ABB AC500 | ABCD | Word-swap FB in Automation Builder |
| ABB ACS880 drive (fieldbus adapter) | ABCD for cyclic, CDAB for some parameters | Per-parameter setting in drive |
| Schneider PM5000 power meter | ABCD (newer) / BADC (older) | Firmware register table |
| Generic Chinese sensor (4-20 mA + Modbus) | BADC (very common) | Master-side byte-swap |
| Endress+Hauser Fieldgate | ABCD | Native |
Edge Cases and Field-Proven Caveats
- Integer reads work, float reads do not. This is the strongest diagnostic. Integers fit in one 16-bit register and are not subject to word boundaries; floats span two registers and therefore expose the byte-order issue. If both 16-bit and 32-bit values decode correctly, the byte order is ABCD.
- Value reads correctly but with the wrong sign. The sign bit of the float is in the most-significant byte (byte A). If the sign flips when the real process variable crosses zero, the permutation is CDAB or DCBA—the high word is in register N+1, not N.
- Value reads as a denormalized number near zero. The mantissa may be decoded as the exponent, and vice versa. Apply word-swap.
- Value reads as a large negative number or NaN. The exponent field may be in the wrong word, producing exponent values above 255 (NaN) or below 0 (denormal). Apply word-swap or full reversal.
- Float reads correctly but writes are inverted. The master is interpreting the write correctly but the slave is decoding with the wrong byte order. Apply the swap on the write direction only, leaving read unchanged—some SCADA drivers allow this.
- Float read works, but second float in a multi-register block fails. Some vendors only apply the non-standard byte order to specific register ranges. Verify with the vendor's register map PDF.
- Modbus gateway adds its own swap. When a Modbus RTU device is bridged through a Modbus TCP gateway (Moxa, Advantech, HMS Anybus), the gateway may normalize to CDAB. Test with both the direct RTU and the gateway in the loop to isolate.
- Floating-point precision loss on scaling. If the float reads in the correct order but the value is off by a factor of 10, 100, or 0.1, the engineering unit scaling is wrong, not the byte order. Check the slave's register map for any scaling constant.
-
NaN propagation. If the float is undefined on the slave (e.g., channel not connected), the slave may write a non-standard bit pattern. A floating-point
NaNin IEEE 754 has exponent 0xFF and a non-zero mantissa. The master may display this as 0, as a question mark, or as a large negative value depending on its NaN handling. - Time-of-day stamps. Some process variables use 32-bit Unix timestamps; these have the same endian problem as floats. The same procedure applies.
Verification Procedure
- Inject a known reference value. Use the slave's configuration tool or a Modbus simulator to force the value of the float at address 578 (or the relevant address). Recommended: 1.0, 100.0, -1.0, 273.15.
-
Read the raw registers. Use a Modbus master tool (e.g.,
modpoll,modbus-cli, QModMaster, CAS Modbus Scanner) to read registers N and N+1 and write down the raw hex values. - Identify the permutation. Match against the manual hex decoding table in this article.
- Apply the SCADA-side swap. Configure the DP / tag with the appropriate swap selector.
- Re-poll and compare. The master should now display 1.0 / 100.0 / -1.0 / 273.15 exactly (within IEEE 754 precision, which is roughly 7 decimal digits for single-precision).
- Repeat under load. Poll the tag at 100 ms or 1 s for 10 minutes and verify the value does not drift. Drift indicates either a swap in only one of read/write directions, or a multi-master contention issue.
- Sign and edge tests. Verify negative values read correctly. Verify the value at 0.0 (all four bytes = 0x00) reads as exactly 0.0, not as a small denormal.
- Range test. Verify the value at 1e6 and 1e-6 reads correctly. These values exercise both the high and low bytes of the mantissa, exposing any byte-level corruption.
Troubleshooting Matrix
| Symptom | Likely root cause | Fix |
|---|---|---|
| Integer read works, float reads as 0 or NaN | CDAB word-swap needed | Swap two 16-bit words in master |
| Float reads with sign flipped relative to process | CDAB or DCBA | Word-swap |
| Float reads as very large number, fluctuating | BADC byte-swap | Swap bytes within each word |
| Float reads as small fluctuating value | DCBA full reversal | Swap both words and bytes |
| Float reads correctly, write goes to wrong register | Read and write have different swap settings | Apply swap to write direction only |
| Float reads correctly on simulator, wrong on real device | Gateway reordering | Disable gateway byte conversion or test direct RTU |
| Float reads correctly, then drifts over time | Multi-master Modbus collision | Reduce poll rate, add inter-frame delay |
| Float reads correctly, but value is 10x, 100x, 0.1x off | Scaling factor mismatch | Check engineering units in register map |
| Float reads correctly in engineering tool, wrong in SCADA | SCADA driver default byte order differs from tool | Configure SCADA swap to match tool |
Best-Practices Checklist
- Document the byte order in the device's register map. If the manufacturer is silent, contact them and get a written answer.
- Configure the SCADA/PLC master with the exact permutation, not a guess.
- Use a Modbus simulator during commissioning to test all four permutations in minutes.
- For new installations, prefer slaves that document ABCD explicitly—this is the historical Modbus standard for multi-register values and is the default in Modicon, ABB, and Schneider documentation.
- When integrating a Modbus TCP gateway between an RTU device and the master, verify the gateway's byte-order behavior. Most gateways default to CDAB on the TCP side.
- Avoid mixing Modbus RTU and Modbus TCP for the same logical value within one project—the byte order may differ between the two transports.
- Use the
READ_VARandWRITE_VARSWAP parameter in Unity Pro / Control Expert to abstract the swap into the I/O scanner rather than into the application code.
Why do integer reads work but float reads fail in Modbus RTU?
Integers fit inside a single 16-bit Modbus register and are not affected by word-order conventions. IEEE 754 single-precision floats are 32 bits, occupying two adjacent 16-bit registers. When the master and slave disagree on whether the high word is in register N or N+1 (and whether each word's bytes are big-endian or little-endian), the assembled 32-bit value is incorrect and typically reads as a large number, a small number, or NaN. Configure the SCADA driver to swap words (CDAB), bytes (BADC), or both (DCBA) as required.
How do I find the correct byte order for a Modbus RTU slave?
Write a known reference value such as 1.0 to the slave and read the two consecutive 16-bit registers back in hexadecimal. The byte pattern for 1.0 is 0x3F800000. Match the high and low words you read against the four permutations: ABCD = 0x3F80 / 0x0000, CDAB = 0x0000 / 0x3F80, BADC = 0x803F / 0x0000, DCBA = 0x0000 / 0x803F. The pattern that reconstructs 1.0 is the slave's correct encoding.
What is the difference between CDAB and BADC byte order?
CDAB means the two 16-bit words are swapped (the high word is in register N+1 rather than N), but the bytes inside each word remain in big-endian order. BADC means each word is correct (high word in N, low word in N+1) but the bytes within each word are swapped (low byte first instead of high byte first). CDAB is the most common non-standard permutation; BADC is common in older Chinese sensor instruments and some Schneider PM5000 meters.
Does the same byte order apply to Modbus TCP and Modbus RTU?
The Modbus application layer (function codes, register model, data interpretation) is identical between RTU and TCP. The byte-order convention is a property of the slave device, not the transport, so the same four permutations apply. However, when a Modbus TCP gateway is inserted between an RTU device and a TCP master, the gateway may normalize or re-order the bytes. Always verify the byte order at the master side, not at the field device side, after the gateway is in the path.
Can the byte order be different for read and write on the same Modbus register?
In theory no, because the encoding is a property of how the data is stored on the slave. In practice, yes, when an asymmetric gateway or a custom Modbus wrapper is involved. Some SCADA drivers allow configuring the swap independently for read and write directions. If the read value is correct but the write is incorrect (or vice versa), configure the swap on the affected direction only and re-test with a known reference value.