Problem Description
Reading a 32-bit IEEE 754 floating-point value from a Modbus RTU/TCP meter in a Siemens SIMATIC S7-300 or S7-400 with STEP 7 and the MB_MASTER library block returns hex data that does not decode to a sensible engineering value. Typical field symptoms:
- Two consecutive 16-bit input words reported by STEP 7 do not match the displayed process value on the meter.
- Byte-swap or word-swap attempts produce another nonsense number (for example, 4.05e17) but never the actual measurement.
- When the process variable is increased, the read value changes by a constant factor, jumps discontinuously, or remains stuck on a garbage number.
-
MB_MASTER.STATUSreturns 0x0000 (no error), which is misleading because the data is still wrong.
Example capture from STEP 7 (MB_MASTER, function code 03 or 04, 2 words starting at the user-supplied start address):
| Word | Hex (STEP 7 WORD) | Decoded as REAL (no swap) |
|---|---|---|
| Address 1 | W#16#0000 | 4.0532397e+17 (not a process value) |
| Address 2 | W#16#5CB4 |
Expected engineering range: ~16.0 to ~16.5 (process value 16.343). Expected IEEE 754 single-precision encoding of the displayed values:
| Display value | IEEE 754 hex, big-endian (Modbus wire order) |
|---|---|
| 23732.0 | 0x46B96800 |
| 16.343 | 0x4182BE77 |
| 1.0 | 0x3F800000 |
| 0.0 | 0x00000000 |
Because 0x0000 0x5CB4 (decoded as 4.0532397e17) does not correspond to any value the meter should be producing, the protocol transaction itself is delivering data the master did not intend to read. This is an addressing error at the Modbus application layer, not a float-format error. The next section shows that the fix is a one-line change to the start address.
Root Cause: Modbus 0-Based vs. 1-Based Addressing
Modbus is a 0-based protocol. The address field of every Modbus PDU is an offset from the start of the relevant register table (coil, discrete input, input register, holding register). The Modbus Application Protocol Specification V1.1b3 defines this address as a 16-bit unsigned offset starting at 0; the function code prefix (1, 2, 3, 4) is purely a documentation convention, not part of the wire format.
Meter manufacturers, however, document their register maps with 1-based numbering for human convenience, and they often add a function-code prefix to that number:
| Label in meter manual | Meaning | Hex to send in MB_MASTER / MB_CLIENT / MODB_341 |
|---|---|---|
| 40001 | Holding register 0 (the very first holding register) | 0x0000 |
| 40002 | Holding register 1 | 0x0001 |
| 40010 | Holding register 9 | 0x0009 |
| 30001 | Input register 0 | 0x0000 |
| "Register 4000" (no prefix, 1-based) | Holding register 3999 | 0x0F9F |
| "Address 4" (no prefix, 0-based) | Holding register 4 | 0x0004 |
The general rules to apply consistently across vendors:
- Subtract 1 from any 1-based number the vendor prints.
- If the vendor prints a "4xxxx" or "3xxxx" five-digit prefix, strip the leading 3 or 4 and subtract 1 from the remaining four digits.
- If the vendor uses 0-based numbering in the manual (some Schneider, ABB, and Siemens SIPROCESS meters), do not subtract — read the offset literally.
- Place the resulting 0-based offset into the master's start address input as a WORD or INT.
START_ADDR is 3999 (0x0F9F), not 4000 and not 4001. Sending 4000 reads the next pair of words past the float and yields unrelated data, which is exactly what the symptom in the source problem (0x5CB4 0x0000) looks like: the master pulled a pair of registers adjacent to the intended float and is now decoding random memory. This single rule resolves roughly 80% of all "Modbus float reads garbage" tickets on Siemens, Allen-Bradley, Schneider, and CODESYS platforms.IEEE 754 Single-Precision Layout (Quick Reference)
For diagnosing whether the master did read the correct pair of words, decode them as follows:
| Bits | Field | Description |
|---|---|---|
| 31 | Sign (S) | 0 = positive, 1 = negative |
| 30..23 | Biased exponent (E) | Stored as E + 127 for single precision; 0 and 255 are reserved |
| 22..0 | Mantissa (M) | Fractional part with implicit leading 1 |
Decoded value = (-1)^S × 1.M × 2^(E-127). Examples to keep at hand during commissioning:
- 16.343 = 0x4182BE77 = 0 10000011 00000101011111001110111
- 23732.0 = 0x46B96800 = 0 10001101 01110010110100000000000
- 1.0 = 0x3F800000
- 0.0 = 0x00000000
- 0x7F800000 = +infinity; 0x7FC00000 = NaN
When STEP 7 shows 0x0000 0x5CB4 in a buffer that you then interpret as a REAL, the resulting value 4.05e17 has exponent bits 0x7F and mantissa bits that do not match any physical process variable. This is a fast visual confirmation that the data is from the wrong register pair.
Word Order Across the Modbus Wire
Modbus RTU/TCP is big-endian on the wire. For a 32-bit float, the slave transmits the high word first, then the low word:
- Wire bytes for 16.343: 0x41 0x82 0xBE 0x77 (high word 0x4182, then low word 0xBE77)
Inside a Siemens S7 CPU, words are stored little-endian. A 32-bit REAL in a DB or memory is laid out with the low word at the lower byte address and the high word at the higher byte address:
- S7 MD100 holding 16.343: MW100 = 0xBE77 (low), MW102 = 0x4182 (high)
This produces a second class of decode errors that mimic the off-by-one problem:
| Wire order (correct) | S7 storage after MB_MASTER (correct layout) | REAL interpretation |
|---|---|---|
| 0x4182 then 0xBE77 (big-endian) | MW0 = W#16#BE77, MW2 = W#16#4182 | 16.343 (correct) |
| 0xBE77 then 0x4182 (byte-swap, wrong) | MW0 = W#16#4182, MW2 = W#16#BE77 | ≈ -0.189 (wrong) |
Best practice: read into a paired 2-word buffer (for example, an ARRAY[0..1] OF WORD in a DB), then assemble the two words into a 32-bit REAL in the S7 little-endian layout. For STEP 7 classic (S7-300/400) the canonical STL pattern is:
// STEP 7 STL - assemble 2 Modbus words into a REAL
// Assumes the first word is in MW0 (0xBE77) and the second word in MW2 (0x4182).
// The MD0 then reads as 16.343 when viewed as REAL.
L MW 0 // low word = 0xBE77
T MD 100 // write to MD100 (REAL target)
L MW 2 // high word = 0x4182
T MW 102 // placed at MW102 (high word of MD100)
// MD100 now holds 16.343 (REAL)
If the value decodes correctly only after a manual byte swap, the data is correct but the destination buffer is laid out in the opposite byte order; the fix is to remap the destination or use a swap block such as:
// STEP 7 STL - byte-swap 32-bit REAL (big-endian Modbus <-> little-endian S7)
L MD 0 // input REAL
TAD // swap all 4 bytes in the accumulator
T MD 10 // swapped REAL
For S7-1200 and S7-1500 with TIA Portal, use the system block "SWAP" with MODE = 1 for a 4-byte word swap, or read the float directly into a tag of type Real using the standard MB_CLIENT data block; the runtime applies the byte order configured in the MB_DATA_PTR connection.
STEP 7 MB_MASTER Configuration Checklist
When the master is an S7 CPU using the standard Modbus master library, confirm the following parameters before changing the address. The MB_MASTER FB (part of the "MODBUS" library for S7-300/400) and the MODBUS_PN blocks for S7-1500/ET200SP all share a similar interface:
-
REQ= rising edge to trigger a single transaction. -
MODE= 0 (send request) or 1 (send/receive on event). - Data pointer must point to a DB large enough:
DATA_LEN≥ 5 bytes for FC03/04 (slave address, function code, byte count, data, implicit CRC). For a 2-word float allocate at least 6 bytes in the data block. - Function code: FC03 for holding registers, FC04 for input registers — match the meter's register class.
-
START_ADDR: 0-based offset, subtract 1 from any 1-based vendor number. - Quantity: 2 for a 32-bit float, 4 for a 64-bit double, 1 for a 16-bit integer.
- Timeout: 1000 ms minimum for RS-485 at 9600 baud, scaled up for 19200/38400 baud or longer cable runs.
- Baud and parity: match the meter's serial settings (2400/4800/9600/19200, 8N1, 8E1, or 8O1).
Common error codes returned by MB_MASTER when the start address is wrong:
| MB_MASTER STATUS (W#16#...) | Meaning in this context | Likely fix |
|---|---|---|
| 0x0000 | Transaction OK, data valid | None — but verify the data with a hex check |
| 0x0001 | No response from slave (timeout or CRC error) | Check wiring, baud, slave address, RS-485 termination |
| 0x0002 | Slave reported exception 02 (illegal data address) | Address out of range — recheck start address vs. vendor map |
| 0x0003 | Slave reported exception 03 (illegal data value) | Quantity or function code not supported |
| 0x0004 | CRC error in receive | Wiring, termination, grounding, EMI shielding |
| 0x0005 | Parameter assignment error | DATA_LEN too short, wrong function code, or invalid start address |
Step-by-Step Resolution
- Pull the meter's register map. Identify the float register(s). Note the exact label as printed in the manual: "4000", "40001", "Holding register 4" — all mean slightly different things. Confirm whether the manual uses 0-based or 1-based numbering by looking at the introductory section of the manual or by reading register 0 (the slave's first register) and comparing to whatever the manual calls it.
-
Compute the 0-based offset.
// Pseudocode for converting vendor-printed address to Modbus wire address // vendorNumber is the number printed in the manual, as a WORD if (label starts with "4" and has 5 digits) { offset = vendorNumber - 40001; // for holding registers } else if (label starts with "3" and has 5 digits) { offset = vendorNumber - 30001; // for input registers } else { offset = vendorNumber - 1; // plain 1-based "register N" } // if vendor documents are explicitly 0-based, replace the above with: // offset = vendorNumber; - Place the offset into MB_MASTER.START_ADDR. For the symptom in the source problem, if the manual said the float is at "address 4000" (1-based, no prefix), START_ADDR = 3999. If the manual said "register 40001" (4xxxx prefix), START_ADDR = 40001 - 40001 = 0 — meaning the float is at the very first holding register of the meter, which is the same as register 0 in 0-based terms. The two conventions collapse to a single 0-based value once the rule is applied consistently.
- Set the quantity to 2 for a 32-bit IEEE 754 float. Each Modbus word is 16 bits, so 2 words = 32 bits = 1 single-precision float.
- Decode the two words into a REAL. Use the S7 little-endian layout described above, or apply a byte swap if the meter ships words in the opposite order (rare; Modbus is big-endian on the wire by spec, and the IEEE 754 high-word-first convention is the standard).
- Verify against the meter's display. Force a known value on the meter (a calibration input of 100.0 is a good test point) and confirm the REAL in the S7 DB reads within single-precision LSB precision of 100.0 (LSB at 100.0 is approximately 7.6e-6).
Verification
Confirm the fix end-to-end with these three checks:
- Hex check. Trigger MB_MASTER and read the first two words of the response. Decode the IEEE 754 single-precision value manually (or with a hex-to-float converter) and confirm the low and high words match the expected encoding of the meter's display value. For 16.343, expect 0xBE77 (low word in the S7 destination) and 0x4182 (high word).
- Sweep test. Apply three calibration values spanning the meter's range (e.g., 0.0, 50.0, 100.0). Confirm the S7 REAL tracks each within one LSB. This catches both the addressing fix and any leftover byte-order issue in one pass.
- Exception code check. With the corrected address, MB_MASTER.STATUS should return 0x0000 on success. Persistent 0x0002 means the meter does not expose the float at the address you are now sending; consult the vendor's full register map, not just the highlighted float row in the marketing brochure.
Cross-Platform Notes
The Modbus 0-based address rule applies to every master. The vendor-to-wire conversion is the only thing that changes.
| Platform | Master block / instruction | Address input | Float handling |
|---|---|---|---|
| Siemens S7-300/400, STEP 7 classic | FB MB_MASTER (MODBUS library) or MODB_341 on CP341 | 0-based WORD on START_ADDR | Manual 2-word to REAL assembly or TAD swap |
| Siemens S7-1200/1500, TIA Portal | MB_CLIENT / MB_SERVER instructions | 0-based on MB_DATA_ADDR | Read directly into Real tag if MB_DATA_PTR is typed Real, otherwise use SWAP block |
| Siemens ET200S / ET200SP | MODBUS_RTU / MODBUS_PN on IM | 0-based per Modbus spec | Same as S7-1500 |
| Schneider Modicon M340 / M580, EcoStruxure Control Expert (Unity Pro) | READ_VAR / WRITE_VAR with MODBUS function block | 0-based on input | Use the %MW area and %MD alias, or the FLOAT_TO_WORD / WORD_AS_FLOAT functions |
| Allen-Bradley MicroLogix 1100/1400, RSLogix 500 | MSG block with MODBUS master (AOI or library) | 0-based integer file offset | Copy two INT words to a SINT array and use COP to a REAL tag |
| Allen-Bradley CompactLogix / ControlLogix, Studio 5000 | MSG with MODBUS TCP or CIP-to-MODBUS gateway | 0-based on the MODBUS side of the gateway | Read into INT[2] tag, then use the COP instruction to move to a REAL tag |
| CODESYS 3.5, Wago PFC200, Beckhoff CX | ModbusTCPMaster / ModbusRTUMaster library | 0-based on input | Read into WORD array, then __ByteSwap__DWORD or manual byte reordering before REAL cast |
Field Commissioning Checklist
Use this checklist on every new Modbus float integration to catch the off-by-one, byte-order, and quantity errors before they ship:
- Confirm the meter manual's address convention (0-based vs 1-based, with or without function-code prefix).
- Read register 0 first, regardless of whether the application needs it. This confirms the master is talking to the right slave and the convention is decoded correctly.
- Read 2 words and decode the IEEE 754 hex manually before assigning the result to a REAL tag. A 30-second hex check prevents a 3-day debug.
- Force a known value on the meter and verify the S7 REAL. Re-verify with a second and third value across the meter's range.
- Record the exact MB_MASTER parameter set (start address, quantity, function code, data pointer) in the project documentation. Off-by-one errors always come back when the next engineer extends the project.
- Monitor MB_MASTER.STATUS in the HMI or web server. A persistent 0x0002 is an addressing bug, not a wiring bug.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| 0x0000 0x5CB4 in response, REAL decodes to ~4e17 | Off-by-one address; reading words adjacent to the float | Subtract 1 from vendor-printed address; verify with exception 0x0002 test |
| Correct hex but REAL decodes to mirrored or wrong-magnitude value | Byte/word order mismatch between Modbus big-endian and S7 little-endian | Re-map destination to place high word second; use TAD byte-swap or S7-1500 SWAP block |
| MB_MASTER.STATUS = 0x0002 | Start address outside the meter's register table | Re-examine vendor register map; subtract 1 from 1-based numbers |
| Value changes with the process variable but by a constant factor (e.g., 10× or 100×) | Reading the wrong pair of registers — possibly a scaled integer or different unit | Walk the vendor register map one register at a time; identify the actual float location |
| Value sticks on 0.0 or 3.4028235e+38 (REAL max) | Empty input buffer or all-ones exponent (uninitialized float) | Check MB_MASTER.DATA_LEN, polling enable, and connection status |
| Intermittent correct then garbage values | RS-485 echo, missing termination, or noise on 2-wire bus | Add 120 Ω termination at both ends, bias resistors, lower baud, shielded cable |
| Garbage only at high baud (≥ 38400) or long cable runs | Signal integrity / propagation delay | Reduce baud to 19200 or 9600, add repeater, separate from VFD cables |
| All-zero words but meter is responding to FC03 | Float lives in a different function class (FC04 input vs FC03 holding) | Switch function code from 03 to 04 (or vice versa) and retest |
Edge Cases and Field-Proven Caveats
- Some meters, notably certain Chinese multifunction power meters and older Carlo Gavazzi units, document their floats at a 0-based address already. In that case, do not subtract 1. Read the meter specification literally and verify with a known calibration value before assuming the convention.
- If the meter has a configurable Modbus base address (PowerLogic PM5000, Schneider iEM3000, and some ABB M2M meters allow shifting the entire register map), confirm the live base address by reading register 0 (or whatever the vendor calls "Device Address") before assuming the float lives at the printed offset.
- For Modbus TCP over a managed Ethernet switch, do not enable IGMP snooping or broadcast suppression on the Modbus VLAN — many slaves do not respond to broadcast requests, and certain switches will silently drop unregistered multicast.
- Some meters expose the float as two separate 16-bit registers plus a third register containing a scaling factor (for example, a Modbus "Long" with explicit exponent register). Decode the scaling register and apply it to the integer pair rather than expecting a native IEEE 754 float.
- For S7-1500 / TIA Portal, the standard Modbus blocks are
MB_CLIENTandMB_SERVERin the "MODBUS TCP" or "MODBUS RTU" instruction set. These expect 0-based addresses inMB_DATA_ADDR, consistent with the protocol spec. The S7-1500 Modbus RTU library and CP1542SP-1 IRC documentation is published on the Siemens Industry Online Support portal at support.industry.siemens.com. - For CP341 / CP441 with the "MODBUS master" option, the
MODB_341andMODB_441blocks use a 0-based address and a "data area" pointer. The CP341 Modbus master driver manual entry is available on the Siemens support portal under entry ID 27013598. - Modbus TCP uses the exact same PDU as Modbus RTU; only the transport changes. The off-by-one and word-order rules above apply identically to TCP port 502. The Modbus Application Protocol V1.1b3 specification defines the 0-based address field for every function code, regardless of transport.
Frequently Asked Questions
Why does my Modbus float come back as a huge number like 4e17 instead of 16.343?
You are reading the wrong pair of registers. The most common cause is a 1-based vs 0-based address error: vendor documentation lists the float at "register 4000" (1-based) but the master sends 4000 in the Modbus PDU (0-based), pulling two words of unrelated data. Subtract 1 from the vendor-printed address and verify with a Modbus exception code 0x02 (illegal data address) test by intentionally sending an out-of-range address to confirm the slave is responsive.
My S7 reads the correct two words but the REAL decodes to a mirrored or wrong-magnitude value. How do I fix the byte order?
Modbus is big-endian on the wire but the S7 stores REALs in little-endian. Copy the two returned words into a 32-bit REAL with the high word placed at the high memory address (for example, MB_MASTER's data goes into MD100 with 0xBE77 in the low word and 0x4182 in the high word). In STEP 7 classic, use the TAD instruction to byte-swap an MD; in TIA Portal, use the SWAP block with MODE = 1 for a 4-byte word swap, or read the float directly into a Real tag using MB_CLIENT's typed data pointer.
What MB_MASTER STATUS code indicates a wrong address on the meter side?
STATUS = W#16#0002 (slave reported exception 02 "illegal data address") is the strong indicator that the master is requesting an address the meter's register table does not contain. Fix the start address per the meter manual's numbering convention. STATUS = 0x0001 indicates no response at all — a wiring or slave address issue rather than a Modbus register offset problem; STATUS = 0x0003 indicates an unsupported function code or quantity.
How many Modbus words do I need to read for a 32-bit float?
Two consecutive 16-bit words. Set MB_MASTER quantity to 2 (or READ_LEN = 2 in CP341/441, or LEN = 2 in MB_CLIENT). A 64-bit IEEE 754 double requires 4 words. A 16-bit signed or unsigned integer requires 1 word. A 32-bit unsigned integer (Modicon "Long") also requires 2 words but does not need the IEEE 754 decode — copy the two words to a DWORD and interpret directly.
Does the off-by-one rule also apply to Modbus TCP, or only RTU?
It applies to both. The Modbus Application Protocol V1.1b3 specification defines a 0-based address field in the PDU for every function code, regardless of whether the transport is RTU over RS-485 or TCP over Ethernet port 502. The vendor's "40001" or "register 4000" label is the only thing that varies; the protocol-level address is always offset by 1 from the human label.
My S7-1500 with TIA Portal and MB_CLIENT reads the right value but the HMI shows 0.0 or a frozen value. What is wrong?
Most often the MB_CLIENT data block is sized too small. MB_DATA_LEN must be at least 5 bytes for FC03/04 read transactions, and the source tag at MB_DATA_PTR must be a typed array of bytes large enough to hold the response. A second common cause is the connection's REQ input not being re-triggered every cycle; MB_CLIENT is level-triggered on busy and edge-triggered on REQ, so a one-shot ladder rung will only produce a single read. See the MB_CLIENT help page in TIA Portal and the Siemens Industry Online Support FAQ for the connection configuration.