Transferring LREAL Values via Pointer Addressing in TIA Portal

David Krause16 min read
S7-1200SiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Overview: Why LREAL Cannot Move Through a Plain Address Register

In TIA Portal projects on S7-1200 (firmware V4.0 and higher) and S7-1500 (firmware V1.0 and higher), programmers frequently need to copy a 64-bit LREAL value from one data-block location to another where the destination offset is computed at runtime. The intuitive approach is to load the source address into one of the CPU's address registers (AR1 or AR2) and use the indirect addressing forms [AR1,P#0.0], [AR2,P#0.0], or the P# pointer syntax to drive a MOVE_BLK or MOVE instruction.

The complication is that AR1 and AR2 in the S7-1200/S7-1500 instruction set are still implemented as 32-bit-wide registers. Even when the symbolic operand on a MOVE block is of type LREAL (8 bytes / 64 bits), the address-register based forms restrict the operand width to BOOL, BYTE, WORD, DWORD, CHAR, INT, DINT, REAL, TIME, S5TIME, or DATE_AND_TIME. LREAL is explicitly excluded from the list of operand types accepted by AR1/AR2 in the TIA Portal information system. The same restriction applies to LWORD and LINT.

This article documents four practical workarounds that preserve the runtime-computed destination offset: byte-level PEEK/POKE, the AT overlay on a non-optimized DB, a generic SCL function block built on DWORD pointer arithmetic, and VARIANT-based generic copying with MOVE_BLK_VARIANT.

LREAL Memory Layout and the Boundary-Alignment Constraint

An LREAL variable conforms to IEEE 754 binary64 (double precision): 1 sign bit, 11 exponent bits, 52 mantissa bits, total of 64 bits = 8 bytes. In an S7-1200/S7-1500 DB, the bytes are stored in little-endian order with the least significant byte at the lowest offset. A non-optimized DB that contains LREAL at offset 0 therefore occupies offsets 0..7; the symbol that the editor exposes will already respect byte alignment because the compiler emits a 4-byte slot for REAL and an 8-byte slot for LREAL.

The byte-allocation table for the structures used in the examples below:

Byte offset LREAL byte (little-endian) REAL byte (little-endian)
+0 bits 0..7 (LSB of mantissa) bits 0..7 (LSB of mantissa)
+1 bits 8..15 bits 8..15
+2 bits 16..23 bits 16..23
+3 bits 24..31 bits 24..31 (MSB)
+4 bits 32..39 not used
+5 bits 40..47 not used
+6 bits 48..55 not used
+7 bits 56..63 (sign + exponent) not used
Field note: The original problem description occasionally treats the value as "4 bytes". That description matches REAL, not LREAL. The byte-level procedures below are written for the full 8-byte LREAL; if the actual variable is REAL, the loops and array sizes simply halve.

When the runtime-computed offset is itself dynamic (for example, "destination = base + 4 × n" for an array of REAL), the compiler cannot prove alignment, so the offset value must always be a multiple of 8 for LREAL and a multiple of 4 for REAL. Writing across an unaligned boundary does not raise a hard fault on S7-1200/S7-1500, but the generated STL emits two byte moves instead of one word/dword move, which costs roughly twice the execution time and can mask access-protection faults.

Why AR1/AR2 Cannot Carry LREAL Directly

The legacy address-register model in the STEP 7 instruction set was defined when S7-300 and S7-400 CPUs exposed 32-bit-wide data operands only. AR1 and AR2 store an area-internal offset (or, in the cross-area form, an area pointer). The instruction decoder pairs AR with the operand width to compute the effective address.

The TIA Portal help text for indirect addressing on S7-1200/S7-1500 lists the supported operand widths explicitly: BOOL, BYTE, WORD, DWORD, SINT, USINT, INT, UINT, DINT, UDINT, REAL, CHAR, S5TIME, TIME, DATE_AND_TIME. LREAL, LWORD, LINT, ULINT are absent. See the TIA Portal information system for the S7-1200 entry "Addressing of operands with the address register" for the canonical list.

Because the indirect forms do not support LREAL, three engineering options remain: drop down to the byte level, build a typed overlay that hides the byte width, or pass a VARIANT plus length to a generic block. Each is covered in the sections that follow.

Solution 1: PEEK/POKE Byte-Level Transfer (Non-Optimized DB)

The PEEK and POKE instructions were added in TIA Portal V13 to give S7-1200/S7-1500 a portable way to read/write arbitrary byte offsets in a data block. The byte-level read/write works on any DB, regardless of its declared type, as long as the DB is configured for standard access (i.e., non-optimized). The variants of interest are:

  • PEEK(area := 16#84, dbNumber := n, byteOffset := m) - returns a BYTE from DB n at offset m.
  • PEEK_WORD(...) / PEEK_DWORD(...) - returns WORD / DWORD.
  • POKE(area := 16#84, dbNumber := n, byteOffset := m, value := b) - writes a BYTE.
  • POKE_WORD(...) / POKE_DWORD(...) - writes WORD / DWORD.

Use the byte-level form to assemble the LREAL one byte at a time, then copy the eight bytes into a typed LREAL tag using the %B0..%B7 slice operators in SCL:

// SCL: copy LREAL from DB_SRC at fixed offset 0 to DB_DST at runtime offset destOff
// destOff is a DINT and must be a multiple of 8 for LREAL.
IF (destOff MOD 8) = 0 THEN
    FOR i := 0 TO 7 DO
        "DB_Dst".dest.%B[i] := PEEK(area := 16#84,
                                    dbNumber := "DB_Src_Number",
                                    byteOffset := 0 + i);
    END_FOR;
END_IF;

The same technique works the other way for POKE writes. Because the loop is unrolled by the SCL compiler on S7-1500, the run-time cost is eight PEEK calls plus eight byte copies, roughly 1-2 microseconds per byte on an S7-1516.

If both DBs are non-optimized, you can collapse the loop with PEEK_DWORD twice, which yields a small speedup. The slice syntax in the assignment keeps the destination byte order correct:

// Faster: two DWORD reads, then reassemble in the typed view
"DstWords".dw0 := PEEK_DWORD(area := 16#84, dbNumber := srcDB, byteOffset := srcOff);
"DstWords".dw1 := PEEK_DWORD(area := 16#84, dbNumber := srcDB, byteOffset := srcOff + 4);
"DB_Dst".dest.%D0 := "DstWords".dw0;
"DB_Dst".dest.%D1 := "DstWords".dw1;

Solution 2: AT Overlay on a Non-Optimized Data Block

The AT construct overlays a variable with a different type view at the same memory location. It is supported on S7-1200 (firmware V4.2+) and on all S7-1500 CPUs. AT can convert a 64-bit LREAL into an ARRAY[0..7] OF BYTE for byte-level access, which is exactly what the indirect copy needs.

Define a non-optimized DB whose only element is the typed LREAL plus an AT overlay:

DATA_BLOCK "DB_LREAL_Scratch"
{ S7_Optimized_Access := 'FALSE' }
STRUCT
    value  : LREAL;          // the symbolic view
    bytes  AT value : ARRAY[0..7] OF BYTE;   // the byte view
END_STRUCT;
END_DATA_BLOCK

The same overlay can be declared locally inside an SCL function block:

FUNCTION_BLOCK "fb_LREAL_Copy"
VAR
    srcView : STRUCT
        val   : LREAL;
        bytes AT val : ARRAY[0..7] OF BYTE;
    END_STRUCT;
    dstView : STRUCT
        val   : LREAL;
        bytes AT val : ARRAY[0..7] OF BYTE;
    END_STRUCT;
END_VAR
BEGIN
    // ... use srcView.bytes[] and dstView.bytes[] for indirect access ...
END_FUNCTION_BLOCK

With the overlay in place, any DINT offset that is a multiple of 8 can drive a block move through the byte array, and the resulting bytes reconstruct the LREAL automatically when the typed view is read elsewhere in the program.

Solution 3: Generic SCL Function Block with Pointer Arithmetic

For projects that have many LREAL fields indexed by computed offsets, a single reusable block keeps the application code clean. The block accepts a source DB number, a destination DB number, a source byte offset, and a destination byte offset, then performs the copy.

FUNCTION_BLOCK "fb_LREAL_IndirectCopy"
VAR_INPUT
    i_srcDB       : WORD;     // DB number of source (non-optimized)
    i_srcOffset   : DINT;     // byte offset within source DB, multiple of 8
    i_dstDB       : WORD;     // DB number of destination (non-optimized)
    i_dstOffset   : DINT;     // byte offset within destination DB, multiple of 8
    i_enable      : BOOL;
END_VAR
VAR_OUTPUT
    o_ok          : BOOL;
    o_error       : DWORD;    // 0 = no error
END_VAR
VAR
    srtBuf : ARRAY[0..7] OF BYTE;
END_VAR
BEGIN
    o_ok := FALSE;
    o_error := 0;

    IF NOT i_enable THEN
        RETURN;
    END_IF;

    IF (i_srcOffset MOD 8) <> 0 OR (i_dstOffset MOD 8) <> 0 THEN
        o_error := 16#8001;   // alignment error
        RETURN;
    END_IF;

    // Read 8 bytes from source
    srtBuf[0] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 0);
    srtBuf[1] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 1);
    srtBuf[2] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 2);
    srtBuf[3] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 3);
    srtBuf[4] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 4);
    srtBuf[5] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 5);
    srtBuf[6] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 6);
    srtBuf[7] := PEEK(area := 16#84, dbNumber := WORD_TO_INT(i_srcDB), byteOffset := i_srcOffset + 7);

    // Write 8 bytes to destination
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 0, value := srtBuf[0]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 1, value := srtBuf[1]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 2, value := srtBuf[2]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 3, value := srtBuf[3]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 4, value := srtBuf[4]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 5, value := srtBuf[5]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 6, value := srtBuf[6]);
    POKE(area := 16#84, dbNumber := WORD_TO_INT(i_dstDB), byteOffset := i_dstOffset + 7, value := srtBuf[7]);

    o_ok := TRUE;
END_FUNCTION_BLOCK

Two operational details worth highlighting:

  1. The area constant 16#84 selects "DB" in the PEEK/POKE cross-area code. Valid values are 16#81 (inputs), 16#82 (outputs), 16#83 (bit memory), 16#84 (DB), 16#85 (instance DB), 16#86 (local).
  2. PEEK/POKE always operate on bytes. PEEK_WORD/PEEK_DWORD are convenience wrappers and do not change the access granularity. Choose the variant that matches the destination slice operator (%B, %W, %D) to avoid a manual byte swap.

Solution 4: VARIANT-Based Transfer with MOVE_BLK_VARIANT

When both the source and the destination exist as typed tags and the runtime offset is the only dynamic element, the modern TIA Portal approach is to use MOVE_BLK_VARIANT with a VARIANT input. This instruction accepts a source VARIANT, a destination VARIANT, and a count, and it will copy the underlying byte range regardless of the data type. S7-1500 (firmware V1.8+) and S7-1200 (firmware V4.2+) both support it.

// Build a VARIANT that points at the LREAL slot whose offset is dynamic.
// "DB_Dst" is non-optimized, "slotBase" is the address of element [0] of the array.
VAR_TEMP
    p_any  : VARIANT;
END_VAR
BEGIN
    p_any := "DB_Dst".slot[idx];   // idx is the runtime index, LREAL element
    MOVE_BLK_VARIANT(SRC := p_any, DST := "DB_Dst".targetSlot, COUNT := 1);
END

Two caveats apply to MOVE_BLK_VARIANT:

  • The destination VARIANT is written in place; it cannot itself be an indexed expression if "DB_Dst" is optimized. Use a non-optimized DB or a temporary slice tag.
  • If the destination and source are different data types (for example, byte array on one side, LREAL on the other), the instruction will refuse to execute and return an error in RET_VAL. Use the same declared type on both ends.

Configuring Standard (Non-Optimized) DB Access

All four solutions depend on the source and destination DBs having standard access enabled. The setting lives in the DB properties under "Attributes > Optimized block access". For each DB involved:

  1. Right-click the DB in the project tree and choose "Properties".
  2. Open the "Attributes" tab.
  3. Uncheck "Optimized block access" (or set S7_Optimized_Access := 'FALSE' in the source view).
  4. Confirm by compiling - non-optimized DBs expose every element as an absolute byte offset in the "Information" tab.
Caution: Switching an existing DB from optimized to non-optimized access re-numbers all offsets and invalidates any HMI tag bindings, S7 connections, or OPC UA mappings that referenced the optimized symbolic name with an offset assumption. Always perform the switch during a planned re-commissioning window and re-export the HMI tag list.

Step-by-Step Commissioning Procedure

  1. Identify the LREAL fields. Open the project DB list and confirm the type of the source and destination tags. If the type is actually REAL, halve all array sizes and offsets in the steps that follow.
  2. Confirm or change DB access mode. Both DBs must be non-optimized. Adjust attributes, recompile, and document the change in the project's DB reference spreadsheet.
  3. Declare the byte overlay. Add an AT view in either the DB or in the local variables of the FB that drives the copy.
  4. Implement the copy. Use PEEK/POKE for one-off transfers, the generic FB for repeated transfers, or MOVE_BLK_VARIANT when both ends are typed.
  5. Add alignment check. Reject any computed offset that is not a multiple of 8 (LREAL) or 4 (REAL) and emit a diagnostic.
  6. Force the inputs in online mode. Set the source LREAL to a recognisable pattern such as 1.0, -2.5e10, and the smallest positive normal value 2.2250738585072014e-308. Capture the destination bytes after the copy and verify they match the source bytes.
  7. Trace under runtime load. Insert a trace on the trigger condition, the computed offset, and the resulting value. Confirm that the offset is stable across scans and that the destination value converges to the source within one scan.

Verification: Online Watch Tables and PLC Traces

The fastest verification is a watch table that displays the typed view and the byte overlay side by side. Add the following columns:

  • Source LREAL in decimal scientific notation (%f or %g).
  • Source bytes[0..7] in hex.
  • Destination LREAL in decimal scientific notation.
  • Destination bytes[0..7] in hex.
  • Offset register in hex.

For a high-confidence check, write the IEEE-754 special values:

Test value Hex bytes (little-endian) Purpose
+0.0 00 00 00 00 00 00 00 00 Verify zero handling, sign bit 0
-0.0 00 00 00 00 00 00 00 80 Verify sign bit propagation
+1.0 00 00 00 00 00 00 F0 3F Exponent = 1023, mantissa = 0
-1.0 00 00 00 00 00 00 F0 BF Sign bit set
+Inf 00 00 00 00 00 00 F0 7F All-ones exponent
NaN 01 00 00 00 00 00 F0 7F Quiet NaN, mantissa LSB set
DBL_MIN 00 00 00 00 00 00 10 00 Smallest positive normal
DBL_MAX FF FF FF FF FF FF EF 7F Largest finite value

If any byte in the destination differs from the source, the most common causes are byte-order assumption errors (big-endian source), optimization that you forgot to disable on one of the DBs, or a partial copy that left the high half of the LREAL at its previous value.

Edge Cases: REAL vs LREAL, Endianness, Cross-DB Copy

REAL vs LREAL. On the S7-1200, the REAL type is 32-bit IEEE-754 single precision (4 bytes). All of the byte-level procedures in this article apply to REAL by reducing the loop bound from 8 to 4 and the alignment check from 8 to 4.

Endianness. The S7-1500 stores multi-byte values in little-endian byte order. If the LREAL originated on a big-endian device (some Modbus or PROFINET third-party instruments), you must reverse the byte sequence. The standard trick is to swap %B0 with %B7, %B1 with %B6, and so on, before assigning the typed view.

Cross-DB copy. When the source DB is optimized and the destination is non-optimized (or vice versa), PEEK/POKE still works on the non-optimized side, but the optimized DB has no absolute byte offsets you can reference. The recommended pattern is to mirror the LREAL once into a non-optimized shadow DB using the compiler-generated symbolic path, then do all indirect access on the shadow.

Library of Functions. Avoid writing the byte-swap logic in every project. Siemens ships a function block SWAP_DWORD in the "Standard library > IEC function blocks" that swaps the four bytes within a DWORD; for LREAL you swap both %D0 and %D1 and then mirror the two halves.

Cross-Platform Notes: Beckhoff TwinCAT and CODESYS Equivalents

The underlying problem is platform-agnostic. Beckhoff TwinCAT 3 supports the explicit conversion BYTE_TO_LREAL_EX, which converts a byte stream to a positive LREAL; see the Beckhoff Information System entry byte_to_lrealex - Beckhoff Information System. For full bidirectional support Beckhoff documents the type conversion operators in the Type conversion operators reference; note that LREAL to UINT can raise an exception error when the value is negative.

Other IEC 61131-3 runtimes such as CODESYS achieve the same result by overlaying a REAL (or LREAL) and a byte array at the same marker address, then performing the copy through the byte array. The pattern is identical to the S7-1500 AT overlay covered in Solution 2: both depend on the compiler honouring the alignment of the underlying memory region and on the runtime exposing an absolute byte view.

For Siemens S7-1200, the canonical reference for the type-conversion rules that govern what is and is not implicitly allowed between LREAL and the byte/integer families is the TIA Portal help entry Implicit conversion of LREAL (S7-1200). Implicit conversion from LREAL is permitted only to REAL, DINT, INT, SINT, BOOL, STRING, and the character types; it is not permitted to BYTE, WORD, DWORD, or any 64-bit integer family without an explicit conversion function. This is why the only viable paths on S7-1200/S7-1500 are the byte-level or VARIANT-based routes.

Troubleshooting Matrix

Symptom Likely cause Fix
Compiler rejects MOVE with operand type LREAL when used with [AR1,P#0.0] AR1/AR2 do not accept LREAL operand width Use byte-level PEEK/POKE, AT overlay, or MOVE_BLK_VARIANT
Online value of destination is always 0.0 Source DB is optimized, no absolute offset available to PEEK Disable optimized access on the source DB or copy the value once into a non-optimized shadow DB
Bytes are mirrored (LSB/MSB swapped) Big-endian source, little-endian destination or vice versa Reverse byte order with SWAP_DWORD applied to both %D0 and %D1
Destination value matches only for half the source range Loop was written for REAL (4 bytes) but type is LREAL (8 bytes) Extend loop bound to 7 and array size to 8
Compiler error "Area-crossing not permitted for PEEK on optimized DB" Target DB is optimized Switch target DB to standard access or copy to a non-optimized DB first
Watch table shows NaN after copy Partial write left stale exponent bits Always write all 8 bytes, even if the source only changes one
Trace shows correct value one scan, then reverts Subsequent logic overwrites the destination because the symbolic and indirect views share storage Place the copy in the last network before the read, or guard the destination write with an interlock

Why does TIA Portal reject LREAL with [AR1,P#0.0]?

The S7-1200/S7-1500 address registers AR1 and AR2 are 32-bit wide. The instruction decoder supports indirect addressing only for operand widths up to DWORD / DINT / REAL. LREAL, LWORD, and LINT are not in the supported list, so the compiler refuses to bind the symbolic operand.

Does the solution require non-optimized data blocks?

Yes. PEEK, POKE, PEEK_WORD, and PEEK_DWORD operate on byte offsets that exist only when the DB is configured with standard (non-optimized) access. The AT overlay also requires the underlying tag to occupy a deterministic byte layout, which optimized access does not guarantee. MOVE_BLK_VARIANT can work on optimized DBs but only when both ends expose a typed symbolic view.

What is the alignment requirement for an LREAL pointer offset?

On S7-1500, an LREAL must be placed on an 8-byte boundary. Computed offsets should be checked with (offset MOD 8) = 0 before the copy. For REAL the boundary is 4 bytes. Misaligned offsets do not fault but generate slower code and can clash with adjacent variables in non-optimized DBs.

Can I use PEEK_DWORD to halve the loop?

Yes. Two PEEK_DWORD reads followed by a %D0/%D1 slice assignment reduces eight PEEK calls to two, which roughly halves the execution time on S7-1500. Make sure the destination LREAL has both %D0 and %D1 assigned; partial writes leave the high half at its previous value and can produce NaN if the exponent field changes.

Does this technique work on S7-300 / S7-400 with STEP 7 Classic?

The principle is the same but the PEEK/POKE family was not part of the original S7-300/400 instruction set. Use the legacy BLKMOV with area pointers, or ANY pointer arithmetic in STL. The byte-by-byte pattern with PIB/PQB/DBB also applies, and the alignment check is identical.

Back to blog