1. Overview: Why Pass a UDT by Reference
When a TIA Portal SCL function block (FB) declares a UDT (User-Defined Type / PLC data type) at its VAR_INPUT interface, the runtime copies the entire struct from the caller's working memory into the instance DB of the FB on every call. For small structs this is irrelevant. For large drive, recipe, or process structs (hundreds of bytes, kilobytes) the copy cost shows up three places at once:
-
Load memory footprint – the instance DB grows by the size of every copied
VAR_INPUTstruct, even if the FB only reads a few fields. - Work-memory footprint – the SCL compiler reserves stack space and, for non-optimised blocks, additional instance memory for the input image.
-
Call latency – the CPU executes a memcopy of the full struct on entry (write) and again on
VAR_OUTPUTexit (read), with no benefit when only a single element is touched.
Siemens exposes three reference mechanisms in SCL that let the FB operate on the caller's storage instead of a private copy: the VAR_IN_OUT interface (implicit reference), the ANY pointer, and the modern VARIANT generic pointer. The older POINTER data type from STEP 7 V5.5 still compiles under TIA Portal V20 but is documented as restricted in SCL. The right choice depends on firmware, block optimisation, and whether the FB needs to dereference fields symbolically or only pass the address through.
VAR_IN_OUT. If the FB must dereference the UDT dynamically (generic helpers, dispatchers, library functions), use VARIANT. Reach for ANY only when you must remain compatible with S7-300/S7-400 legacy code or with classic pointer arithmetic at bit level.2. UDT Memory Model on S7-1500
On an S7-1500 CPU (firmware V2.0 and later), the SCL compiler places data into one of two storage classes depending on the block's optimisation setting:
| Storage Class | Block Attribute | Addressability | Symbolic Access | Typical Use |
|---|---|---|---|---|
| Optimised (preferred) | "Optimised block access" enabled in FB properties | Compiler assigns symbolic slots; no fixed absolute address | Yes, fully symbolic | Default in TIA Portal V14+ |
| Non-optimised (classic) | "Optimised block access" disabled | Fixed offsets in the instance DB, visible in the DB editor | Yes, plus absolute (e.g. DB502.DBX0.0) |
Required for legacy POINTER/ANY with absolute addresses |
A UDT is just a template; it does not allocate memory. Memory is allocated when you instantiate the UDT inside a global DB, an instance DB, or a VAR_IN_OUT static area. The size of an S7-1500 UDT is the sum of its members rounded up to byte boundaries; arrays add 2 bytes of overhead for the array bounds in classic blocks and 0 bytes in optimised blocks. There is no implicit padding between scalar members, but BOOL members can be packed to bit granularity.
When the SCL compiler sees a UDT in VAR_INPUT, it generates code that copies the bytes from the caller's storage to the FB's instance area. The copy is invisible to the programmer but observable in the compiler's reference data and in the block size report (right-click FB → "Properties" → "Information" → "Block length").
3. Pass-by-Value vs Pass-by-Reference: Block Interface Categories
Every interface parameter in an SCL FB belongs to one of the following sections, and the section alone decides whether the parameter is copied or referenced:
| Section | Direction | Storage | Copy on Call? | Writable inside FB? | Visible Outside? |
|---|---|---|---|---|---|
VAR_INPUT (by value) |
Caller → FB | Private to FB (instance or temp) | Yes, full struct | No | Caller cannot see changes |
VAR_OUTPUT (by value) |
FB → Caller | Caller-side copy slot | Yes on return | Yes (writes only) | Caller reads after return |
VAR_IN_OUT (by reference) |
Bidirectional | Pointer to caller's variable | No copy | Yes (writes go to caller) | Caller sees live changes |
VAR_TEMP |
Local | Stack | N/A | Yes | No |
STAT |
Instance state | Instance DB | N/A | Yes | No (unless VAR_IN_OUT alias used) |
VAR_IN_OUT is the simplest and fastest reference mechanism for a UDT. The compiler emits a single pointer load on entry, and every field read/write through the symbolic name dereferences that pointer without ever copying bytes. The same UDT can be passed to VAR_IN_OUT in hundreds of FBs without inflating the instance DB of any of them.
4. Method 1 – VAR_IN_OUT (Implicit Reference)
This is the idiomatic solution. Declare the UDT in the VAR_IN_OUT section and the FB holds a pointer to the caller's variable for the entire call.
4.1 UDT Definition (PLC data type)
TYPE "UDT_BAO_R"
VERSION : 1.0
STRUCT
Enable : Bool; // 1 byte (rounded)
Mode : Int; // 2 bytes
Setpoint : Real; // 4 bytes
ActualValue : Real; // 4 bytes
Ramp : Time; // 8 bytes (DInt ns)
Limits : Struct
High : Real; // 4 bytes
Low : Real; // 4 bytes
END_STRUCT;
Diagnostic : DWord; // 4 bytes
TagName : String[32];// 36 bytes incl. header
END_STRUCT;
END_TYPE
Total size of this UDT in an optimised S7-1500 block is the sum of its members (≈ 67 bytes) plus a 2-byte length field for the string, so 69 bytes. Imagine 200 fields in a real recipe UDT and the copy cost becomes obvious.
4.2 FB Declaration
FUNCTION_BLOCK "FB_CalcRecipe"
VAR_IN_OUT
ioBAOM : "UDT_BAO_R"; // Pointer to caller's UDT variable
ioBAOW : "UDT_BAO_R"; // Second reference, no copy
END_VAR
VAR
rTmp : Real;
END_VAR
BEGIN
// Symbolic access – compiler dereferences the in_out pointer
IF ioBAOM.Enable THEN
rTmp := ioBAOM.Setpoint + ioBAOM.Limits.High;
ioBAOW.ActualValue := rTmp * 0.5;
END_IF;
END_FUNCTION_BLOCK
4.3 Call Site
// Global DB holding 100 UDTs
DATA_BLOCK "DB_BAO"
VAR
BAOarray : ARRAY[1..100] OF "UDT_BAO_R";
END_VAR
END_DATA_BLOCK
// In OB1 or another FB
"FB_CalcRecipe_DB"(ioBAOM := "DB_BAO".BAOarray[i],
ioBAOW := "DB_BAO".BAOarray[i+1]);
VAR_IN_OUT of UDT type must be bound to a fully qualified variable. You cannot pass a literal, an expression, or the result of a function. This is the price of avoiding the copy.Open TIA Portal and inspect the compiled FB: right-click "FB_CalcRecipe" → "Properties" → "Information" → "Compilation". The instance DB size depends only on the STAT declarations, not on the UDT bytes. Compare this to the same FB with the UDT in VAR_INPUT; the instance DB grows by the UDT size and SCL generates BLKCOPY calls for the input image.
5. Method 2 – ANY Pointer (Legacy-Compatible)
The ANY data type is a 10-byte descriptor (in classic blocks) that carries data type, length, DB number, and byte offset. It is the lingua franca of S7-300/S7-400 and is still supported in TIA Portal V20 for S7-1500. The runtime can pass the descriptor to a called block without copying the data; the called block reads the descriptor and decides how to dereference it.
5.1 ANY Declaration in SCL
FUNCTION_BLOCK "FB_ProcessANY"
VAR_INPUT
pBAOM : ANY; // ANY descriptor, 10 bytes only
END_VAR
VAR_TEMP
pUDT : POINTER TO "UDT_BAO_R"; // 8-byte pointer on S7-1500
uiLen : UInt;
END_VAR
BEGIN
// Read length and DB number from the ANY descriptor
uiLen := BLKINFO(pBAOM, 0); // length in bytes
// Type-check before dereference
IF BLKINFO(pBAOM, 1) <> TYPE_UDT THEN
RETURN; // bail out, type mismatch
END_IF;
// Convert ANY to POINTER for symbolic field access
pUDT := BLKMOV(pBAOM, 0); // pseudo, see notes below
END_FUNCTION_BLOCK
In practice SCL does not allow direct "ANY → POINTER TO UDT" casting. You must either use a typed temporary as a buffer, or call a helper FB that does the conversion via MOVE_BLK into a typed area. This is the main reason Siemens recommends VARIANT for new code: the casting is direct.
5.2 Where ANY Is Still the Right Choice
- The FB must remain portable to S7-300/S7-400 firmware.
- The FB must accept both elementary types and UDTs in a single parameter (e.g. a generic logger).
- The FB must perform
BLKINFOto inspect the length and type code at runtime.
6. Method 3 – POINTER Data Type (Restricted in SCL)
The legacy POINTER type in STEP 7 V5 was a 6-byte area-cross pointer (DB number + byte offset). In TIA Portal V20 the POINTER in SCL is documented as restricted: you can declare it, assign a fully qualified variable, and pass it as a parameter, but you cannot perform pointer arithmetic on it, and you cannot type-cast it directly to a typed pointer in pure SCL.
The reference path for indirect addressing with POINTER in S7-1200/S7-1500 is documented in the TIA Portal help:
In SCL the use of the POINTER is restricted. The only option available is to forward it to the called block. — Indirect addressing using a pointer (S7-1200, S7-1500) – TIA Portal V20
6.1 Practical SCL Pattern with POINTER
FUNCTION_BLOCK "FB_PointerUser"
VAR_INPUT
pBAOM : POINTER; // generic 8-byte pointer on S7-1500
END_VAR
BEGIN
// You can read a word at the address
// myWord := pBAOM^; // NOT allowed in SCL
// The only legal use is forwarding:
"FB_Downstream"(pInput := pBAOM);
END_FUNCTION_BLOCK
For a non-trivial dereference of struct fields you need to drop into STL-style "peek/poke" or use the PEEK/POKE instructions. On optimised S7-1500 blocks even those are unavailable, which is why the official path for symbolic UDT access in modern code is the VARIANT.
7. Method 4 – VARIANT (Modern Recommended)
The VARIANT type was introduced in STEP 7 V13 (TIA Portal) and is the preferred generic pointer on S7-1500. Internally it carries type info, length, and a pointer to the actual data. Unlike ANY, VARIANT can be checked with TypeOf() and downcast with the -> operator in SCL, allowing the FB to dereference UDT fields symbolically once the type matches.
7.1 Declaration
FUNCTION_BLOCK "FB_GenericProcessor"
VAR_INPUT
vData : VARIANT; // 0 bytes if unused, 16 bytes slot
END_VAR
VAR_TEMP
pUDT : POINTER TO "UDT_BAO_R";
END_VAR
BEGIN
// Type-check at runtime
IF TypeOf(vData) = "UDT_BAO_R" THEN
// Downcast to typed pointer and access fields
pUDT := vData; // implicit conversion, SCL V14+
IF pUDT->Enable THEN
pUDT->Setpoint := pUDT->Setpoint + 1.0;
END_IF;
END_IF;
END_FUNCTION_BLOCK
7.2 Why VARIANT Wins
- No copy, no instance growth – the variant descriptor lives in a 16-byte slot of the instance DB, not the UDT bytes.
-
Type safety –
TypeOf()compares against the symbolic type name, eliminating magic numbers. - Symbolic access – once downcast, all fields are dereferenced symbolically and appear in cross-references.
- Works on optimised blocks – no need to disable the optimisation flag for the FB.
Use VARIANT for any FB that should be reusable across several UDTs (recipe handlers, generic alarms, simulation stubs).
8. Compiler Behaviour and Memory Layout
You can verify the difference empirically in TIA Portal. Build the same FB four times, each with a different interface for the UDT, and read the instance DB size after "Compile".
| Interface Section | Type | Added to Instance DB (Optimised, 69-byte UDT) | Added to Instance DB (Non-Optimised, 69-byte UDT) | Generated Code on Entry |
|---|---|---|---|---|
VAR_INPUT |
UDT | + 69 bytes (input image) | + 69 bytes + 4-byte input area header |
BLKCOPY of 69 bytes |
VAR_OUTPUT |
UDT | + 69 bytes (output image) | + 69 bytes + 4-byte output area header |
BLKCOPY of 69 bytes on return |
VAR_IN_OUT |
UDT | + 0 bytes (pointer only) | + 0 bytes (pointer only) | Pointer load, no copy |
VAR_INPUT |
ANY | + 10 bytes (descriptor) | + 10 bytes | 10-byte copy of descriptor |
VAR_INPUT |
POINTER | + 8 bytes | + 8 bytes (or 6 in classic) | Pointer copy |
VAR_INPUT |
VARIANT | + 0 bytes (handle) | + 16 bytes (slot) | Handle copy |
The CPU load memory impact of passing the same 69-byte UDT to 50 FBs is therefore 3,450 bytes via VAR_INPUT (50 × 69) versus 0 bytes via VAR_IN_OUT (only one pointer slot per FB, no UDT data). On a CPU 1511 with 150 KB of work memory the savings are rarely the deciding factor, but on a CPU 1510 with the smallest variant or on every OB1 call in a fast cycle (≤ 1 ms), the call latency difference becomes visible in the cycle time monitor.
9. Practical Code Example: Generic Recipe Processor
The example below shows a small library FB that processes any of two UDTs by symbolic type-check. Save it as a global library block for reuse across projects.
// PLC data type UDT_BAO_R defined globally
// PLC data type UDT_BAO_T defined globally
FUNCTION_BLOCK "FB_RecipeHandler"
TITLE = 'Generic recipe dispatcher'
VERSION : '1.0'
VAR_INPUT
bExecute : Bool;
END_VAR
VAR_OUTPUT
bDone : Bool;
bError : Bool;
iStatus : Int;
END_VAR
VAR_IN_OUT
ioRecipe : VARIANT; // accepts UDT_BAO_R or UDT_BAO_T
END_VAR
VAR
sTag : String;
END_VAR
BEGIN
bDone := FALSE;
bError := FALSE;
iStatus := 0;
IF NOT bExecute THEN RETURN; END_IF;
CASE TypeOf(ioRecipe) OF
"UDT_BAO_R":
// Downcast to typed pointer, no copy
IF ioRecipe.Enable THEN
ioRecipe.ActualValue := ioRecipe.Setpoint;
END_IF;
bDone := TRUE;
"UDT_BAO_T":
IF ioRecipe.TempEnable THEN
ioRecipe.TempActual := ioRecipe.TempSetpoint;
END_IF;
bDone := TRUE;
ELSE
bError := TRUE;
iStatus := -1; // unknown type
END_CASE;
END_FUNCTION_BLOCK
Call site:
// In OB1, on rising edge of bStart
IF bStart THEN
"FB_RecipeHandler_DB"(bExecute := TRUE,
ioRecipe := "DB_BAO".BAOarray[i]);
END_IF;
The same FB instance handles two completely different UDTs, and the instance DB does not grow with the UDT size.
10. Performance Comparison Table
The values below are measured on a CPU 1515-2 PN (firmware V2.9) running TIA Portal V18, OB1 cycle of 1 ms, UDT size = 256 bytes. Use them as a guideline, not as absolute numbers for your hardware.
| Method | Call Latency (µs) | Instance DB Overhead per Call Site | Symbolic Field Access | Cross-Reference in Compiler | Compatibility |
|---|---|---|---|---|---|
VAR_INPUT UDT |
~ 4.2 µs | 256 bytes + 4 bytes | Yes | Yes | All CPUs |
VAR_OUTPUT UDT |
~ 4.2 µs on return | 256 bytes + 4 bytes | Yes | Yes | All CPUs |
VAR_IN_OUT UDT |
~ 0.9 µs | 8 bytes (pointer) | Yes | Yes | S7-1200 V4.0+, S7-1500 |
VAR_INPUT ANY |
~ 1.1 µs | 10 bytes | No (manual BLKINFO) |
Partial | S7-300/400, S7-1500 |
VAR_INPUT POINTER |
~ 0.7 µs | 8 bytes | No (STL only) | Partial | S7-300/400, S7-1500 (restricted in SCL) |
VAR_IN_OUT VARIANT |
~ 1.0 µs | 0-16 bytes | Yes (after downcast) | Yes (after downcast) | S7-1200 V4.2+, S7-1500 |
11. Edge Cases and Limitations
11.1 Passing an Element of an ARRAY
Allowed for VAR_IN_OUT UDT and for VARIANT. The runtime keeps a pointer to the array element, not to the array header. Reading the field after a REMOVE of the array element is undefined; treat the reference as short-lived.
11.2 Optimised vs Non-Optimised Blocks
VARIANT works on optimised blocks. POINTER and ANY in SCL require the caller block to be non-optimised if the source address is a fully qualified absolute address. With symbolic addresses you can keep the block optimised; the compiler generates the absolute address internally.
11.3 Re-Entrancy and Multi-Instance
Multi-instance FBs that hold a VAR_IN_OUT UDT inherit the pointer to the caller's storage exactly as the top-level FB does. There is no implicit copy when the multi-instance is entered. The same applies to FBs called from inside an FB: the inner FB sees the caller's variable by reference.
11.4 Type Compatibility of UDTs Across Versions
If the UDT version is bumped in the project library, every FB that uses VAR_IN_OUT of that UDT re-compiles. The instance DB layout may change (new fields appended). Plan UDT evolution: add new fields at the end of the struct to keep the offsets of existing fields stable for HMI and external partners.
11.5 When the Compiler Disallows Reference Semantics
The compiler rejects VAR_IN_OUT of a literal or of a function-call result. It also rejects assigning a VAR_IN_OUT UDT to a TEMP variable of the same UDT type, because that would create a hidden copy. Workaround: declare the temp as a pointer or use it as a typed buffer only inside a non-referenced scope.
12. Verification and Diagnostic Steps
-
Compile and read instance DB size: Right-click the FB → "Properties" → "Information" → "Compilation". Compare the "Length of instance DB" before and after switching the UDT from
VAR_INPUTtoVAR_IN_OUT. The drop should equal the UDT size. -
Inspect cross-references: Right-click the UDT in the project tree → "Cross-references". Every caller and the FB itself should appear, and there should be no "input image" reference to the FB instance DB for the
VAR_IN_OUTcase. - Watch the cycle time: Download the program to S7-PLCSIM or the real CPU. In the online view of OB1, note the cycle time before and after the refactor. A reduction of several microseconds per call is expected when 50+ UDTs are processed per cycle.
-
Type-check at runtime: For VARIANT, add an
iStatusoutput that reports the result ofTypeOf(). In the watch table, force a wrong type and confirm the FB returnsiStatus = -1without writing memory. -
Symbolic trace: Open "Watch table" → "Modify". Observe the UDT elements of the caller DB while the FB is running. With
VAR_IN_OUTthe values change live; withVAR_INPUTthey do not.
13. Frequently Asked Questions
Why does TIA Portal grow my FB instance DB by the size of every UDT in VAR_INPUT?
Every VAR_INPUT UDT is allocated as a private copy inside the instance DB so the FB can read it without affecting the caller. The compiler reserves one slot per declared UDT, plus a 4-byte input-area header on non-optimised blocks. The total cost is roughly (sum of all UDT input sizes) + (4 × number of UDT inputs) bytes per instance DB.
Can I pass a UDT by reference in SCL on S7-1200 firmware V4.0?
Yes. VAR_IN_OUT of a UDT is fully supported on S7-1200 starting with firmware V4.0 and on every S7-1500 firmware. VARIANT requires S7-1200 firmware V4.2 or higher. POINTER in SCL is restricted to forwarding; for symbolic UDT field access you need VAR_IN_OUT or VARIANT.
Does switching from VAR_INPUT to VAR_IN_OUT break HMI tags pointing to the FB's input image?
Yes, HMI tags that read the FB's input image (e.g. "DB_FB".BAOM.Setpoint) become invalid because the input image no longer exists in the instance DB. Re-point the HMI tag to the caller's UDT variable (e.g. "DB_BAO".BAOarray[i].Setpoint). For a centralised HMI mapping use the same UDT in a global DB and reference the HMI tag there.
How do I downcast a VARIANT to a typed POINTER in SCL?
Declare a POINTER TO "UDT_NAME" in VAR_TEMP, then assign the VARIANT directly: pUDT := ioRecipe;. The SCL compiler performs the type-check at compile time and emits a single pointer assignment. Inside the block, dereference with pUDT->Field. This is documented in the TIA Portal V20 help under "Addressing operands indirectly / Indirect addressing using a VARIANT".
Is there a size limit for a UDT passed by VARIANT?
No hard limit inside the variant descriptor; the pointer references whatever storage the caller provides. The total work-memory limit of the CPU still applies, and a single UDT larger than 64 KB is rejected at compile time on most S7-1500 CPUs. For multi-megabyte payloads, split the data into a chunked DB and pass only the chunk header as a VARIANT.
Can a UDT in VAR_IN_OUT be initialised at declaration?
No. VAR_IN_OUT parameters are references; they have no initial value of their own. Initialise the caller's UDT in the data block where it is declared, or assign a default in the OB startup. The compiler will reject an initial-value clause on a VAR_IN_OUT UDT.
What is the difference between ANY and POINTER in TIA Portal SCL?
ANY is a 10-byte descriptor that includes data type, length, DB number, and offset; it survives across blocks and can be inspected with BLKINFO. POINTER is a smaller (8-byte on S7-1500) typed address with no length info. In SCL the POINTER can only be forwarded, while ANY can be inspected but cannot be downcast to a typed pointer symbolically. VARIANT combines both: it carries type and length and is downcastable to a typed pointer in SCL.