SCL Symbolic DB Access via Variable on S7-1200/1500

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

SCL Symbolic DB Access via Variable on S7-1200/1500

Problem Definition: Indirect Symbolic DB Reference in SCL

When an SCL routine must select a data block at runtime (for example, choosing DB1, DB2, or DB3 based on a recipe index or HMI input) and read or write a tag symbolically inside that DB, the most natural code form fails to compile. The classic failure shape is shown below.

FUNCTION FC1 : VOID
VAR_INPUT
    iDB : BLOCK_DB;      // Any DB number at runtime
END_VAR
BEGIN
    DB1.DB_VAR := 1;          // OK - fully symbolic, DB is fixed
    iDB.DBX0.0 := 1;          // OK - purely absolute access via the handle
    iDB.DB_VAR := 1;          // COMPILE ERROR - symbolic tag on a variable DB
END_FUNCTION

The compiler reports an error of the form "The identifier 'DB_VAR' is unknown in the data block 'iDB'" or "No instance of the data block was opened". The error is consistent across TIA Portal V13.1 through V20, STEP 7 V5.6, and SCL compilers used on the ET 200 Pro (IM 154-8 PN/PN and IM 154-3 PN variants). It is not a bug; it reflects how SCL resolves tag names at compile time.

The BLOCK_DB system type is an opaque handle that the compiler treats as an absolute pointer. Once the DB number is captured in iDB, the compiler no longer has a fixed type signature to look up tags against, so symbolic lookups are rejected. Absolute access with DBX / DBW / DBD remains valid because the compiler does not need to know the tag layout to emit a load/store to a fixed byte offset.

This forces an architectural decision: either redesign the storage so a fixed reference resolves symbolically, or switch from symbolic to absolute addressing at the cost of losing the symbolic-table lookup and the ability to use the same code against future DB revisions.

Why Direct Symbolic Access Fails with a BLOCK_DB Variable

SCL compiles symbolic access in two passes. In pass one, the compiler reads the symbolic table and assigns absolute memory offsets. In pass two, it walks the generated STL/MC7 code, fixing the literal operands. Symbolic lookups are anchored on the compile-time DB number; that anchor is fixed in the compiled code.

When you write iDB.DB_VAR, the tag name DB_VAR is bound to the type declared in the open DB (here, DB1 with type DB_VAR). The handle iDB arrives at runtime and may point to a different DB (DB2, DB3) with a different type signature. Mixing the two produces:

  • Type-safety violation when DB2 has a different layout from DB1.
  • A compile-time error because the compiler cannot bind DB_VAR to a runtime-dispatchable variable on the chosen CPU.

For ET 200 Pro on the older K5.3.6.x firmware family (used with STEP 7 V5.x plus the ET 200 Pro GSD files), symbolic tables do not expose runtime-dispatchable DB types. For S7-1200/S7-1500 with TIA Portal, an analogous situation applies to DB_ANY handles in SCL but does allow Variant-based access (covered below).

The four operating patterns listed below each resolve the conflict in a different place:

  1. Make the container indirect, not the DB.
      Use an ARRAY OF UDT in a single DB.
  2. Keep the DB indirect but switch the addressing to absolute.
      Use the PEEK/POKE family of intrinsics.
  3. Keep the DB indirect and use a self-describing handle.
      Use VARIANT pointer dereferencing with type guards.
  4. Use a function block that owns its instance data; dispatch by instance pointer.
      Multi-instance (S7-300/400), multi-instance DB, or static-in-FB (S7-1500).

Solution 1: ARRAY of UDT-Based DBs (Recommended Pattern)

The cleanest answer on S7-300/400, S7-1200, and S7-1500 alike is to abandon multi-DB dispatch and consolidate everything into a single DB that contains an ARRAY of a user-defined type (UDT). Each array element now plays the role of the original DB, and the index is a plain INT variable that the compiler accepts in any symbolic expression.

Step 1 - Define a UDT.

TYPE "UDT_Recipe"
  STRUCT
      Task1Active : BOOL;
      Task1Speed  : INT;
      Task2Active : BOOL;
      Task2Speed  : INT;
      RecipeName  : STRING[32];
      PassCount   : DINT;
  END_STRUCT
END_TYPE

Step 2 - Declare the container DB. Use Only instance-specific option so the DB has a fixed block number and a fully symbolic type signature.

DATA_BLOCK "DB_Recipes"
  STRUCT
      Items : ARRAY[1..16] OF "UDT_Recipe";
      Count : INT;
  END_STRUCT
BEGIN
END_DATA_BLOCK

Step 3 - Read and write the dispatchable element.

FUNCTION FC10 : VOID
VAR_INPUT
    iIndex : INT;   // 1..16 from HMI or scheduler
    iValue : INT;   // new speed value
END_VAR
BEGIN
    IF (iIndex >= 1) AND (iIndex <= 16) THEN
        "DB_Recipes".Items[iIndex].Task2Speed := iValue;        // symbolic write
        IF "DB_Recipes".Items[iIndex].Task1Active THEN
            "DB_Recipes".Items[iIndex].PassCount :=
                "DB_Recipes".Items[iIndex].PassCount + 1;
        END_IF;
    END_IF;
END_FUNCTION

This form is fully symbolic, type-safe, refactor-friendly, and supported on every S7 CPU that runs SCL.

Memory Layout of DB_Recipes
Offset (byte) Symbol Type Notes
0.0 Items[1].Task1Active BOOL Recipe 1 status
2.0 Items[1].Task1Speed INT Recipe 1 speed
4.0 Items[1].Task2Active BOOL  
6.0 Items[1].Task2Speed INT  
8.0 Items[1].RecipeName STRING[32] 34 bytes incl. length prefix
... ... ... Repeats every 64 bytes
960.0 Items[16].PassCount DINT Last element
964.0 Count INT Active recipe count

If the original code already has DB1, DB2, ... DB16 hard-coded, the migration is essentially a copy of each DB's contents into Items[i]. The on-the-wire DB number remains DB_Recipes; HMI tags shift from DB1,DB_VAR to DB_Recipes,Items[1],DB_VAR - update once in the HMI tag configuration.

Solution 2: PEEK / POKE Intrinsic Functions (TIA Portal)

When the storage must stay in separate DBs because some external system (a Siemens panel, WinCC, a third-party OPC UA server, or a legacy peer) is bound to DB1..DBn by absolute number, the DB-dispatch needs to move from symbolic to absolute addressing. TIA Portal SCL provides four intrinsics that are documented in the Siemens reference manual Basics of indirect addressing (S7-1200, S7-1500):

  • PEEK(area :=, dbNumber :=, byteOffset :=) — read 1 byte, 1 word, 1 dword, or 1 lword.
  • PEEK_BOOL(area :=, dbNumber :=, byteOffset :=, bitOffset :=) — read a bit.
  • POKE(area :=, byteOffset :=, value :=) — write a 8/16/32/64-bit value.
  • POKE_BOOL(area :=, byteOffset :=, bitOffset :=, value :=) — write a bit.
  • PEEK_BOOL variant overloads exist from TIA V15.1; PEEK_WORD and explicit PEEK_DWORD overloads from TIA V16.

The area enum corresponds to 16#81 for DB (the bit-or-with 0x80 set flags the IDB area). The complete allowable values:

Area IDs for PEEK / POKE
Area Hex Dec Used by
Inputs (I) 0x81 129 PEEK(area:=129, byteOffset:=i)
Outputs (Q) 0x82 130 output read-back
Merkers (M) 0x83 131 scratchpad flags
DB 0x84 132 most common case for this article
IDB (instance DB) 0x85 133 multi-instance read

Source: Siemens Knowledge Base Siemens Support Portal - search ID 109755052 for the official cheat sheet.

Write equivalent of the original example.

FUNCTION FC1 : VOID
VAR_INPUT
    iDB        : BLOCK_DB;
    iByteOff   : DINT;
    iBitOff    : INT;
END_VAR
BEGIN
    // Write a BOOL bit at the requested offset into the runtime-chosen DB
    POKE_BOOL(area  := 16#84,
              dbNumber := WORD_TO_INT(iDB),
              byteOffset := iByteOff,
              bitOffset  := iBitOff,
              value := TRUE);
END_FUNCTION

Read equivalent.

FUNCTION FC2 : INT
VAR_INPUT
    iDB      : BLOCK_DB;
    iWordOff : DINT;
END_VAR
BEGIN
    FC2 := PEEK(area      := 16#84,
                dbNumber   := WORD_TO_INT(iDB),
                byteOffset := iWordOff);
END_FUNCTION

The signature of PEEK in TIA V20 is:

FUNCTION PEEK : VOID
VAR_INPUT
    area       : BYTE;     // 16#81..16#85 as above
    dbNumber   : UINT;
    byteOffset : DINT;
END_VAR
VAR_OUTPUT
    value      : VARIANT;  // {_:BYTE,_:WORD,_:DWORD,_:LWORD}
END_VAR

Modern TIA Portal folds the result into a VARIANT, so the caller must coerce it with INT_TO_ or use an assignment that matches the requested width.

STEP 7 V5.x alternative: on the classic S7-300/400 CPU, PEEK/POKE are not built-in. Use L DBW [MD 20] / T DBW [MD 20] with a memory-format pointer, or an STL UC FB[n] call together with an ANY pointer built by P#DB... BYTE 1. See solution 4.

Solution 3: Variant-Based Indirect Access (S7-1500 Only)

S7-1500 CPUs (and ET 200 Pro controllers based on the S7-1500 such as the IM 154-8 PN/PN CPU variant) support VARIANT pointer dereferencing. The Symbolic access to variant-tagged data instruction pair MOVE_BLK / Serialize / Deserialize gives fully symbolic, type-checked reads and writes when paired with a VARIANT input. This combines the benefits of solutions 1 and 2 with strong type safety.

FUNCTION_BLOCK FB_RecipeIO
VAR
    StaticValue : INT := 0;
END_VAR
VAR_INPUT
    pTarget : VARIANT;   // points into the chosen DB
END_VAR
BEGIN
    // Read symbolic tag into StaticValue
    IF VariantTypeToDBType(pTarget) = INT_TYPE THEN
        StaticValue := VariantGet(BYTE_TO_INT_AT_OFFSET(pTarget, 0));
    END_IF;
END_FUNCTION_BLOCK

For full symbolic read/write, declare the target tag as a VARIANT parameter of the FB, then pass the actual symbol at the call site:

// Caller
"DB_Recipes".Items[iIndex].Task2Speed := "DB_Recipes".Items[iIndex].Task2Speed + 1;
VariantPut(    dst := "DB_Recipes".Items[iIndex].Task2Speed,
               src := DeltaSpeed );

This is the only pattern in which the DB element being accessed is itself a runtime pointer rather than a fixed symbol. It is supported from CPU firmware V2.5 on S7-1500 (Firmware V2.5 / FW 2.5.0 / 6ES7515-2AM02-0AB0) and is not available on S7-300/400 or on S7-1200 (which only has a stripped VariantGet/VariantPut without tag-of-tag dereferencing).

Solution 4: Multi-Instance and Any-Pointer Method (Legacy S7-300/400)

For STEP 7 V5.x where the original poster's K5.3.6.1 firmware revision applies, the canonical answer is to use the ANY pointer built with P#DBi.DBXb BYTE n, then dereference it with the indirect STL instructions OPN DI + L DIB [AR1, P#0.0]. The method survives into TIA Portal for STEP 7-1500 firmware compatibility mode but is rarely used on modern projects.

FUNCTION FC1 : VOID
VAR_TEMP
    tAny : ANY;
END_VAR
BEGIN
    tAny := P#DB1.DBX0.0 BYTE 1;        // build pointer at runtime
    // ... or compute the byte offset and DB number
    tAny := P##DBToByte;                // symbolic operator for instance-of-FB
END_FUNCTION

The SCL-friendly version is to declare a UDT array and use "MyDB".MyArray[i].MyStruct which compiles to the same STL pointer arithmetic. This essentially brings you back to solution 1.

Comparison Table of Indirect DB Access Methods

Indirect DB access patterns at a glance
Method Platform Symbolic? Type-safe? Performance When to choose
ARRAY of UDT S7-300/400/1200/1500 Yes Yes 1 load, 1 store per access (fast) Default. Best for new code.
PEEK / POKE S7-1200/1500 (TIA V13+) No No (no symbol check) Function-call overhead, ~6 µs on S7-1516 External fixed-DB-number contracts
Variant / VariantPut S7-1500 FW ≥ 2.5; ET 200 Pro CPU 1516pro-2 PN (FW 2.5+) Yes (at caller) Yes Pointer chase + serialization, ~10 µs Type-safety required with runtime DB choice
Multi-instance / ANY S7-300/400, STEP 7 V5 Yes (FB local) Yes Optimized to indirect STL Legacy maintenance, K5.3.6.1 ET 200 Pro
Per-DB scalar All Yes (one DB only) Yes Inline STL load/store (1 µs) Single fixed DB, never changes

Platform-Specific Notes: ET 200 Pro, S7-300, S7-1500

The original poster runs an ET 200 Pro on K5.3.6.x firmware, which corresponds to the IM 154-8 PN/PN base or the SIMATIC ET 200pro F-CPU module. The CPU behavior between ET 200 Pro sub-models differs enough to change the recommended pattern.

ET 200 Pro CPU modules and indirect DB support
CPU module Order number Firmware Best pattern
ET 200pro F-CPU 1516pro-2 PN 6ES7516-2PN03-0AB0 V2.8 / V2.9 VARIANT or ARRAY
ET 200pro CPU 1516pro-2 PN 6ES7516-2PN02-0AB0 V2.5 / V2.6 VARIANT from V2.5, ARRAY before
IM 154-8 PN/PN (no CPU) 6ES7154-8PN00-0AB0 - S7-300/400 host via PROFIBUS / PROFINET
IM 154-3 PN (no CPU) 6ES7154-3AB00-0AB0 - STEP 7 V5.x host, classic DBs

ET 200 Pro with no built-in CPU is a distributed I/O head. It appears in the host CPU as slots in the PROFIBUS/PROFINET address space. The host CPU handles the dispatch, so the question of which pattern to use falls back to the host family:

  • S7-300 (CPU 314, CPU 315-2 PN, CPU 317-2, etc.) - ARRAY or ANY pattern.
  • S7-400 - same as S7-300; VARIANT access is not available.
  • S7-1500 (used as host) - any of the four patterns; preferred order ARRAY > VARIANT > PEEK/POKE > ANY.
  • S7-1200 (used as host) - ARRAY or PEEK/POKE; VARIANT tag-of-tag tag dereferencing limited.
Firmware alignment. When migrating from STEP 7 V5 to TIA Portal, re-check each affected CPU's FW. The PEEK / POKE intrinsics appear from TIA Portal V13.1 SP1 for S7-1200 and TIA V13 plus update 4 for S7-1500. Earlier TIA versions only ship WORD_TO_BLOCK_DB / BLOCK_DB_TO_WORD helpers and the indirect STL set. Reference: Addressing variables in global data blocks - STEP 7 Basic V13.1 - ID 109054417.

Verification & Commissioning Steps

Whichever pattern is chosen, commission in this order:

  1. Compile clean
    After implementing the pattern, perform Compile > Software (rebuild all). The watch table must show zero unresolved references, zero warnings about ambiguous symbolic lookups. TIA Portal: Project tree > right-click PLC > Compile > Software (rebuild all).
  2. Offline/online comparison
    Establish Online > Compare offline/online. The comparison must be coherent on every block that touches the dispatch logic. A mismatch indicates a stale target build, common when K5.3.6.1 projects are re-implemented in TIA.
  3. Watch the dispatch index
    Create a watch table with the index variable, the selected DB handle, and the affected tag. Toggle the index and observe the live values. With ARRAY pattern, watch "DB_Recipes".Items[iIndex].Task1Active directly. With PEEK/POKE, watch both the byte/bit offsets and the live signal at DB[dbNumber].DBX[byteOffset].X[bitOffset] in a separate watch row.
  4. Boundary tests
    Test the upper and lower index bounds (1 and 16 in the example). Test invalid indices (0, -1, 17, +32767) to confirm the IF guard fires and prevents an out-of-range access. On S7-300 this is doubly important because uncaught array index out-of-range faults become STOP (OB121 not present).
  5. Trace the pointers
    On S7-1500, use the built-in trace to record the index, the VariantGet return value, and the destination tag at 1 kHz for 10 seconds. Compare against the HMI indication.
  6. HMI source change
    When migrating to the ARRAY pattern, search the HMI project (WinCC Comfort/Professional, TIA Portal HMI, or third-party like Ignition or C-more) for any tag pointing at DB1,DB_VAR, DB2,DB_VAR, …, and re-target to DB_Recipes,Items[1],Task1Active. AutomationDirect C-more's Siemens S7-300 Ethernet (ISO over TCP/IP) Addressing Help shows the same memory-type syntax; confirm that the new PLC tag structure still resolves on the panel.
  7. Protective STOP test
    Force the dispatch into a deliberately out-of-range value while online, confirm the IF guard sends the block to no-op rather than triggering an access error. On ET 200 Pro / S7-1500, a clean retry should not engage the OB121 handler; on S7-300, OB121 must be installed if any indirect access is to occur.

Common Pitfalls and Compiler Diagnostics

Typical compile errors and remediation
Compiler message Cause Fix
The identifier 'DB_VAR' is unknown in the data block 'iDB' Symbolic tag lookup against a BLOCK_DB handle Switch to ARRAY pattern; restructure to remove DB dispatch
No instance of the data block was opened DB number runtime-dispatched, symbolic anchor missing Same as above; or use OPN DI[i] if migrating legacy STL
Declaration of variable iDB hides the same name in the instance DB Multi-instance collision Rename the parameter or use a different section (VAR_TEMP)
Cannot convert type 'POINTER TO STRING' to 'POINTER TO BOOL' Mixed ANY pointer types in an FB call Build separate ANY constructors per used type; do not share
Time-stamped Online-Diff error 'Different DB numbers' only in one CPU Firmware mismatch on hardware config Open Device View in the HW catalog, select correct firmware version

Safety and Diagnostic Notes

Indirect DB access concentrated in a single dispatcher is a known source of soft error storms. Recommendations for production projects:

  • Install OB121 (programming error) on every S7-300/400 host. Without it, an array-index out-of-range fault turns the CPU to STOP.
  • On S7-1500, install OB80 (time error), OB82 (diagnostic interrupt), and enable Peripheral & Word & Byte access checking in CPU properties so that OPN-wrong-area faults raise a faulted status rather than crash.
  • Wrap the dispatcher call in a DB_ANY_TO_UINT guard so callers cannot request DB 0 (which is the system DB on older PLCs). DB 0 must never be opened; any code path leading to PEEK with dbNumber=0 will fault the CPU.
  • For safety projects on ET 200 Pro F-CPU: indirect dispatch is allowed only if the safety integrity check confirms that the dispatch index cannot be tampered with. Use the F-runtime group to write the index, treat it as a safety-related variable, and validate against an HMI acknowledgment before applying to F-tag access.

Recap and Decision Flowchart

Use the following checklist to pick a method for the next project:

  1. Can the storage live in a single DB? → ARRAY of UDT (Solution 1).
  2. Do external contracts require fixed DB numbers? → PEEK / POKE (Solution 2).
  3. Is type safety required with dispatchable DBs, and is the host S7-1500 with FW ≥ 2.5? → VARIANT (Solution 3).
  4. Is the host S7-300/400 and the project STEP 7 V5? → ANY pointer / Multi-instance (Solution 4) with an ARRAY-based core.

For the original poster's ET 200 Pro K5.3.6.1 setup, the simplest production-ready answer is solution 1: keep all DBs as old-fashioned DB1..DBn if downstream WinCC or HMI bindings demand it, and on the SCL side expose a single helper FC that uses an INPUT integer to index into an internal case structure, with each case symbolic-accessing the matching DB. This is identical to the direct-DB form, but compiled as a single dispatcher, so the compiler does not reject it.

FUNCTION FC_Dispatch : VOID
VAR_INPUT
    iIndex : INT;
    iValue : INT;
END_VAR
BEGIN
    CASE iIndex OF
        1: DB1.Task1Active := BOOL_TO_BOOL(TRUE);
           DB1.Task1Speed  := iValue;
        2: DB2.Task1Active := BOOL_TO_BOOL(TRUE);
           DB2.Task1Speed  := iValue;
        3: DB3.Task1Active := BOOL_TO_BOOL(TRUE);
           DB3.Task1Speed  := iValue;
        // ... up to DBn
    ELSE
        // Discard - guard against invalid index
    END_CASE;
END_FUNCTION

This keeps the symbolic API at the DB level and is legal on every CPU that runs SCL. Code volume grows linearly with N, so prefer solutions 1-3 if N is large.

FAQ

Why does iDB.DB_VAR := 1; fail to compile when both DB1 and iDB are BLOCK_DB type?

The compiler anchors symbolic lookups on the compile-time DB number. With iDB as a runtime variable, the anchor is lost, so the compiler falls back to absolute addressing only - which means it cannot resolve the symbol name. Either move to an ARRAY of UDT (Solution 1) or switch to PEEK/POKE intrinsics (Solution 2).

Can I use PEEK and POKE on ET 200 Pro with software K5.3.6.1?

PEEK/POKE intrinsics are TIA Portal functions for S7-1200/1500 from TIA V13.1 SP1 / V13 update 4. The K5.3.6.1 firmware you describe corresponds to STEP 7 V5.x projects using IM 154-8 PN or IM 154-3 PN. In that environment use the OPN DI + L DIB[AR1, P#0.0] pattern, or replace the dispatch with an ARRAY of UDT (Solution 1) which compiles on every SCL target.

What is the area byte for PEEK when reading from a DB?

Use area byte = 16#84 (decimal 132) for the DB area on S7-1200/1500. Inputs are 16#81, Outputs 16#82, Markers 16#83, Instance DBs 16#85. The values are documented in Siemens ID 109755052 and the TIA Portal help for PEEK/POKE intrinsics.

Is VariantPut symbolic or absolute?

The destination argument of VariantPut is a VARIANT; the actual symbol is bound at the call site. The whole point of using VARIANT is to keep the call site symbolic while passing a runtime-dispatchable pointer. From CPU firmware V2.5 on S7-1500, this gives full type safety alongside runtime dispatch.

Which pattern keeps the smallest impact on the HMI / WinCC tag structure?

The ARRAY-of-UDT pattern (Solution 1) shifts each downstream tag from DB<i>,DB_VAR to DB_Recipes,Items[<i>],DB_VAR. To minimize HMI rework, keep Solution 1 collapsed into a single DB so only one DB number changes. If HMI/WinCC tags are bound by absolute DB number (rare with current WinCC V16/V17), use Solution 2 (PEEK/POKE) and have one tag per recipe index with an HMI-side calculation.

Back to blog