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:
- SERIALIZE / DESERIALIZE on an instance of the same data type — returns the number of bytes the runtime will move.
- VARIANT + EQ_Type — generic functions that work on any data type passed by reference, useful for TUSEND/TURCV, custom protocols, and Modbus payload mapping.
- Symbolic / UDT approach — preferred for new code; sizes are always known at compile time and no runtime calculation is required.
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. TheATview 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 |
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
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
- 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.
- Call
FB_GetSizeOfStructon each tag from a cyclic OB (e.g. OB1) and watcho_SizeBytesin the watch table. - Cross-check the value with the offline SIZEOF on the same tag: place
SIZEOF("dbTest".u1)in a global constant and compare. - Use the Program info > Compilation tab in TIA Portal to read the cross-reference and confirm the block sizes match.
- 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 |
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.