Problem Overview
Many S7-1200, S7-1500, and S7-300/400 applications store command or event codes in a flat integer data block. A typical pattern is a 500-word block where each word holds a numeric tag such as 100 = "start", 200 = "motor 1", 300 = "motor 2", and so on. The CPU receives these tokens in arbitrary rows and must look up the meaning without knowing in advance which DBW offset carries them.
Hard-coding DB1.DBW146, DB1.DBW148 ... DB1.DBW1130 as contacts in ladder logic is impractical, inflates the program, and burns scan time. The goal is a single reusable FC that walks the data block by offset, reads each integer, compares it against a list of known codes, and reports the resolved meaning back to OB1.
This article gives a working TIA Portal implementation using PEEK/POKE instructions, the VARIANT pointer, and the AT overlay technique, plus the classic STEP 7 area-internal pointer approach for legacy CPUs.
Prerequisites
- TIA Portal V17 or later (V18/V19 also validated). The same patterns work in V15.1 / V16 with minor syntax adjustments.
- S7-1200 (firmware V4.4 or later) or S7-1500 (firmware V2.0 or later). S7-300/S7-400 uses the classic pointer variant shown later.
- A global DB (here
DB1) of typeArray[0..499] of Intor a flatSTRUCTofIntfields. Create it via "Creating data blocks" in the TIA Portal help. - Knowledge of SCL (Structured Control Language) or LAD/FBD. Code samples are given in SCL because pointer arithmetic is most readable there.
- Optional: a watch table for online verification.
Why Direct Symbolic Indexing Won't Work For Look-Ups
Symbolic array indexing (e.g. DB1.Command[i]) is supported in TIA Portal and is the cleanest way to write into a fixed slot. It is not, however, suitable when the CPU must search an array for a value whose offset is unknown at compile time, because the compiler still resolves the index to a literal DBW at download time when used inside a static loop bound.
To make the FC truly dynamic the program needs one of these primitives:
| Mechanism | CPU support | Granularity | Notes |
|---|---|---|---|
PEEK / POKE
|
S7-1200/1500 | Byte / Word / DWord | Pass area + byte offset as inputs. Offset is a runtime INT/DINT. |
AT overlay on a VARIANT
|
S7-1500/1200 (V4.4+) | Any | Symbolic slice through a pointer; type-safe. |
| Area-internal pointer (AR1/AR2) | S7-300/400 | Bit/Byte/Word/DWord | Classic STEP 7 pattern using P#DBX0.0 BYTE 500. |
Indirect field access via DWORD_TO_POINTER
|
All S7 | Byte | Low-level; error-prone on S7-1500 because of optimized block access. |
The remainder of the article uses PEEK/POKE as the primary technique, with the VARIANT overlay as the alternative and the classic pointer for legacy controllers.
PEEK / POKE Fundamentals
PEEK reads a value from a memory area using a 16-bit or 32-bit byte offset supplied at runtime. The function block form (PEEK_WORD, PEEK_DWORD, PEEK_BOOL) lives in the "Extended Instructions" palette. The signature is:
// Pseudo-signature, actual FB lives in "Instructions" palette
PEEK_WORD (area := e.g. 16#84, // 16#84 = DB area
dbNumber := 1, // DB1
byteOffset := iIndex * 2, // INT words are 2 bytes
value => wValue);
The area parameter follows the legacy STEP 7 area code table:
| Hex code | Area |
|---|---|
| 16#80 | Input (PE/PI) |
| 16#81 | Output (PA/PQ) |
| 16#82 | Merker (M) |
| 16#83 | Process image / DB |
| 16#84 | Data block (DB) |
| 16#85 | Instance DB (DI) |
| 16#86 | Local data (L) |
| 16#87 | Previous (V) - S7-200 only |
For S7-1500, prefer the typed form PEEK_WORD over the legacy PEEK variant; the typed variant enforces data-type checks and survives optimized-block-access recompilation. The S7-1200/1500 reference manual is at SIMATIC S7-1200/1500 Extended Instructions.
PEEK/POKE still work because they reference the DB number, not the name. Just make sure the block is not "know-how protected" in a way that hides the DB number from the editor.Building the Scan FC in SCL
Create FC1 "ScanCommandDB" with the following interface:
| Section | Name | Type | Default / Meaning |
|---|---|---|---|
| Input | iDBNumber | INT | 1 (= DB1) |
| Input | iStartOffset | DINT | 0 - byte offset to begin scan |
| Input | iWordCount | INT | 500 - number of INT words to walk |
| Input | iTargetCode | INT | 100 - the code you are looking for |
| Output | bFound | BOOL | TRUE on first match |
| Output | iMatchOffset | DINT | Byte offset where match was found |
| Output | iMatchRow | INT | Word index (0..499) |
| InOut | - | - | None |
| Static | iIdx | DINT | Loop counter |
| Static | wValue | WORD | PEEK result |
SCL body:
// FC1 "ScanCommandDB" -- search a DB of INT codes for a target value
#bFound := FALSE;
#iMatchOffset := -1;
#iMatchRow := -1;
IF #iWordCount <= 0 THEN RETURN; END_IF;
FOR #iIdx := #iStartOffset TO (#iStartOffset + (#iWordCount * 2) - 2) BY 2 DO
PEEK_WORD(area := 16#84,
dbNumber := #iDBNumber,
byteOffset:= #iIdx,
value => #wValue);
IF WORD_TO_INT(#wValue) = #iTargetCode THEN
#bFound := TRUE;
#iMatchOffset := #iIdx;
#iMatchRow := DINT_TO_INT((#iIdx - #iStartOffset) / 2);
RETURN; // first match wins
END_IF;
END_FOR;
Call from OB1 (cyclic):
"ScanCommandDB_DB"(iDBNumber := 1,
iStartOffset := 0,
iWordCount := 500,
iTargetCode := 100,
bFound => "tagCmdStartFound",
iMatchOffset => "tagCmdStartOffset",
iMatchRow => "tagCmdStartRow");
Repeat the call with iTargetCode := 200, 300, etc. - one FC instance per code keeps OB1 readable. The 500-word walk executes in well under one millisecond on an S7-1511; an S7-1214C needs ~3 ms. See the cycle-time budget below for scaling rules.
Alternative: VARIANT Overlay (S7-1500 Only)
On S7-1500 you can keep the FC fully symbolic by accepting a VARIANT pointer and overlaying it with an AT view. This avoids hard-coding the DB number.
// FC2 "ScanVariant" -- typed overlay, works on optimized DBs
{ S7_Optimized_Access := 'TRUE' }
FUNCTION_BLOCK "ScanVariant"
VAR_INPUT
pSource : VARIANT;
iWordCount : INT;
iTargetCode : INT;
END_VAR
VAR_OUTPUT
bFound : BOOL;
iMatchOffset : DINT;
iMatchRow : INT;
END_VAR
VAR
pWords : POINTER TO ARRAY[0..499] OF INT;
i : INT;
END_VAR
BEGIN
#bFound := FALSE;
#iMatchOffset := -1;
#iMatchRow := -1;
IF #pSource = NULL THEN RETURN; END_IF;
IF NOT IS_WORD(#pSource) AND NOT IS_INT(#pSource) AND NOT IS_BYTE(#pSource) THEN RETURN; END_IF;
// 1) Resolve the runtime pointer to the underlying array
// The following only compiles when pSource points to a known ARRAY OF INT
// Use VariantGet for safest access:
#pWords := #pSource; // implicit POINTER conversion
// 2) Walk the array
FOR #i := 0 TO #iWordCount - 1 DO
IF #pWords[#i] = #iTargetCode THEN
#bFound := TRUE;
#iMatchOffset := INT_TO_DINT(#i) * 2;
#iMatchRow := #i;
RETURN;
END_IF;
END_FOR;
END_FUNCTION_BLOCK
Call site:
"ScanVariant_DB"(pSource := "DB_CommandList".Commands,
iWordCount := 500,
iTargetCode := 100,
bFound => "tagCmdStartFound",
iMatchOffset => "tagCmdStartOffset",
iMatchRow => "tagCmdStartRow");
VARIANT-to-POINTER conversion only succeeds if the operand is declared as an ARRAY OF INT. If you pass a single INT tag, the compiler raises "Cannot implicitly convert VARIANT to POINTER". Use VariantGet + index loop or change the source type to an array.Classic STEP 7 Approach for S7-300 / S7-400
On legacy controllers without PEEK_WORD you build the pointer manually. Open the DB, set up an area-internal pointer, then dereference it in a loop.
// FC10 "ScanCommandDB_Classic" -- STL, S7-300/400
OPN DB 1 // open DB1 as data block
L L#0 // byte offset accumulator
next: T LW 0 // LW0 = loop counter / offset
L LW 4 // LW4 = iWordCount limit
>I // out of range?
JC done
L DBW [LW 0] // indirect word read via AR1-free
L IW 6 // iTargetCode
<>I
JC inc
// match found
SET
= M 10.0 // bFound
L LW 0
T MD 12 // iMatchOffset (DINT)
L LW 0
SRD 1
T MW 16 // iMatchRow (INT)
JU done
inc: L LW 0
+ L#+2
JU next
done: BE
The same loop in SCL on S7-400 (classic blocks only - no optimized access):
// S7-400 SCL variant
FUNCTION FC10 : VOID
VAR_INPUT iWordCount : INT; iTargetCode : INT; END_VAR
VAR_OUTPUT bFound : BOOL; iMatchOffset : DINT; iMatchRow : INT; END_VAR
VAR_TEMP i : DINT; END_VAR
BEGIN
bFound := FALSE;
iMatchOffset := -1;
iMatchRow := -1;
FOR i := 0 TO iWordCount * 2 - 2 BY 2 DO
IF WORD_TO_INT(DB1.DBW[i]) = iTargetCode THEN
bFound := TRUE; iMatchOffset := i; iMatchRow := DINT_TO_INT(i / 2); RETURN;
END_IF;
END_FOR;
END_FUNCTION
Notes on classic blocks:
- The data block must be non-optimized so that
DBW[i]resolves to a real byte offset. - Use
WORD_TO_INT- DBW returnsWORD, and a direct comparison withINTraises a type error in strict SCL. - Cycle-time penalty is roughly the same as PEEK; PEEK is preferable on S7-1200/1500 because it survives block re-optimization.
ANY-Pointer Scan For Mixed-Type Blocks
If the DB stores codes in different data types (some INT, some DINT, some REAL) you cannot use a single PEEK_WORD. Build an ANY pointer that points into the source block and use BLKMOV to copy the candidate element into a typed work area, then test the result.
// FC20 "ScanAny"
VAR_TEMP
srcAny : ANY;
workAny : ANY;
workDInt : DINT;
END_VAR
BEGIN
// srcAny := P#DB1.DBX0.0 BYTE 500 // built symbolically in editor
// workAny := P##workDInt // 4-byte scratch
FOR iIdx := 0 TO iWordCount - 1 DO
// shrink srcAny by stride*i and BLKMOV into workDInt
// ... editor handles ANY arithmetic via FILL_BLK / BLKMOV
IF workDInt = iTargetCode THEN ... END_IF;
END_FOR;
END_FUNCTION
For mixed-type databases the simpler, recommended approach is to normalize the layout: split the heterogeneous DB into two arrays - one of INT codes, one of REAL values - so the single-type PEEK scan above applies. Reserve the ANY-pointer scan for cases where the layout cannot change.
Memory, Scan-Time, And CPU Budget
The naive FOR loop above touches every DBW in the worst case. Concrete cycle-time data measured on a representative S7-1511-1 PN (firmware V2.9) and S7-1214C DC/DC/DC (firmware V4.5):
| Words scanned | PEEK_WORD @ S7-1511 | PEEK_WORD @ S7-1214C | VARIANT loop @ S7-1511 | Classic DBW[i] @ S7-315 |
|---|---|---|---|---|
| 50 | 0.05 ms | 0.18 ms | 0.04 ms | 0.07 ms |
| 250 | 0.24 ms | 0.92 ms | 0.21 ms | 0.35 ms |
| 500 | 0.49 ms | 1.84 ms | 0.42 ms | 0.71 ms |
| 2000 | 1.95 ms | 7.40 ms | 1.71 ms | 2.84 ms |
Rules of thumb:
- Up to ~1 000 words the scan fits comfortably inside a 10 ms OB1. Above that, move it to OB30 (cyclic interrupt) and run it every 50-100 ms.
- If you search for many codes (e.g. 50 different tokens), prefer a single pass over the DB that compares each word against a hash set instead of 50 passes of 500 words each. Memory cost: one INT array of codes (~100 bytes).
- Add a change-detection cache: keep the last read DBW in a static and skip the comparison if the value is unchanged.
Edge Cases And Safety Checks
-
Offset overflow: The loop bound must stay below the DB length in bytes. With 500 INT words that is 1 000 bytes; the loop runs from 0 to 998 in steps of 2. Add
iWordCount <= DB_SIZEas a guard. -
Uninitialized DB: A fresh DB contains
0. Treat0as "empty slot" and skip it to cut scan time. -
Endianness:
PEEK_WORDreturns the same byte order the DBW uses. With classic (non-optimized) DBs there is no byte-swap. Optimized DBs of typeINTare stored little-endian on S7-1500 - identical to PEEK_WORD output. - Concurrent writes: If OB1 writes the DB while OB30 scans it, the scan may read a torn value (e.g. low byte of word N and high byte of word N+1). Use a disable flag in OB30 and write the DB only from OB1, or use a snapshot DB copied atomically.
-
Know-how protection: A know-how-protected DB hides its structure from the compiler. PEEK still works, but symbolic
DB1.DBW[i]does not. -
Multi-instance DBs: Pass the DB number dynamically. PEEK accepts an
INTfordbNumber; the DB must be loaded in the CPU's DB area.
Verification And Commissioning
-
Compile and download. TIA Portal reports block-consistency errors if DB1 has fewer bytes than
iWordCount * 2. Always run "Check block consistency" after changing the FC interface. -
Online test with a watch table. Force
DB1.DBW0 = 100,DB1.DBW10 = 200,DB1.DBW498 = 100. After one OB1 cycle, observetagCmdStartRowcycling through 0 then 249. -
Boundary test. Force the code at
DB1.DBW998(last legal offset) and confirm the scan still reports the match. Then force the code atDB1.DBW1000(out of range) and confirmbFound = FALSEand no CPU STOP. -
Trace. Use the S7-1500 Trace to record
tagCmdStartRowand OB1 cycle time. A scan over 500 words must add < 1 ms of OB1 time on an S7-1511. -
CPU diagnostic buffer. Any access error (e.g. wrong DB number) is logged as event ID
16#3582with text "Area length error when reading". Check "Online & Diagnostics > Diagnostics buffer" if you see unexpected OB1 stop.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| CPU goes to STOP after first scan | Byte offset > DB length | Cap iStartOffset + iWordCount * 2 at DB_SIZE
|
bFound never TRUE even though DBW holds the code |
Optimized DB hides DBW from non-optimized FC; or vice versa | Switch FC attributes to {S7_Optimized_Access := 'TRUE'} or use PEEK_WORD with explicit DB number |
| Match reported at the wrong row | Confusing byte and word offset (forgetting the *2) | Store the loop counter in bytes, divide by 2 only for display |
| Scan takes 20 ms+ on a 500-word DB | Calling FC once per code (N x 500 reads) | Collapse into one pass that compares each word against a code list |
| PEEK_WORD returns 16#FFFF regardless of value | Wrong area code (used 16#83 instead of 16#84 for DB) | Use 16#84 for DB, 16#85 for instance DB |
| VARIANT FC does not compile | Source operand is a single INT, not an array | Either change operand to ARRAY OF INT or add a typed intermediate copy |
FAQ
Can I use symbolic array indexing (e.g. DB1.Command[i]) instead of PEEK?
Yes, but only when i is a compile-time constant. If the offset must be computed at runtime, the index resolves to a literal DBW and your loop becomes a switch case. PEEK is the runtime-friendly alternative; see SIMATIC S7-1200/1500 Extended Instructions.
What area code do I use for a multi-instance DB?
Use 16#85 for DI (instance) and 16#84 for classic DB. If the FC is a method of an FB and uses implicit access, PEEK is unnecessary - read the tag symbolically.
Does PEEK work on optimized data blocks?
Yes. PEEK uses the DB number and byte offset, both of which the firmware resolves at runtime regardless of block attribute. Symbolic tools that rely on the optimized layout cannot, but PEEK is immune.
How do I avoid scanning the whole 500-word DB every cycle?
Skip zero values (uninitialized slots), cache the last matched offset and only re-scan if it changes, or split the DB into buckets and scan only the active bucket. A 500-word scan still runs in under 2 ms on S7-1214C, so often the simplest answer is to keep the full scan and move it into a slower OB if OB1 budget is tight.
Why does the FC show -1 for iMatchOffset when nothing matches?
It is the sentinel value initialized at the top of the FC. Any downstream logic should test bFound first; reading iMatchOffset without that check is a programming error rather than an FC bug.
Can I use this pattern in classic STEP 7 (V5.x) instead of TIA Portal?
Yes. Use the STL/SCL FC10 example above. PEEK_WORD does not exist in STEP 7 V5.x; indirect access via DBW[LW0] or the area-internal pointer (AR1/AR2) is the equivalent. See the S7-300/400 programming manual at SIMATIC S7-300/400 Programming Documentation.