Calculate Variable and UDT Structure Size in TIA Portal

David Krause12 min read
SiemensTIA PortalTutorial / 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

Overview

Determining the byte footprint of a PLC tag, UDT, ARRAY, or STRUCT is a recurring requirement on S7-1200 and S7-1500 controllers. The legacy STEP 7 V5.x ANY-pointer dissection technique that returned 100% reliable sizes for DBs, FBs, and FCs is no longer applicable inside TIA Portal when the target block has the Optimized block access attribute enabled. With optimized memory, the compiler reorders elements and inserts padding to align 16-bit, 32-bit, and 64-bit members on natural boundaries, so the ANY pointer is masked to a generic handle and the byte/word count can no longer be derived from the pointer structure itself.

Three engineering-grade alternatives solve the problem in TIA Portal V15.1 through V18:

  1. SERIALIZE / DESERIALIZE on an instance of the same data type — returns the number of bytes the runtime will move.
  2. VARIANT + EQ_Type — generic functions that work on any data type passed by reference, useful for TUSEND/TURCV, custom protocols, and Modbus payload mapping.
  3. Symbolic / UDT approach — preferred for new code; sizes are always known at compile time and no runtime calculation is required.
Important: Symbolic-only programming on S7-1500 is the recommended path for new development. The methods below cover the cases where a generic or dynamic length is unavoidable (UDP/TCP send, Get/Put, OPC UA write, etc.).

Why the ANY Pointer Trick No Longer Works

On S7-300/400, a function block can dissect the ANY pointer it receives and read byte 6/7 to obtain the length. On S7-1200/1500, blocks marked as Optimized hide the underlying P#DBnx.DBBy BYTE n representation behind a handle. The compiler resolves access in the load unit and the runtime refuses to disclose absolute byte counts when the symbol is not pinned to a fixed location. The TIA Portal V17 S7-1500 Function Manual documents this behavior in the section on optimized block access.

Two workarounds were historically attempted:

  • Disabling Optimized block access on the instance DB. This defeats the purpose of using S7-1500 and breaks symbolic-only programming on every consumer of the block.
  • Forcing an AT view on top of a VARIANT. The AT view inherits the variant's runtime type but does not return its size.

The SERIALIZE route is the official Siemens mechanism for getting the byte count of a structured type whose layout is determined by the compiler.

Prerequisites

  • TIA Portal V16 or later (V15.1 supports SERIALIZE/DESERIALIZE but with a smaller type set; V17+ is recommended).
  • S7-1200 CPU firmware V4.4 or later, or any S7-1500 CPU.
  • S7-1500 CPUs must run firmware V2.0 or later for full VARIANT and SERIALIZE support.
  • An instance DB with Optimized block access enabled.
  • Familiarity with SCL (Structured Control Language) and the TIA Portal instruction browser under Basic Instructions > Moving operations.

Method 1 — SERIALIZE Instruction (Primary Method)

The SERIALIZE instruction serializes the source data into a sequential byte stream starting at the requested offset. When you start at offset 0 and let the function fill a destination ARRAY OF BYTE sized to the maximum possible payload, the return parameter RET_VAL reports the actual number of bytes that the runtime copied — that value is the byte size of the structured source.

Step 1 — Create the source data block

Create a global DB (or instance DB) holding the structured variable whose size you want to determine. Mark the block as Optimized block access. Inside the DB define a tag of the UDT or STRUCT you need to measure, for example:

DATA_BLOCK "dbRecipe"
  STRUCT
    stHead : "udtHeader";     // 32 bytes by definition
    arrVal : ARRAY[1..64] OF REAL; // 256 bytes
    stTail : "udtTrailer";    // 12 bytes
  END_STRUCT;
END_DATA_BLOCK

Step 2 — Create the size-calculation FB

Insert a new function block, name it FB_GetSizeOfStruct, language SCL, optimized access on. Add the following interface:

Section Name Type Comment
Input i_Start BOOL One-shot trigger
InOut io_Source VARIANT Any structured tag
Output o_SizeBytes DINT Byte size of io_Source
Output o_Error BOOL TRUE on serialization failure
Output o_Status WORD RET_VAL of SERIALIZE
Static s_Serializer "udtSerializer" Static instance of the same UDT/STRUCT (optional)
Temp t_Bytes ARRAY[0..8191] OF BYTE Scratch buffer for SERIALIZE destination
Sizing the scratch buffer: 8192 bytes covers the practical maximum for a single S7-1500 structured tag. Increase to 65534 for edge cases, but be aware that large static temp arrays count against the work-memory limit of the CPU.

Step 3 — SCL body

IF i_Start THEN
  o_Error := FALSE;
  o_Status := 0;
  o_SizeBytes := 0;
  // Serialize the entire source from offset 0
  o_Status := SERIALIZE(SRC := io_Source,
                        DEST := t_Bytes,
                        OFFSET := 0,
                        LEN  := -1);   // -1 = serialize all remaining bytes
  IF o_Status = 0 THEN
    // DESERIALIZE round-trip on the same data type to discover length
    o_Status := DESERIALIZE(SRC := t_Bytes,
                            DEST := s_Serializer,
                            OFFSET := 0,
                            LEN := -1);
    IF o_Status = 0 THEN
      // SizeByType variant: use sizeof on the destination
      o_SizeBytes := TO_DINT(SIZEOF(s_Serializer));
    ELSE
      o_Error := TRUE;
    END_IF;
  ELSE
    o_Error := TRUE;
  END_IF;
END_IF;

Step 4 — Why the round trip is needed

The SERIALIZE function returns the number of bytes that were moved only when the destination buffer is exactly the right size. The pattern shown above is the most portable: serialize into a generous scratch buffer, then deserialize back into a typed static instance of the same UDT, and use the SIZEOF operator on the typed instance. SIZEOF evaluates at compile time for fixed UDTs and returns a DINT in bytes, including any compiler-inserted alignment padding. The S7-1500 programming and operating manual confirms that SIZEOF on a static UDT instance is supported for all elementary and derived data types.

Method 2 — Variant + EQ_Type for Generic Functions

When the data type is not known at compile time, the VARIANT input combined with EQ_Type lets the engineer branch on the runtime type. The pattern is used inside the Siemens Open Library and is also visible in the TIA Portal help on the Variant data type.

FUNCTION "fcSizeOfVariant" : DINT
VAR_INPUT
  i_Data : VARIANT;
END_VAR
VAR_TEMP
  t_Size : DINT;
END_VAR
BEGIN
  t_Size := 0;
  IF IS_ARRAY(i_Data) THEN
    t_Size := CountOfElements(i_Data) * TypeSizeOfElement(i_Data);
  ELSIF EQ_Type(i_Data, Type_TOFF) THEN
    t_Size := 16;  // TOD, TIME, LTIME
  ELSIF EQ_Type(i_Data, Type_BOOL) THEN
    t_Size := 1;
  ELSIF EQ_Type(i_Data, Type_BYTE) OR EQ_Type(i_Data, Type_CHAR) THEN
    t_Size := 1;
  ELSIF EQ_Type(i_Data, Type_WORD) OR EQ_Type(i_Data, Type_S5TIME) OR EQ_Type(i_Data, Type_DATE) THEN
    t_Size := 2;
  ELSIF EQ_Type(i_Data, Type_DWORD) OR EQ_Type(i_Data, Type_TIME) OR EQ_Type(i_Data, Type_DINT) THEN
    t_Size := 4;
  ELSIF EQ_Type(i_Data, Type_LWORD) OR EQ_Type(i_Data, Type_LREAL) OR EQ_Type(i_Data, Type_LTIME) OR EQ_Type(i_Data, Type_DTL) THEN
    t_Size := 8;
  ELSIF EQ_Type(i_Data, Type_STRING) THEN
    t_Size := 256;  // default Siemens string header + max
  ELSIF EQ_Type(i_Data, Type_WSTRING) THEN
    t_Size := 16382;
  ELSE
    t_Size := -1;  // STRUCT/UDT — caller must use SERIALIZE
  END_IF;
  fcSizeOfVariant := t_Size;
END_FUNCTION
EQ_Type availability: EQ_Type is part of the Comparator operations > Variant folder. On S7-1200 firmware V4.4+ it is fully supported. On older firmware (V4.2/V4.3) use TYPE_OF to read the data type code and compare against the constants in the Type constants for Variant table.

Elementary data type reference

The byte widths below match the TIA Portal S7-1200 manual collection on elementary data types:

Type Bytes Type Bytes
BOOL 1 BYTE / CHAR 1
WORD / S5TIME / DATE 2 INT / UINT 2
DWORD / TIME / DINT / UDINT / REAL 4 TOD 4
LWORD / LINT / ULINT / LREAL / LTIME / DTL 8 STRING (default) 256
WSTRING (default) 16382 POINTER / ANY 6 / 10

Method 3 — Symbolic / UDT Approach (Preferred for New Code)

If the data being passed is always the same UDT, the cleanest approach is to declare the parameter type as the UDT itself and let the compiler determine the size. SIZEOF(MyUDT) is evaluated at compile time and the value is available offline in the program info. The TIA Portal online help confirms that SIZEOF can be applied to:

  • A data type directly (e.g. SIZEOF(REAL) → 4)
  • A tag declared with that type (e.g. SIZEOF("dbI".tag1) → 4)
  • The instance DB of a typed FB

For UDP payload sizing in TUSEND/TUSENDC, the only code required is:

"instTUSEND"(data := io_Payload,
              len  := SIZEOF(io_Payload));

No runtime calculation, no SERIALIZE block, no scratch buffer. The same pattern works for TRCV, TSEND, ICONNECT, and most communication instructions that need a length parameter.

Integration with TUSEND / TUSENDC (UDP)

The TUSEND and TUSENDC instructions require the LEN input to match exactly the number of bytes the remote peer will receive. Mis-sizing the field is the most common reason that TUSEND reports 16#8085 (LEN invalid) or that the receiving device discards malformed packets. The SCL snippet below combines the SERIALIZE method with TUSEND:

// Trigger calculation once when data is ready
"instGetSize"(i_Start := bReady,
              io_Source := "dbRecipe".stMsg,
              o_SizeBytes => "dbSend".dwSizeBytes,
              o_Error     => "dbSend".bSizeError);

IF "dbSend".dwSizeBytes > 0 AND NOT "dbSend".bSizeError THEN
  "instTUSEND"(REQ   := bReady,
               ID    := 1,
               LEN   := TO_UINT("dbSend".dwSizeBytes),
               ADDR  := t_RemoteAddr,
               DATA  := "dbRecipe".stMsg);
END_IF;

Error Codes and Diagnostics

RET_VAL (hex) Meaning Corrective action
0000 No error
8085 LEN out of range (TUSEND/TUSENDC) Recalculate size with FB_GetSizeOfStruct and feed LEN from o_SizeBytes
8252 SERIALIZE: OFFSET + LEN larger than source Use OFFSET 0 and LEN -1 (entire source)
8253 Not enough memory at DEST_ARRAY Increase the size of t_Bytes or pass a typed UDT instance instead of a byte array
8254 Source is not a structured type Pass a UDT/STRUCT/ARRAY, not a single BOOL/INT
80B1 VARIANT points to detached / freed area Re-evaluate the source pointer; check DB length
80B2 EQ_Type: variant has invalid handle Variant uninitialized — gate the call with a valid input flag

Complete lists are in the S7-1500 system manual (error codes for SERIALIZE) and the S7-1200 system manual (error codes for the Variant instructions).

Verification Steps

  1. Create a test DB with three UDTs of known size (e.g. 8, 32, and 256 bytes) and place an instance of each as separate tags.
  2. Call FB_GetSizeOfStruct on each tag from a cyclic OB (e.g. OB1) and watch o_SizeBytes in the watch table.
  3. Cross-check the value with the offline SIZEOF on the same tag: place SIZEOF("dbTest".u1) in a global constant and compare.
  4. Use the Program info > Compilation tab in TIA Portal to read the cross-reference and confirm the block sizes match.
  5. Send the resulting payload to a PC-based receiver (Wireshark on UDP port 2000 is sufficient) and confirm the packet length matches o_SizeBytes.

Troubleshooting Matrix

Symptom Likely cause Fix
o_SizeBytes stays at 0 i_Start not pulsed; OB1 only calls FB once Use rising-edge detection or a watchdog pattern
o_Status = 8253 Scratch buffer t_Bytes too small Increase ARRAY bound, or pass a typed instance and use SIZEOF directly
TUSEND reports 16#8085 LEN cast to UINT and value > 65535 Cap payload at 1460 bytes (typical UDP MTU) before sending
Size changes after firmware update Compiler padding differs between firmware revisions Always read size at runtime; do not hard-code
Size is 0 for nested UDT Source variant is a scalar, not a structure Wrap the scalar in a STRUCT with one member
EQ_Type always returns FALSE Variant handle lost on multi-instance DB Pass the structured tag directly, not a pointer to it
GetSizeOfStruct works in PLCSIM but not on real CPU Real CPU older firmware Update to V2.0+ (S7-1500) or V4.4+ (S7-1200)

Field-Proven Caveats

  • The Optimized block access attribute cannot be toggled without losing the symbol table. Plan block design before download.
  • When an S7-300/400 master exchanges data with an S7-1200/1500 via GET/PUT, the 1500 must still present data in a DB laid out exactly the way the 300/400 expects. A UDT on the 1500 that mirrors the legacy layout is the cleanest bridge.
  • Compiler padding for nested STRUCTs follows natural alignment, not minimal alignment. A UDT containing BOOL + LREAL will report 16 bytes, not 9.
  • The DESERIALIZE step in the SERIALIZE round-trip is required only when the destination is dynamic. For fixed UDTs, SIZEOF on the typed instance is enough.
  • On S7-1500 CPUs with firmware V2.5 or later, SERIALIZE/DESERIALIZE supports DTL, LDT, LWORD, WCHAR, and WSTRING. On older firmware, the type set is restricted.

Choosing the Right Method

Method Best for Drawback
Symbolic / SIZEOF New projects, fixed UDTs Does not handle dynamic types
SERIALIZE round-trip Generic size discovery on optimized blocks Requires scratch buffer and 1–2 ms of execution time
VARIANT + EQ_Type Library blocks, type-dispatching functions Does not give the size of nested STRUCTs without recursion
AT view on DB Backwards compatibility with STEP 7 V5 code Forces unoptimized access, breaks symbolic-only programming
Use the symbolic / UDT method on every new function block. Reserve SERIALIZE and VARIANT methods for library and protocol code where the type cannot be pinned at design time.

FAQ

Why does the classic ANY pointer size trick fail in TIA Portal V16+ on S7-1500?

Optimized blocks replace the underlying P# pointer with a handle. The runtime resolves the access in the work memory and no longer exposes the byte count, so any dissection of the ANY pointer returns zero or invalid data. The fix is to use SERIALIZE on the structured tag and read SIZEOF of the typed destination.

Can I keep the block unoptimized and keep using the old ANY trick?

Yes, but at the cost of symbolic-only programming and the S7-1500 performance benefits. Disabling the optimized attribute re-enables the legacy layout and the old code works, however every consumer of the block must also be unoptimized, which is rarely acceptable on modern projects.

What does SERIALIZE RET_VAL 16#8253 mean and how do I fix it?

16#8253 means the destination array does not have enough free memory for the serialized payload. Increase the size of the destination ARRAY OF BYTE, or pass a typed UDT/STRUCT instance as DEST and read SIZEOF on it directly.

Is SIZEOF evaluated at compile time or at runtime in TIA Portal?

For fixed data types and UDTs, SIZEOF is a compile-time constant. The compiler substitutes the byte count at build time, so there is zero runtime overhead. For VARIANT inputs, SIZEOF is not applicable; use SERIALIZE or EQ_Type instead.

What is the maximum UDT size that SERIALIZE can handle on an S7-1500?

SERIALIZE can serialize up to the size of the destination buffer. In practice, an 8192-byte scratch ARRAY OF BYTE covers every standard S7-1500 structured tag. For larger payloads, split the data into segments or move directly to the typed destination and avoid the byte array altogether.

Does the result change after a firmware update?

Rarely. The natural-alignment padding rules used by the TIA Portal compiler are stable across firmware versions, so SIZEOF and SERIALIZE results stay the same. The exception is the introduction of new elementary types (LWORD, DTL, LTIME) which are supported only on firmware V2.5+ for S7-1500 and V4.4+ for S7-1200.

Back to blog