Passing DB Name as FC Input Parameter in SCL (TIA Portal)
Reusable Function (FC) blocks that read or write to multiple instance data blocks are a recurring need on S7-1200 and S7-1500 controllers. A common symptom: code compiles when the FC hard-codes the target DB name, but the FC cannot be retargeted at runtime without recompiling or duplicating logic. In SCL on TIA Portal there are two production-ready solutions: a typed IN_OUT parameter backed by a PLC Data Type (UDT), and the DB_ANY data type combined with the OPN / LAR1 register operations. Both are valid; each carries trade-offs in performance, type safety, and maintainability.
1. Problem Definition
You maintain an FC (for example, FC2_SequenceStep) that processes one step of a machine sequence. The sequence data lives in a global DB (for example, SEQUENCE_1) and includes fields such as StepNumber, StepTime, NextStep, FaultCode, and Outputs.
Compiling the FC with a hard-coded DB reference works:
SCL
// Hard-coded reference - inflexible
IF "SEQUENCE_1".StepNumber = 10 THEN
"SEQUENCE_1".StepTime := "SEQUENCE_1".StepTime + 1;
END_IF;
The problem appears when a second DB (SEQUENCE_2) must drive the same logic. Three paths are typically considered:
- Duplicate the FC for every DB and call the right copy. This scales linearly with the number of sequences and forces copy-paste maintenance.
- Pass an IN/IN_OUT parameter that carries the DB identity. This is the goal — but the parameter type matters.
- Use indirect addressing through pointer registers. This was the standard STL pattern (
OPN DB,LAR1) but is awkward in SCL.
2. Root Cause: Why the DB Symbol Cannot Be an FC Parameter Directly
A global DB reference in SCL resolves to an absolute address at compile time. The FC parameter interface accepts typed values (INT, BOOL, ARRAY, UDT, etc.) or pointer-like values, but it cannot accept a DB symbol as a parameter slot. When the SCL compiler encounters "SEQUENCE_1".StepNumber, it emits a direct DB-DB area-internal address with a fully-resolved offset. The DB is part of the generated code, not a runtime variable.
To make the DB a runtime decision, the FC must either:
- Receive a copy of the DB's payload (or a reference to it) as a typed parameter — the UDT/IN_OUT approach.
- Receive a numeric or symbolic handle to the DB and dereference it with register indirection — the DB_ANY approach.
3. Solution A: UDT + IN_OUT Parameter (Recommended Default)
This is the cleanest, type-safe pattern and should be your default unless you have a documented reason to use indirection. The DB becomes a typed container; the FC receives the data (or a reference to the data) through its interface.
3.1 Create the PLC Data Type (UDT)
In the project tree, expand PLC data types and add a new type named Sequence:
SCL
// PLC data type: "Sequence" (UDT)
TYPE "Sequence"
STRUCT
StepNumber : INT; // current step index
StepTime : TIME; // dwell in current step
NextStep : INT; // next step index on success
FaultCode : INT; // 0 = OK, <>0 = error code
Outputs : WORD; // output bit image
Flags : ARRAY[0..15] OF BOOL;
END_STRUCT;
END_TYPE
3.2 Create a Global DB Containing One or More Sequence Instances
Rather than building one DB per sequence with the UDT fields at the top level, place named Sequence instances inside a single DB. This gives the compiler a stable data layout and avoids DB sprawl.
SCL
DATA_BLOCK "myDB"
STRUCT
Sequence1 : "Sequence"; // machine line 1
Sequence2 : "Sequence"; // machine line 2
Sequence3 : "Sequence"; // machine line 3
ActiveIdx : INT; // which sequence the FC should use
END_STRUCT;
BEGIN
END_DATA_BLOCK
3.3 Declare the FC with an IN_OUT Parameter of the UDT
SCL
FUNCTION "FC2_SequenceStep" : VOID
VAR_INPUT
TickMs : INT; // cycle tick
END_VAR
VAR_IN_OUT
Seq : "Sequence"; // typed reference into myDB.SequenceN
END_VAR
VAR_TEMP
i : INT;
END_VAR
BEGIN
// The Seq parameter is the live alias of myDB.SequenceN
Seq.StepTime := Seq.StepTime + INT_TO_TIME(TickMs);
IF Seq.StepTime >= T#5s THEN
Seq.StepNumber := Seq.NextStep;
Seq.StepTime := T#0s;
END_IF;
// Array scan uses typed bounds
FOR i := 0 TO 15 DO
Seq.Flags[i] := (Seq.Outputs.%X{i}) OR Seq.Flags[i];
END_FOR;
END_FUNCTION
3.4 Call the FC with the Correct Instance
SCL
CASE "myDB".ActiveIdx OF
1: "FC2_SequenceStep"(TickMs := 100, Seq := "myDB".Sequence1);
2: "FC2_SequenceStep"(TickMs := 100, Seq := "myDB".Sequence2);
3: "FC2_SequenceStep"(TickMs := 100, Seq := "myDB".Sequence3);
END_CASE;
The compiler validates the type match between myDB.Sequence1 and the Seq : "Sequence" interface. Mismatches are caught at compile time, not in the field.
3.5 Inline SCL View of the Generated Code
For diagnostic purposes you can monitor the UDT instance live. Online > Monitor/Modify gives a structured view; the slice syntax "myDB".Sequence1.StepTime is valid in watch tables, HMI tags, and OPC UA exposed symbols.
4. Solution B: DB_ANY + OPN / LAR1 Indirection
DB_ANY was introduced for S7-1200/1500 to replace the legacy WORD-based DB-pointer pattern (DBNO/DBLW). It is a 32-bit identifier that points to a data block without exposing its content. To use the data inside, you still need to load the start address into AR1 and address it through P# / % accessors, or wrap it with an AT overlay.
4.1 Declare the FC with a DB_ANY Input
SCL
FUNCTION "FC2_SequenceStep_Any" : VOID
VAR_INPUT
TickMs : INT;
pSeq : DB_ANY; // symbolic handle to a DB
END_VAR
VAR_TEMP
pData AT pSeq : ARRAY[*] OF BYTE; // SCL 0-byte AT overlay forbidden; use ST-equivalent
END_VAR
BEGIN
END_FUNCTION
DB_ANY parameter cannot be combined with an AT overlay in the FC's VAR_TEMP section — the compiler rejects it. The AT-overlay trick is permitted only inside FB static sections (multi-instance background). For FC usage you must drop down to register-level addressing.4.2 The Working Pattern: OPN + LAR1 + Symbolic Field Access
The SCL compiler will accept OPN and LAR1 instructions when embedded inline. You can then dereference the area using pointer offsets. The cleanest form on S7-1500 is to call an FB inside which AT-overlaid static variables exist; the FC opens the DB, the FB addresses the overlay.
SCL
FUNCTION_BLOCK "FB_SequenceStep"
VAR
// AT overlay lives here, not in VAR_TEMP
StepNumber AT %MD10 : INT;
StepTime AT %MD14 : TIME;
NextStep AT %MD22 : INT;
FaultCode AT %MD26 : INT;
Outputs AT %MW30 : WORD;
END_VAR
BEGIN
// Body uses StepNumber, StepTime, ... directly
StepTime := StepTime + INT_TO_TIME(100);
END_FUNCTION_BLOCK
SCL
FUNCTION "FC2_SequenceStep_Caller" : VOID
VAR_INPUT
TickMs : INT;
pSeq : DB_ANY;
END_VAR
VAR_TEMP
dbNum : DINT;
END_VAR
BEGIN
// Convert the DB_ANY handle to a DB number
dbNum := DB_ANY_TO_DINT(pSeq);
IF dbNum = 0 THEN RETURN; END_IF;
// Open the DB and load its start address into AR1
OPN DB[dbNum];
LAR1 ;
// The FC must invoke an FB whose AT overlay now points into the opened DB
"InstSequenceStep"(StepNumber := ??? );
END_FUNCTION
This pattern requires that the FB instance's data block (or the target DB) has identical layout. It is fragile and harder to maintain; it is also the only way to handle truly heterogeneous DBs (where each DB has a different structure that the FC must adapt to at runtime).
4.3 Converting DB_ANY to a Usable Pointer
Siemens exposes several conversion blocks in the standard library:
| Block | Purpose | Notes |
|---|---|---|
DB_ANY_TO_DINT |
DB number as DINT | 0 means invalid/null |
DB_ANY_TO_UINT |
DB number as UINT | Preferred for OPN DB[n]
|
WORD_TO_BLOCK_DB |
Legacy DB pointer cast | Compatibility only |
These functions are documented in the TIA Portal help under Instructions > Basic instructions > Addressing > DB_ANY. Once you have the number, OPN DB[ UINT_VARIABLE ] opens the block, and LAR1 with no operand loads the start of the opened area into AR1.
4.4 Why the DB_ANY Compiler Error Appears
A frequent error during this exercise is:
Compiling SCL: "DB_ANY" cannot be combined with "AT" overlay in VAR_TEMP.
Root cause: AT overlays in VAR_TEMP need a fixed local stack address, but DB_ANY is resolved at runtime. The compiler cannot bind a static offset to a dynamic area. Move the AT overlay into an FB's static area, or drop the AT overlay and use direct pointer arithmetic with P#.
5. Side-by-Side Comparison
| Criterion | UDT + IN_OUT | DB_ANY + OPN/LAR1 |
|---|---|---|
| Type safety | Compile-time check of every field access | No check — runtime offsets are author responsibility |
| Performance | One pointer pass; native field access | Multiple instructions per field; pointer math per call |
| Memory copy | None (IN_OUT passes pointer) | None, but area-register swap costs cycles |
| HMI / OPC UA mapping | Direct symbol access | Indirect via DB index |
| Maintenance | Add a field to UDT → propagates | Renumber offsets manually |
| Refactoring risk | Low — compiler enforces | High — silent misreads |
| DB layout freedom | All instances share one UDT shape | Any DB layout, any size |
| Best fit | Repeated, identical structures | Truly dynamic, heterogeneous targets |
6. Choosing the Right Pattern
- Use UDT + IN_OUT when: all target DBs share the same record shape (steps, alarms, parameters). This covers ~95% of machine sequence code, recipe data, axis parameter sets, and report records.
- Use DB_ANY when: the FC must read or write DBs whose layout is not known at compile time — for example, library FBs that operate on a customer DB without recompiling the library, or generic diagnostic utilities that walk arbitrary structures.
- Hybrid pattern: put the UDT in a single DB with an array of instances, and let the FC index into the array. You keep UDT type safety and gain dynamic instance selection without DB_ANY.
SCL
DATA_BLOCK "myDB"
STRUCT
Sequences : ARRAY[1..16] OF "Sequence";
ActiveIdx : INT;
END_STRUCT;
BEGIN
END_DATA_BLOCK
SCL
// Indexed instance selection — still strongly typed
"FC2_SequenceStep"(TickMs := 100,
Seq := "myDB".Sequences["myDB".ActiveIdx]);
ActiveIdx. STEP 7 does not eliminate this even in optimized blocks. If ActiveIdx can be operator-set, add explicit validation before the call to avoid PLC stop on out-of-range index.7. SCL Expression Rules That Affect Both Patterns
When calling an FC/FB inside SCL, the call is an expression statement. Parameter passing follows the rules in the SCL expressions and operations reference:
- Simple data types (INT, DINT, REAL, BOOL, etc.) are passed call-by-value; the FC receives a copy on the stack.
- Complex data types (STRUCT, ARRAY, STRING, UDT) are passed call-by-reference when the parameter is IN_OUT, by-pointer at the language level. The SCL keyword
IN_OUTis the only way to receive a writable typed reference. - Tags of type DB_ANY cannot be combined with slice syntax (
.field) directly. They must first be converted into a usable handle.
8. Common Pitfalls and Field-Tested Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
| Compiler: "DB_ANY cannot be combined with AT" | AT overlay declared in VAR_TEMP | Move AT into FB static section, or remove AT and use P# pointer arithmetic |
| FC writes have no effect | Parameter declared INPUT instead of IN_OUT | Switch interface section to VAR_IN_OUT |
| Compiler: "Type mismatch" at FC call | Passed symbol is not of the UDT | Confirm caller passes myDB.Sequence1 and not myDB.Sequence1.StepNumber
|
| PLC goes STOP with SF LED | Index out of range on array parameter | Bound-check before indexed access; use WITH guard or pre-validate |
| AR1 corrupted after FC returns | FC opened DB but caller expected previous DB open | Use OPN DI for instance DBs and OPN DB for globals; save/restore if multiple are interleaved |
| HMI cannot reach field | Field accessed via DB_ANY indirection | Expose a typed HMI-visible DB; avoid indirect symbols on HMI tags |
| Optimized block access warning | Symbolic access disabled on DB | Enable "Optimized block access" on DB properties |
9. Verification Procedure
After implementing either pattern, run through this checklist before commissioning:
- Compile clean. Project > Compile > Software (rebuild all). No warnings on DB/FC access.
- Download to PLC. Use "Download to device" with "consistent download" enabled for the affected blocks.
-
Monitor online. Right-click the FC in the program tree > Monitor/Modify. Confirm
Seq.StepNumber(or the DB_ANY-routed equivalent) shows the expected instance. -
Force a value change. In a watch table, write a new
StepNumberintomyDB.Sequence1and confirm the FC reflects it on the next scan. -
Swap the instance. Change
ActiveIdxfrom 1 to 2; confirm the FC now operates onmyDB.Sequence2without recompile. -
HMI smoke test. Display
myDB.Sequence1.StepTimeon an HMI screen; toggle the active instance and confirm the value stream changes. - Cycle-time budget. Open the online & diagnostics > Cycle time view. Confirm the FC adds no more than the documented per-call cost.
- Stop-recovery. Pull the SD card (S7-1500) or trigger a STOP-RUN cycle; confirm the DB data survives and the FC re-initializes correctly.
10. Extended Example: Parameterized Recipe Bank
A practical use of the UDT pattern is a recipe bank where each recipe has identical structure but different values:
SCL
// PLC data type: Recipe
TYPE "Recipe"
STRUCT
Name : STRING[32];
Setpoint : REAL;
Tolerance : REAL;
RampTime : TIME;
HoldTime : TIME;
Enabled : BOOL;
END_STRUCT;
END_TYPE
SCL
DATA_BLOCK "Recipes"
STRUCT
Items : ARRAY[1..50] OF "Recipe";
Count : INT;
END_STRUCT;
BEGIN
END_DATA_BLOCK
SCL
FUNCTION "FC_ApplyRecipe" : VOID
VAR_IN_OUT
R : "Recipe";
END_VAR
VAR_TEMP
i : INT;
END_VAR
BEGIN
IF NOT R.Enabled THEN RETURN; END_IF;
// Apply setpoint with ramp
// (assume an analog-output FC elsewhere)
"FC_SetAnalog"(Setpoint := R.Setpoint, Ramp := R.RampTime);
"FC_StartTimer"(Duration := R.HoldTime);
END_FUNCTION
SCL
// OB1 cycle
FOR i := 1 TO "Recipes".Count DO
"FC_ApplyRecipe"(R := "Recipes".Items[i]);
END_FOR;
The same FC handles every recipe without recompilation; the array index decides which instance is active.
11. Migration Notes from Legacy STL Patterns
Code carried over from STEP 7 Classic often uses OPN DB[MW10], LAR1 P#DBX 0.0, and indirect field reads like L DBW [AR1,P#2.0]. Modernizing this to TIA Portal SCL:
- Replace
OPN DB[MW10]with a typedDB_ANYinput and an internalOPN DB[UINT_VARIABLE]. - Replace
LAR1 P#DBX 0.0with a P# pointer constructed from the opened area, or eliminate entirely by using an AT overlay inside an FB. - Replace
L DBW [AR1,P#2.0]with SCL slice notation:myField.%W0or a UDT-typed reference.
OPN, LAR1, TAR1, LAR2 still compile inside SCL source files when marked as inline statements — but mixing pointer manipulation with high-level SCL control flow is the most common source of subtle bugs. Prefer the UDT/IN_OUT form unless you have a measured need for DB_ANY.12. FAQ
Can I pass a DB name as an FC parameter directly in SCL?
No. A DB symbol is resolved to an absolute address at compile time and cannot be bound to a parameter slot. Use a UDT-typed IN_OUT parameter (preferred) or a DB_ANY input combined with OPN/LAR1 indirection to achieve a runtime-retargetable FC.
Why does DB_ANY combined with AT in VAR_TEMP fail to compile?
An AT overlay in VAR_TEMP requires a fixed local stack offset, but DB_ANY resolves to the target DB only at runtime. The SCL compiler cannot bind a static offset to a dynamic area. Move the AT overlay into an FB's static section, or remove the AT and address the area with P# pointer arithmetic.
Is IN_OUT or INPUT correct for passing a structured parameter to an FC?
Use VAR_IN_OUT for complex types (STRUCT, ARRAY, UDT, STRING). Simple data types can use VAR_INPUT, but for UDTs you want IN_OUT because it passes a pointer to the original storage rather than copying the entire structure onto the stack, and it allows the FC to write back to the caller's variable.
When is DB_ANY truly required instead of a UDT?
Use DB_ANY when the FC must operate on a DB whose layout is unknown at compile time — for example, a library block that processes customer-specific DBs without recompilation, or a generic diagnostic tool that walks heterogeneous structures. For any pattern where all targets share one shape, the UDT/IN_OUT form is simpler, faster, and type-safe.
How do I convert a DB_ANY handle into an open DB?
Use the standard conversion block (DB_ANY_TO_DINT or DB_ANY_TO_UINT) to obtain the DB number, then issue OPN DB[ UINT_VARIABLE ] in the same SCL source file. The compiler accepts OPN as an inline instruction; pair it with LAR1 ; to load the area start address into AR1 for indirect access, or call an FB whose AT overlay already points into the opened DB.
Does indexed access on a UDT array stay type-safe?
Yes — the compiler still resolves Recipes.Items[i].Setpoint against the UDT definition. The variable i is runtime, but each field access is fully checked. Add explicit bounds validation on i before the access to prevent PLC stop on out-of-range indices.