Convert a CODESYS REAL into two Modbus WORD registers

6 min read · Published November 3, 2026

A REAL is 4 bytes, so it travels as two consecutive Modbus WORD registers holding its original bytes. Pack it with MEMCPY into a two-element WORD array, never a numeric conversion. Then agree word order with the master and prove the pair with a known test value.

A REAL is 4 bytes; a Modbus register carries 2. So one REAL rides as two consecutive WORD registers that hold its original bytes, unchanged. Pack it with a memory copy: MEMCPY(ADR(awTemp[0]), ADR(rTemp), 4), where 4 is a byte count. Keep numeric conversions out of this path, because REAL_TO_WORD turns 23.7 into 23 and the fraction is lost before the value leaves the PLC. Below are the declaration, the copy, the register mapping, and the word-order agreement that makes the master decode exactly the number you packed.

What rides in a register

IEC 61131-3 fixes the storage sizes, and a Modbus register is 16 bits, so the register count follows from the byte count:

Type Bytes Registers What it carries
WORD 2 1 whole numbers up to 65535
DWORD 4 2 32-bit integers and bit patterns
REAL 4 2 process values with a fraction
LREAL 8 4 double-precision values

A REAL is one IEEE 754 single-precision value: 1 sign bit, 8 exponent bits, 23 mantissa bits. Reserve consecutive registers for each value, for example 40001 and 40002 in the classic holding-register numbering. Mind the numbering: that notation counts from 1, while many masters speak protocol addresses from 0, so client register 40001 can travel as address 0 on the wire. Confirm each end's convention before you debug data formats.

Why a numeric conversion is the wrong tool

REAL_TO_WORD(23.7) gives 23. The master decodes the pair as a float and shows 23.0: the fraction died in the PLC. Any value above 65535 does not fit a WORD at all. REAL_TO_DWORD is also arithmetic, and on large inputs the conversion itself overflows, a failure covered in REAL_TO_DWORD FPU overflow and how to guard it. The register pair must carry the float's own 4 bytes. That is a copy job, and a copy preserves every bit.

What you need

  • The CODESYS project, with the value held in a REAL variable.
  • MEMCPY resolved. In CODESYS 3 it is a standard-library call; if the name shows unresolved, add the StringUtils library in the Library Manager.
  • Two free consecutive registers in the Modbus slave configuration.
  • A master that reads both registers in one request, decodes a 32-bit float, and exposes a word-order option.
  • A test value with four distinct bytes, such as 23.7.
  • Basic communication already working end to end. Read one known WORD through the same slave first. If that read fails, the fault is connection, addressing or mapping, and no data-format work will fix it; Modbus RTU master no reply and address errors walks that repair.

The copy, step by step

  1. Declare the value and its register pair:
VAR
    rTemp  : REAL;                 // the process value
    awTemp : ARRAY[0..1] OF WORD;  // its two Modbus registers
END_VAR
  1. Copy once per task cycle, before the slave can serve a request:
MEMCPY(ADR(awTemp[0]), ADR(rTemp), 4);   // 4 is bytes, not registers

The arguments are destination address, source address, byte count. The length is 4 because a REAL occupies 4 bytes; for an LREAL use 8 and grow the array to four elements.

  1. Map awTemp[0] to the lower register address and awTemp[1] to the next one up.
  2. Point the master at that address for a count of 2 registers in a single read.
  3. Set the decoder to 32-bit float and leave word order at the default until the test value tells you otherwise.
  4. Watch rTemp and both array elements online, in hex, while the source changes.

Pointers and unions do the same job by sharing memory instead of copying:

VAR
    pDword : POINTER TO DWORD;
    dwBits : DWORD;
END_VAR
pDword := ADR(rTemp);
dwBits := pDword^;   // the REAL's 32 bits seen as a DWORD

Then split dwBits into its two WORDs. A UNION gives both views under one name, and declaring a REAL and a BYTE array at the same address, such as both at %MW0, also works. Every one of these is bit reinterpretation and all deliver the same bytes; the copy into a plain array keeps the compiler in charge of addresses, so prefer it. Other controllers expose the same idea as a cast, shown in bit, byte and Dword packing with cast syntax.

Example: packing a 23.7 °C tank reading into registers 40001 and 40002

Inputs:

  • rTemp = 23.7 °C, a tank temperature you set in the PLC for the test.
  • 4 bytes per REAL, 2 bytes per register, from IEC 61131-3.
  • The 1 + 8 + 23 bit layout of IEEE 754 single precision.

The rule in words: the 32 bits split in half, the first 16 fill the lower register and the last 16 fill the next, most significant byte of each register first on the wire.

The arithmetic:

  • 23.7 = 1.48125 × 2^4, so the exponent field is 4 + 127 = 131 and the stored fraction is 0.48125.
  • 0.48125 × 2^23 = 4,037,017.6, which rounds to 4,037,018, hex 3D999A.
  • The full word is 0x41800000 + 0x3D999A = 0x41BD999A. Bytes on the wire: 41 BD 99 9A.
  • Register 40001 holds 0x41BD = 16,829. Register 40002 holds 0x999A = 39,322.
  • The master, decoding high word first, returns 23.7000008, the nearest float to 23.7.

Decision: raw reads of 16,829 and 39,322 mean the mapping is correct and you can enable float decoding. A decode near -1.6 × 10^-23 means the two words are swapped; a decode near -0.047 means the two bytes inside each register are swapped. Fix the order in the master's decoder setting, one change at a time, and re-read.

Word order: four ways the same bytes can arrive

The Modbus protocol fixes the byte order inside one register, most significant byte first. The order of the two registers in a float is left to agreement between the ends, and tools differ. A link can therefore deliver the right four bytes while the application shows the wrong number. With the 23.7 test value:

Layout Register 40001 Register 40002 Decodes to
High word first, the default on most tools 0x41BD 0x999A 23.7
Words swapped 0x999A 0x41BD about -1.6 × 10^-23
Bytes swapped inside each register 0xBD41 0x9A99 about -0.047
Both swapped 0x9A99 0xBD41 about -6 × 10^-23

Test with a fractional value, never with 0.0: zero is 0x00000000 and all four layouts decode it to 0, hiding the mistake until the plant runs real values. Read the pair as raw WORDs in hex at both ends, agree the order, then apply the change in exactly one place. If both ends swap, the swaps cancel and the setup works until someone changes one end. When packing changes are followed by checksum errors, CRC mismatches that come from word packing and byte order traces that family. The same discipline applies when a value is framed into bytes for a screen over a serial cable, covered in serial command byte encoding for display control.

Keep the pair in step

Build both registers from one application value at one point in the task. If the source changes between two separate updates, the master can receive the high word of the new value and the low word of the old one, and the decoded number is wrong even though both reads succeed. Copy once per cycle and have the master request both registers in one read. A combined request still may not deliver an atomic snapshot of the pair; if wrong values appear only while the source ramps quickly, check how the runtime schedules the communication task and move the copy so the pair is never half-updated.

How to check it worked

  1. Set rTemp to 23.7. In online mode, awTemp must show 16#41BD and 16#999A.
  2. At the master, read the two registers raw: 16,829 and 39,322.
  3. Enable float decoding: the master shows 23.7.
  4. Set rTemp to 100.0. That value is 0x42C80000, so expect 17,096 and 0. If you instead read 17,096 and 39,322, the low word is stale from the 23.7 copy: the pair decodes to about 100.3, a near miss you would overlook on a gauge, and the copy runs at the wrong point in the cycle.

The decision

Recommendation: pack every REAL with MEMCPY into a two-element WORD array, map the two words to consecutive registers, and refresh the array once per task cycle. Standardize on the high word in the lower register address, set the master's decoder to that order, and prove it with 23.7 before the plant runs. Reserve REAL_TO_WORD for the case where the master genuinely wants a 16-bit integer.

From here, what shows the number is a separate choice: specifying a read-only process display for a plant covers that side, and shop-floor screens covers where such a board mounts and how large its text must be at a given viewing distance.

Questions

Is there a built-in CODESYS function that packs a REAL into two WORDs?

No single built-in does the packing, but MEMCPY from the standard library does it in one call, and open-source IEC 61131-3 libraries add wrapper functions built on the same copy. A function whose name starts REAL_TO_ is an arithmetic conversion: it changes the value, so it never belongs in the register path.

Which word order should the master decode?

Start with the high word in the lower register address, the layout most tools call big-endian. Confirm it with a fractional test value such as 23.7: the raw pair must read 16,829 then 39,322. A decode near 10 to the minus 23 means the words are swapped; change the order in the master's decoder, in that one place, and re-read.

How many registers does an LREAL need?

Four. An LREAL is 8 bytes under IEC 61131-3, so it fills an ARRAY[0..3] OF WORD with MEMCPY(ADR(awVal[0]), ADR(lrVal), 8). Reserve four consecutive registers and repeat the word-order test across all four words before trusting the decoded value.

My slave mapping tool accepts WORDs only. Can I map the REAL directly?

Yes, by declaring the REAL and a WORD or BYTE array at the same address, for example both at %MW0. That removes the copy but pins the layout to one fixed address. Prefer MEMCPY into a normal array when the tool accepts it, and use the overlap only where the mapping cannot be fed an array.

Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.

Keep reading

Build your signAsk for a quote