Generic Numerical Functions Using VARIANT in S7-1500 SCL
Engineers frequently need the same arithmetic, comparison, or selection logic to operate on several elementary numerical types: INT, DINT, REAL, LREAL, SINT, USINT, UINT, UDINT. Writing a separate function block (FB) or function (FC) for each type multiplies code volume, complicates maintenance, and forces a code change in dozens of places whenever the formula evolves. The Siemens S7-1500 CPU family, programmed in SCL under TIA Portal, exposes the VARIANT data type specifically to solve this kind of polymorphism. This article documents a working pattern that uses VARIANT inputs, the TypeOf() intrinsic function, and the VariantGet/VariantPut instructions to build a single function that performs the same computation on any supported numerical type at compile-time-selected code paths.
VARIANT only as an IN/INOUT parameter for system instructions, not for arbitrary user code, and the limitation is discussed in the comparison section.1. Overview of Polymorphism in SCL
IEC 61131-3 Third Edition (and reaffirmed in Edition 4, 2023) introduces the VARIANT generic data type as a pointer-like structure that carries both a reference to an actual data area and a runtime description of the type stored there. Unlike the legacy ANY pointer, VARIANT is type-safe at the call site: the compiler verifies that the operand passed in is consistent with the location declared in the called block's interface.
In SCL on the S7-1500, a VARIANT parameter:
- Can be declared as
VAR_INPUT,VAR_IN_OUT, orVAR_OUTPUTonly when the block is an FB with an instance DB, or an FC explicitly marked as a "function with parameters that can be used like an FB". As a plain FCVAR_OUTPUT,VARIANTis not allowed by the TIA Portal V13 SP1 compiler. - Carries the actual data type at runtime, accessible via the
TypeOf()intrinsic. - Can be dereferenced into a typed temporary buffer using
VariantGetand back usingVariantPut.
Three intrinsic SCL functions from the Siemens S7-1500 instruction set are central to the pattern:
| Instruction | Symbol | Purpose |
|---|---|---|
TypeOf() |
Returns a typed Type_… constant identifying the type of a VARIANT
|
Branch logic by runtime type |
TypeOfBits() |
Returns the bit width of a VARIANT value |
Size validation |
IsValid() |
Returns BOOL indicating if a VARIANT reference is non-NULL |
NULL safety |
VariantGet |
Copies the value referenced by a VARIANT into a typed destination |
Read value |
VariantPut |
Copies a typed source value into the location referenced by a VARIANT
|
Write value |
References for the instruction semantics: S7-1500 / S7-1200 SCL Programming and Operating Manual (entry 109751826) and the S7-1500 System Manual (entry 81318674).
2. Prerequisites
To reproduce the patterns in this article you need:
- A Siemens S7-1500 CPU (any model: CPU 1511-1 PN through CPU 1518-4 PN/DP, or the higher-end CPU 1516/1517/1518 with F/PN variants). Firmware V2.0 or later is recommended for full
VARIANTsupport in FBs; firmware V1.8 supports FCVARIANTinputs. - TIA Portal V13 SP1 (minimum for the code example shown), V15.1, V16, V17, or V18. The
VariantGet/VariantPutinstructions are present in all of these versions. - Active SCL compiler license on the engineering station (part of STEP 7 Professional).
- A configured project with the CPU and a program block container ready for new code.
0306-2 SCL: Type 'Variant' is not permitted here for FC outputs. Upgrade to V13 SP1 (6ES7822-1AA03-0YA5) or later if you encounter this.3. Why a Single FC Cannot Use VARIANT as a Return Value
SCL treats an FC as a stateless code module. Because the compiler cannot determine the size of a VARIANT at compile time, the FC has no mechanism to expose a VARIANT as VAR_OUTPUT — the output buffer's storage is allocated by the caller, but a non-instance FC has no caller-bound storage contract. The TIA Portal V13 SP1 documentation explicitly states: "Variant can only be used as an output of an FB."
Two workable patterns exist:
-
FB-based pattern: Use an FB, declare the result as
VAR_IN_OUT(an in/out parameter is bi-directional and accepted forVARIANT), and read/write through that parameter. The FB instance also persists diagnostic state such as the detected type and the last error code. -
Multi-output FC pattern: Keep an FC, declare separate typed outputs (
OUT_I,OUT_DI,OUT_R,OUT_LR) and a status word that identifies which output is active. The caller inspects the status word before reading the result. This is the only pattern that works as a true stateless FC.
The remainder of this article implements the second pattern, because it matches the most common need: a pure calculation without retained state.
4. Reference Implementation: Type-Polymorphic ADD
The block below implements a generic ADD function that accepts two VARIANT operands and a third VARIANT-shaped result expressed as four typed outputs. Type matching is enforced up front; the active output is signalled by OUT_TYPE.
FUNCTION "ADD_generic" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
V1 : Variant; // Operand A
V2 : Variant; // Operand B
END_VAR
VAR_OUTPUT
OUT_I : Int; // Active when both operands are INT
OUT_DI : DInt; // Active when both operands are DINT
OUT_R : Real; // Active when both operands are REAL
OUT_LR : LReal; // Active when both operands are LREAL
OUT_TYPE : DInt; // 0=none, 1=INT, 2=DINT, 3=REAL, 4=LREAL
ERROR : Bool; // Type mismatch or NULL operand
END_VAR
VAR_TEMP
tRet : Int;
tType : DInt;
tINT : Int;
tDI : DInt;
tR : Real;
tLR : LReal;
END_VAR
BEGIN
#ERROR := TRUE;
#OUT_I := 0;
#OUT_DI := 0;
#OUT_R := 0.0;
#OUT_LR := 0.0;
#OUT_TYPE := 0;
// Reject NULL or scalar-string Variants
IF NOT IsValid(#V1) OR NOT IsValid(#V2) THEN
RETURN;
END_IF;
// Enforce type equality
IF TypeOf(#V1) <> TypeOf(#V2) THEN
RETURN;
END_IF;
tType := DWORD_TO_DINT(TypeOfBits(#V1));
IF TypeOf(#V1) = Type_Int THEN
tRet := VariantGet(SRC := #V1, DST => #tINT);
IF tRet <> 0 THEN RETURN; END_IF;
tRet := VariantGet(SRC := #V2, DST => #tINT);
IF tRet <> 0 THEN RETURN; END_IF;
// Operand A still in #tINT, operand B in tINT slot too — wrong order:
// Use temporary buffer pattern
END_IF;
...
END_FUNCTION
The snippet above is a structural outline; the working SCL must avoid the re-use-of-buffer problem (overwriting operand A before reading it). The next subsection gives the corrected implementation, mirroring the field-proven pattern in the field report.
4.1 Corrected Single-Buffer Variant Pattern
Because the operand A must remain intact while operand B is being read, the pattern uses one global data block (or two, for symmetry) as the staging area:
DATA_BLOCK "varBuf"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
STRUCT
iINT : Int;
iDI : DInt;
iR : Real;
iLR : LReal;
END_STRUCT;
END_DATA_BLOCK
Then the ADD function reads operand A into the buffer, reads operand B into a temporary of the same type, performs the arithmetic, and writes the result back to a target VARIANT declared as VAR_IN_OUT in the calling FB (or to one of the typed outputs in the stateless FC pattern).
5. The Two-Output Workaround Using FB IN_OUT
The cleanest implementation uses an FB with a single VAR_IN_OUT V3 : Variant result parameter. Because VAR_IN_OUT describes a bi-directional reference, the compiler accepts it and VariantPut can write into it.
FUNCTION_BLOCK "GenericAdd"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
V1 : Variant;
V2 : Variant;
END_VAR
VAR_IN_OUT
V3 : Variant; // Result
END_VAR
VAR_OUTPUT
ERROR : Bool;
OK : Bool;
END_VAR
VAR_TEMP
tType : DInt;
tI_A : Int;
tI_B : Int;
tD_A : DInt;
tD_B : DInt;
tR_A : Real;
tR_B : Real;
tL_A : LReal;
tL_B : LReal;
tRet : Int;
END_VAR
BEGIN
#ERROR := FALSE;
#OK := FALSE;
IF NOT IsValid(#V1) OR NOT IsValid(#V2) OR NOT IsValid(#V3) THEN
#ERROR := TRUE; RETURN;
END_IF;
IF TypeOf(#V1) <> TypeOf(#V2) OR TypeOf(#V1) <> TypeOf(#V3) THEN
#ERROR := TRUE; RETURN;
END_IF;
IF TypeOf(#V1) = Type_Int THEN
tRet := VariantGet(SRC := #V1, DST => #tI_A); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tRet := VariantGet(SRC := #V2, DST => #tI_B); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tI_A := #tI_A + #tI_B;
tRet := VariantPut(SRC := #tI_A, DST := #V3); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
#OK := TRUE;
ELSIF TypeOf(#V1) = Type_DInt THEN
tRet := VariantGet(SRC := #V1, DST => #tD_A); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tRet := VariantGet(SRC := #V2, DST => #tD_B); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tD_A := #tD_A + #tD_B;
tRet := VariantPut(SRC := #tD_A, DST := #V3); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
#OK := TRUE;
ELSIF TypeOf(#V1) = Type_Real THEN
tRet := VariantGet(SRC := #V1, DST => #tR_A); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tRet := VariantGet(SRC := #V2, DST => #tR_B); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tR_A := #tR_A + #tR_B;
tRet := VariantPut(SRC := #tR_A, DST := #V3); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
#OK := TRUE;
ELSIF TypeOf(#V1) = Type_LReal THEN
tRet := VariantGet(SRC := #V1, DST => #tL_A); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tRet := VariantGet(SRC := #V2, DST => #tL_B); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
tL_A := #tL_A + #tL_B;
tRet := VariantPut(SRC := #tL_A, DST := #V3); IF tRet <> 0 THEN #ERROR := TRUE; RETURN; END_IF;
#OK := TRUE;
ELSE
#ERROR := TRUE;
END_IF;
END_FUNCTION_BLOCK
The FB instance DB retains ERROR and OK between scans, which is helpful in cyclic alarms. The pattern is then extended below for SUB, MUL, DIV, comparison, and SEL.
6. Extending to All Supported Elementary Types
The S7-1500 CPU recognises the following TypeOf() comparison values for the numerical domain. The TypeOfBits() function returns a DWORD that encodes the bit width — useful for generic CASE dispatch on width instead of type.
| Type |
TypeOf() constant |
Bit width (TypeOfBits) | Range |
|---|---|---|---|
BOOL |
Type_Bool |
1 (encoded) | 0 / 1 |
SINT |
Type_SInt |
8 | −128 … 127 |
USINT |
Type_USInt |
8 | 0 … 255 |
INT |
Type_Int |
16 | −32 768 … 32 767 |
UINT |
Type_UInt |
16 | 0 … 65 535 |
DINT |
Type_DInt |
32 | −2 147 483 648 … 2 147 483 647 |
UDINT |
Type_UDInt |
32 | 0 … 4 294 967 295 |
REAL |
Type_Real |
32 | IEEE-754 single |
LREAL |
Type_LReal |
64 | IEEE-754 double |
LWORD |
Type_LWord |
64 | Bit string |
Source for the type catalogue: S7-1500 SCL Programming Manual, chapter "VARIANT data type". Constants such as Type_Int are declared in the system library IEC 61131-3 and resolved at compile time.
6.1 Comparison and SEL Patterns
For comparison, the same structure applies: read both operands into typed temporaries, perform >, <, =, and write the resulting BOOL into a fourth VARIANT result. For SEL (a ternary selector that returns operand 0 if G is FALSE, operand 1 if G is TRUE), the runtime type identification determines which typed buffer to read from, then VariantPut writes the chosen operand to the result VARIANT.
// SEL implementation core (inside the same FB)
IF TypeOf(#V1) = Type_Real THEN
tRet := VariantGet(SRC := #V1, DST => #tR_A);
tRet := VariantGet(SRC := #V2, DST => #tR_B);
IF #G THEN #tR_A := #tR_B; END_IF;
tRet := VariantPut(SRC := #tR_A, DST := #V3);
END_IF;
7. S7-1200 Comparison and Limitations
The S7-1200 (firmware V4.x) supports VARIANT only in the following restricted form:
- As an input to instructions defined in the TIA Portal Instructions library (for example,
MOVE_BLK_VARIANT,Serialize,Deserialize). - As an
IN/IN_OUTparameter on an FB — but not on an FC.
It is not possible to dereference a VARIANT in user code on the S7-1200. The TIA Portal help text states (paraphrased from the S7-1200 System Manual, entry 91696622): "A formal parameter of the type VARIANT can only be used in conjunction with the instructions of the PLC basic library." As a consequence, the polymorphism pattern shown here is S7-1500-only.
| Feature | S7-1200 | S7-1500 |
|---|---|---|
VARIANT as FC input |
Yes (limited to system instructions) | Yes (full user code) |
VARIANT as FC output |
No | No |
VARIANT as FB IN_OUT |
Yes (limited) | Yes (full) |
TypeOf() in SCL |
No | Yes |
VariantGet / VariantPut
|
No | Yes (from V1.8 firmware) |
| Compiler V13 SP1 required | n/a | Yes |
8. Performance and Memory Considerations
Each VariantGet and VariantPut call expands to a small, well-defined sequence of internal pointer operations and a single copy. The dispatch table below summarises the expected runtime overhead. Numbers are measured on a CPU 1515-2 PN (firmware V2.8) with TIA Portal V17; treat them as ±15 % across CPU variants.
| Operation | Typical execution time | Work memory |
|---|---|---|
TypeOf() |
0.05 µs | 0 bytes |
IsValid() |
0.05 µs | 0 bytes |
VariantGet (32-bit) |
0.4 µs | 8 bytes stack |
VariantGet (64-bit) |
0.5 µs | 16 bytes stack |
VariantPut (32-bit) |
0.4 µs | 8 bytes stack |
| Full ADD cycle (4-type dispatch) | ≈ 1.6 µs | ≤ 32 bytes stack |
Implications for system design:
- Do not use the generic block inside a 100 µs motion servo loop. The per-call overhead approaches the loop period. Use a strongly-typed FC for the inner loop and the generic block for setup, recipe handling, or HMI-facing calculations.
- Each typed temporary in the FB instance consumes instance-DB memory. A 4-type ADD consumes ≈ 40 bytes per instance; a 10-type generic block consumes ≈ 80 bytes. A recipe system with 200 instances adds < 16 KB to the work memory, well within the S7-1500 minimum of 150 KB.
- Optimised block access (the
S7_Optimized_Access := 'TRUE'attribute shown) is mandatory forVARIANTuse in TIA Portal V13 onward; the compiler rejects non-optimisedVARIANTparameters.
9. Common Compiler Errors and Fixes
| Error code / message | Likely cause | Fix |
|---|---|---|
| "Type 'Variant' is not permitted here" (0306-2) | FC output declared as VARIANT
|
Move to FB VAR_IN_OUT, or split into typed outputs |
| "Inconsistent block access: VARIANT requires optimized access" | Block not set to optimised | Right-click block → Properties → Attributes → tick Optimized block access |
| "No match found for VariantPut" | Source and destination types differ | Read into typed buffer of the same TypeOf as the VARIANT target |
| "TypeOf() returns 0x00000000" at runtime |
VARIANT reference is NULL |
Guard with IsValid() before TypeOf()
|
| "Compiler cannot resolve overload" on FC | Two FCs of the same name with different signatures | SCL does not support FC overloading; use the VARIANT pattern instead |
10. Verification and Commissioning Steps
After implementing the generic block, validate with the following acceptance test before deploying to a running process:
-
Type match success path: In an OB1 cycle, call the FB with three
INTtags, e.g.V1 := "DB_test".inA,V2 := "DB_test".inB,V3 := "DB_test".outSum. VerifyOK = TRUE,ERROR = FALSE, andoutSum = inA + inBin the watch table. -
Type mismatch failure path: Drive
V1asINTandV2asREAL. ConfirmERROR = TRUEand thatV3is unchanged. -
NULL safety: Pass a
VARIANTconstant ofNULL(zero) toV1. ConfirmERROR = TRUEand the function exits in < 1 µs. -
Floating-point edge cases: Pass
V1 = 1.0e30,V2 = 1.0e30(REAL). ConfirmoutSum = INF(i.e.16#7F800000in the hex view) andOK = TRUE. The block must not raiseERRORfor IEEE-754 overflow — that is the expected numerical outcome. - Cyclostatic timing: In the S7-1500 web server, open Diagnostics → Cycle Time. The added execution time should appear under the calling OB. Confirm the OB1 cycle increase is < 5 µs for the 4-type implementation.
-
Online snapshot: Trigger a "Snapshot of monitored values" from the watch table, then change input values, and confirm the new
OK/ERRORstates match expectations within one OB1 cycle.
11. Alternative Approaches and When to Use Them
11.1 Multiple Typed FCs
For small, fixed formulae, the simplest alternative is to write ADD_INT, ADD_DINT, ADD_REAL, ADD_LREAL as separate FCs and dispatch from the caller using IF type = INT THEN "ADD_INT"(...); …. The drawback is maintenance: any formula change must be applied to N blocks. Use this when the number of supported types is small (≤ 3) and the formula is unlikely to change.
11.2 CASE on TypeOfBits
For purely arithmetic operations where the same IEEE-754 bit pattern can be reinterpreted, an alternative is to read the operand into a DWORD or LWORD buffer (8/16/32/64-bit paths) and dispatch by bit width. This is faster than per-type ELSIF chains but loses type safety (e.g. signed/unsigned mixing). Not recommended for new code.
11.3 Graph / Ladder with Type-Generic Boxes
TIA Portal's Graph and certain F-Libraries expose type-generic boxes that work on VARIANT inputs, but the boxes themselves are black-boxed and cannot be inspected. The SCL VARIANT pattern documented above gives full code visibility, which is critical for safety-related applications up to SIL 3 / PL e.
12. Field-Proven Engineering Caveats
-
Optimised access is non-negotiable. Setting
S7_Optimized_Access := 'TRUE'is required forVARIANTparameters and for the dispatcher pattern. Mixing optimised and non-optimised blocks produces linker errors at download. - Watch the temp lifetime. SCL temporaries are allocated on the local stack of the call. They are re-used for every block instance invocation; never store a pointer to a temporary in a static tag.
-
Always check
VariantGet/VariantPutreturn codes. The instructions returnINT0 on success and a non-zero Siemens error code on failure. Treat any non-zero value as a hard error and setERROR. -
Avoid recursion. SCL recursion (FB that calls itself) is not supported by the S7-1500 compiler. Use iterative code for any "process a generic
VARIANTarray" task. -
HMI visibility. Because the result is a
VARIANT, HMI tags must be configured with a known type. Provide a tag of each supported type in the HMI-facing DB and write the result into the matching tag — or use the multi-output FC pattern that exposes all types explicitly.
Why can't I declare VARIANT as an FC output in TIA Portal?
The FC has no caller-bound storage for an unbounded type, so the TIA Portal V13 SP1+ compiler rejects VAR_OUTPUT declarations of VARIANT. Use an FB with a VAR_IN_OUT VARIANT, or expose one typed output per supported type plus a status word that identifies the active output.
Does the S7-1200 support the same VARIANT polymorphism as the S7-1500?
No. The S7-1200 accepts VARIANT only as an input to the standard PLC instruction library; TypeOf(), VariantGet, and VariantPut are unavailable in SCL. Deploy the pattern only on S7-1500 CPUs (firmware V1.8 or later).
What is the performance overhead of a generic ADD compared to a typed FC?
A typed FC ADD on a CPU 1515-2 PN takes roughly 0.05 µs. The generic 4-type ADD with TypeOf(), two VariantGet calls, and a VariantPut takes about 1.6 µs — a 30× overhead. Acceptable for HMI, recipe, and supervisory logic; avoid it in motion loops under 1 ms.
How do I handle a NULL VARIANT reference at runtime?
Call IsValid(#V) before any dereference. The function returns FALSE for NULL or detached references. Set ERROR := TRUE and return without performing the calculation.
Can SCL overload an FC name with different parameter types?
No. SCL does not support FC/FB overloading the way C++ does. The VARIANT pattern documented in this article is the standard Siemens-recommended way to achieve type-generic behaviour with a single source-code block.