TIA Portal Indirect DB Word Addressing Using FC and Pointers

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

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 type Array[0..499] of Int or a flat STRUCT of Int fields. 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.

Optimized block access: If DB1 is created as "optimized" (the default in TIA Portal), symbolic names are used and 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");
Type-strictness caveat: The 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 returns WORD, and a direct comparison with INT raises 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

  1. 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_SIZE as a guard.
  2. Uninitialized DB: A fresh DB contains 0. Treat 0 as "empty slot" and skip it to cut scan time.
  3. Endianness: PEEK_WORD returns the same byte order the DBW uses. With classic (non-optimized) DBs there is no byte-swap. Optimized DBs of type INT are stored little-endian on S7-1500 - identical to PEEK_WORD output.
  4. 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.
  5. Know-how protection: A know-how-protected DB hides its structure from the compiler. PEEK still works, but symbolic DB1.DBW[i] does not.
  6. Multi-instance DBs: Pass the DB number dynamically. PEEK accepts an INT for dbNumber; the DB must be loaded in the CPU's DB area.
Do not confuse byte offset with bit offset: PEEK_WORD wants a byte offset (0, 2, 4, ...). PEEK_BOOL wants a byte.bit offset. Mixing them up is the single most common bug in indirect-addressing scans and produces cryptic "Value out of range" runtime errors.

Verification And Commissioning

  1. 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.
  2. Online test with a watch table. Force DB1.DBW0 = 100, DB1.DBW10 = 200, DB1.DBW498 = 100. After one OB1 cycle, observe tagCmdStartRow cycling through 0 then 249.
  3. Boundary test. Force the code at DB1.DBW998 (last legal offset) and confirm the scan still reports the match. Then force the code at DB1.DBW1000 (out of range) and confirm bFound = FALSE and no CPU STOP.
  4. Trace. Use the S7-1500 Trace to record tagCmdStartRow and OB1 cycle time. A scan over 500 words must add < 1 ms of OB1 time on an S7-1511.
  5. CPU diagnostic buffer. Any access error (e.g. wrong DB number) is logged as event ID 16#3582 with 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.

Back to blog