Generic Numerical Functions Using VARIANT in S7-1500 SCL

David Krause15 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

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.

Engineering scope: The pattern is documented for the S7-1500 family (firmware V1.x and later) with TIA Portal V13 SP1 onward. The S7-1200 supports 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, or VAR_OUTPUT only 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 FC VAR_OUTPUT, VARIANT is 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 VariantGet and back using VariantPut.

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:

  1. 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 VARIANT support in FBs; firmware V1.8 supports FC VARIANT inputs.
  2. TIA Portal V13 SP1 (minimum for the code example shown), V15.1, V16, V17, or V18. The VariantGet/VariantPut instructions are present in all of these versions.
  3. Active SCL compiler license on the engineering station (part of STEP 7 Professional).
  4. A configured project with the CPU and a program block container ready for new code.
Compiler note: Earlier TIA Portal V13 (pre-SP1) builds raise error 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:

  1. FB-based pattern: Use an FB, declare the result as VAR_IN_OUT (an in/out parameter is bi-directional and accepted for VARIANT), and read/write through that parameter. The FB instance also persists diagnostic state such as the detected type and the last error code.
  2. 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_OUT parameter 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 for VARIANT use in TIA Portal V13 onward; the compiler rejects non-optimised VARIANT parameters.

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:

  1. Type match success path: In an OB1 cycle, call the FB with three INT tags, e.g. V1 := "DB_test".inA, V2 := "DB_test".inB, V3 := "DB_test".outSum. Verify OK = TRUE, ERROR = FALSE, and outSum = inA + inB in the watch table.
  2. Type mismatch failure path: Drive V1 as INT and V2 as REAL. Confirm ERROR = TRUE and that V3 is unchanged.
  3. NULL safety: Pass a VARIANT constant of NULL (zero) to V1. Confirm ERROR = TRUE and the function exits in < 1 µs.
  4. Floating-point edge cases: Pass V1 = 1.0e30, V2 = 1.0e30 (REAL). Confirm outSum = INF (i.e. 16#7F800000 in the hex view) and OK = TRUE. The block must not raise ERROR for IEEE-754 overflow — that is the expected numerical outcome.
  5. 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.
  6. Online snapshot: Trigger a "Snapshot of monitored values" from the watch table, then change input values, and confirm the new OK/ERROR states 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 for VARIANT parameters 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 / VariantPut return codes. The instructions return INT 0 on success and a non-zero Siemens error code on failure. Treat any non-zero value as a hard error and set ERROR.
  • Avoid recursion. SCL recursion (FB that calls itself) is not supported by the S7-1500 compiler. Use iterative code for any "process a generic VARIANT array" 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.

Back to blog