Send a REAL value in a 12-byte TCP telegram

7 min read · Published November 4, 2026

A 12-byte telegram needs a 12-byte container. An LWORD stops at 8 bytes, so build the telegram as a byte array, place the header and address by index, and copy the value with MEMCPY. Assigned as an integer, 70 goes out as 46 00 00 00 and a float decoder reads almost zero; copied as a REAL, 70.0 goes out as 00 00 8C 42 and decodes as 70.0.

Copy the REAL's four bytes straight into the telegram with MEMCPY, and 70.0 goes on the wire as 00 00 8C 42, the little-endian pattern a float decoder reads back as 70.0. A plain assignment of 70 puts 46 00 00 00 in those bytes instead: the bit pattern of the integer 70, which reads as almost zero at any float decoder. An LWORD cannot carry the telegram either way, because it holds only half of it. This page gives the container choice, the declarations, the copy, the byte math behind 00 00 8C 42, and the checks that confirm the telegram before the first send.

An LWORD stops at half the telegram, so it needs a byte container

LWORD is a 64-bit bitstring: 8 bytes, 16 hex digits. That is the largest elementary type in IEC 61131-3, shared with LREAL, so no scalar reaches 12. Hex is a notation, not a storage type: 16#AF in a BYTE stores one byte, 16#428C0000 in a DWORD stores four. Carry the telegram in something sized in bytes.

Container Size Verdict for this telegram
LWORD 8 bytes Covers the header and half of the address. Too small.
ARRAY [0..11] OF BYTE 12 bytes Offsets 0 to 11 map one to one onto header, address, value.
Packed STRUCT with three 4-byte fields 12 bytes packed Named fields in one declaration. Keep {attribute 'pack_mode' := '1'} on the type and confirm SIZEOF reports 12.
STRING 1 byte per character plus a terminator Wrong container: a zero byte is legal payload and a terminator would cut it short.

The array is the direct route, and the rest of the page uses it. The packed struct is worth moving to once the format is stable: {attribute 'pack_mode' := '1'} forces 1-byte alignment, so a DINT, a UDINT and a second DINT pack to exactly 12 bytes with no padding.

What you need

  • A byte buffer sized to the telegram: aTelegram : ARRAY [0..11] OF BYTE. A larger buffer also works; with ARRAY [0 .. gvlSetting.gc_wMaxTelegram] OF BYTE the bound is inclusive, so capacity is gvlSetting.gc_wMaxTelegram + 1 bytes. Buffer capacity and write length are two different numbers, and the write takes the telegram length, 12.
  • The value as a REAL: rValue : REAL := 70.0. A REAL is 4 bytes under IEC 61131-3; an LREAL fills twice that.
  • The TCP write block and its buffer pointer input, NBS.TCP_Write.pData, aimed at ADR(aTelegram) or ADR(aTelegram[0]).
  • The receiver's field definition, confirming the value field is a 4-byte float. The rest of the page assumes that, which is what a REAL holds.

The names aTelegram and rValue are this page's own. MEMCPY, ADR, SIZEOF and NBS.TCP_Write.pData are the library's names.

Build the telegram and send the REAL

  1. Declare the buffer and the value.

    PROGRAM SendTelegram
    VAR
        aTelegram : ARRAY [0..11] OF BYTE; // header 0..3, address 4..7, value 8..11
        rValue    : REAL := 70.0;          // travels as 4 IEEE 754 bytes
    END_VAR
    
  2. Fill header and address by index, in the transmission order the receiver expects:

    aTelegram[0] := 128;  // 80
    aTelegram[1] := 9;    // 09
    aTelegram[2] := 0;    // 00
    aTelegram[3] := 0;    // 00   header  = 80 09 00 00, or 2432 read little-endian
    aTelegram[4] := 140;  // 8C
    aTelegram[5] := 16;   // 10
    aTelegram[6] := 0;    // 00
    aTelegram[7] := 0;    // 00   address = 8C 10 00 00, or 4236 read little-endian
    
  3. Copy the REAL's bytes at the value offset:

    rValue := 70.0;
    MEMCPY(ADR(aTelegram[8]), ADR(rValue), 4);
    

    SIZEOF(rValue) is 4, so MEMCPY(ADR(aTelegram[8]), ADR(rValue), SIZEOF(rValue)) is the same call. ADR(aTelegram[8]) points at offset 8, so the four bytes land at offsets 8, 9, 10 and 11. Keep numeric conversions off this path: REAL_TO_DINT rounds 70.0 to 70 and writes the integer's bytes, the exact fault this page repairs. Where a REAL must pass through an integer type elsewhere, guard it as REAL_TO_DWORD FPU overflow: fix Modbus data and guard it shows.

  4. Hand the write block the buffer address and the length: NBS.TCP_Write.pData takes ADR(aTelegram) or ADR(aTelegram[0]), and the block's byte-count input gets 12, the telegram length.

  5. Send, then read the value back at the server.

One safety point belongs next to MEMCPY: the function copies the count you give it and validates neither size nor pointer, and its own documentation warns that a wrong size or a bad pointer can crash the runtime or read memory the caller does not own. Size every count from SIZEOF or the fixed 4, and never copy an LREAL's wider pattern into a field that holds 4.

Worked example: 70.0 in bytes 8 to 11 of a 12-byte telegram

The inputs, and where each comes from: the telegram is 12 bytes with the header at bytes 0 to 3, the address at 4 to 7 and the value at 8 to 11, little-endian, the order this telegram's fields use. The header bytes are 128, 9, 0, 0 and the address bytes 140, 16, 0, 0, from the telegram built above. The value is 70.0 as a REAL, 4 bytes under IEC 61131-3. IEEE 754 single precision gives every REAL its layout: 1 sign bit, 8 exponent bits biased by +127, 23 mantissa bits.

The rule in words: a single-precision float stores the sign, then the true exponent plus 127, then the 23 mantissa bits of the normalized value 1.mantissa × 2^exponent, and MEMCPY transmits those 4 bytes in the order the controller stores them.

The arithmetic: 70 = 64 + 4 + 2 = 1000110 in binary, which normalizes to 1.000110 × 2⁶. Exponent field: 127 + 6 = 133 = 10000101 in binary. Mantissa field, 23 bits: 000110 followed by 17 zeros, so 00011000000000000000000. Assemble all 32 bits, sign, exponent, mantissa: 0 10000101 00011000000000000000000, grouped in fours: 0100 0010 1000 1100 0000 0000 0000 0000, which reads as 16#428C0000. Lowest byte first, as little-endian transmission puts them: 00 00 8C 42.

The result: bytes 8 to 11 carry 00 00 8C 42, and the full telegram reads 80 09 00 00 8C 10 00 00 00 00 8C 42. Its integer fields decode as 128 + 9 × 256 = 2432 for the header and 140 + 16 × 256 = 4236 for the address, matching the telegram as declared.

The decision: if the receiver defines the value field as a 4-byte float, this telegram is finished. If the receiver specifies an 8-byte double, the value cannot fit the 12-byte layout: 4 + 4 + 8 = 16 bytes, so rebuild the buffer, the field offsets and the write length to 16 against the receiver's documented layout, and only then copy the LREAL.

Check the telegram, then the wire, then the server

  1. In the buffer. After the copy and before the write, the runtime's online view must show aTelegram[8] to aTelegram[11] as 16#00, 16#00, 16#8C, 16#42. This one check separates the two faults that look identical at the server: 46 00 00 00 there means the copy is missing and the integer is still on the wire; the right pattern there with a wrong value at the server points downstream, to byte order or framing.
  2. On the wire. The write must move 12 bytes per telegram. More than 12 means the length came from the buffer capacity, and the surplus bytes shift every field the receiver reads after the first telegram.
  3. At the receiver. The decoded value must read 70.0. If it reads near zero while the buffer holds 00 00 8C 42, compare byte order: a big-endian receiver wants 42 8C 00 00 in bytes 8 to 11, so place those four bytes individually in reverse and skip the copy for this field. A capture of the received bytes settles which order arrived.

No bytes arriving at all is a different fault class: check link state, the endpoint and the write block's status output before touching the payload. A link that runs normally and then stalls hours later has its own causes, covered in why a SysLibSockets TCP client stops after hours.

Framing on the receive side

TCP is an ordered byte stream with no message boundaries. One write does not produce one read: the server's first read may return 5 bytes and its second 7. The receiver must accumulate until it holds 12, decode, and carry the remainder into the next telegram. If you rebuild frames from partial reads on the PLC side too, the same accumulation applies; accumulating multiline serial reads into variables in CODESYS works through it for serial lines, and the pattern carries over unchanged.

When the same REAL has to cross a Modbus link instead of raw TCP, its four bytes travel as two 16-bit registers, and word order decides which half lands first; converting a CODESYS REAL into two Modbus WORD registers handles that case.

Which container and layout to use

Recommendation: keep the telegram in ARRAY [0..11] OF BYTE, place the header and address by index, and copy the value with MEMCPY(ADR(aTelegram[8]), ADR(rValue), SIZEOF(rValue)), writing 12 bytes per send. Move to a packed STRUCT with {attribute 'pack_mode' := '1'} once the format is stable, and only after SIZEOF on the struct reports 12; the value field can stay a DINT, because MEMCPY writes the REAL's bit pattern into its 4 bytes all the same. If the receiver specifies a 64-bit double, grow the telegram to 16 bytes first, 4 + 4 + 8 with the value at offsets 8 to 15, and then copy the LREAL into it.

Questions

Can I send an LREAL in the same 12-byte telegram?

No. LREAL is the 64-bit double type. With a 4-byte header and a 4-byte address the payload becomes 4 + 4 + 8 = 16 bytes: grow the buffer to 16, write 16, and confirm the receiver specifies that layout before sending. Copying an LREAL into offset 8 of a 12-byte array would write 4 bytes past the end of the array.

What does the server see if bytes 8 to 11 still hold 46 00 00 00?

Those four bytes are the 32-bit integer 70 in little-endian order, 16#00000046. Read as an IEEE 754 float they carry a zero exponent field, so the decoder returns the denormal 70 × 2⁻¹⁴⁹, about 9.8 × 10⁻⁴⁴, effectively zero. Length and header still look right, which is why the link seems healthy while the value is wrong.

Does MEMCPY convert the value or fix the byte order?

Neither. MEMCPY copies exactly the number of bytes you give it, in the order the runtime stores them. If the receiver decodes big-endian, place the value bytes reversed yourself: byte 8 as 42, byte 9 as 8C, bytes 10 and 11 as 00. One converter check settles the order before the first send.

Why not build the telegram as a STRING or one hex literal?

A zero byte is legal data in a binary telegram, so string functions that stop at a terminator return the wrong length, and one hex literal such as an LWORD holds only its own width. Take the write length from the telegram itself, 12 bytes here, and keep the payload in BYTE cells with explicit offsets.

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