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.
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_VARto 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:
- Make the container indirect, not the DB.
Use anARRAY OF UDTin a single DB. - Keep the DB indirect but switch the addressing to absolute.
Use thePEEK/POKEfamily of intrinsics. - Keep the DB indirect and use a self-describing handle.
UseVARIANTpointer dereferencing with type guards. - 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.
| 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_BOOLvariant overloads exist from TIA V15.1;PEEK_WORDand explicitPEEK_DWORDoverloads 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 | 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.
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
| 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.
| 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.) -
ARRAYorANYpattern. - S7-400 - same as S7-300;
VARIANTaccess 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.
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:
-
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). -
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. -
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].Task1Activedirectly. With PEEK/POKE, watch both the byte/bit offsets and the live signal atDB[dbNumber].DBX[byteOffset].X[bitOffset]in a separate watch row. -
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). -
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. -
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 atDB1,DB_VAR,DB2,DB_VAR, …, and re-target toDB_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. -
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
| 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:
- Can the storage live in a single DB? → ARRAY of UDT (Solution 1).
- Do external contracts require fixed DB numbers? → PEEK / POKE (Solution 2).
- Is type safety required with dispatchable DBs, and is the host S7-1500 with FW ≥ 2.5? → VARIANT (Solution 3).
- 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.