S7-1500 SCL: Compare DInt Arrays Across Three Data Blocks

David Krause15 min read
S7-1200SiemensTutorial / 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

1. Problem Definition and Functional Goal

On a SIMATIC S7-1500 controller, three global data blocks are present:

  • DB1ARRAY[0..N1] of DINT (source indices)
  • DB2ARRAY[0..N2] of DINT (search values)
  • DB3ARRAY[0..N2] of DINT (result indices)

For every element DB1[i], the FB must scan the entire DB2. When DB1[i] = DB2[j], the index i is written to DB3[j]. Positions in DB2 that have no match must keep a sentinel value (commonly -1 or the previous content). The DB lengths must be configurable from the caller so the same FB works across projects of different scale.

The runtime contract is:

  • Result is deterministic: each DB2[j] receives at most one value.
  • If the same value appears in DB1 multiple times, only the last matching index survives (overwrite semantics).
  • If DB2[j] matches multiple entries in DB1, only the lowest index is preserved unless the iteration direction is reversed in the FB.

The same logical problem can be expressed with the structured compare instructions CompType — Compare tag structured data types (S7-1500) and EQ_TypeOfDB — Compare data type of an indirectly addressed DB for EQUAL with a data type (S7-1500). Those instructions answer a different question (data-type equality, not value equality across arrays) and are not a substitute for the loop logic described below; they are mentioned only because they appear in the Siemens instruction set for S7-1500 STL comparators and are sometimes confused with this use case.

2. Why SCL, Not STL, on S7-1500

Field note: On S7-1500, SCL is the only first-class language for symbolic, optimized, and structured array access. STL still compiles, but the address-register model (AR1, AR2) and the load/transfer semantics are inherited from S7-300/400 and interact badly with optimized block access. Use SCL for any non-trivial array logic on S7-1500.

Reasons to select SCL over STL for this task:

  • Symbolic array indexing with ARR[i] eliminates the pointer arithmetic that breaks optimized blocks.
  • FOR / WHILE loops with proper loop-variable scope keep the index logic in one readable block.
  • VARIANT IO allows DB-agnostic parameter passing (the same FB instance works against any DB whose symbol resolves to ARRAY OF DINT).
  • Optimized block access (S7_Optimized_Access := TRUE) requires symbolic, type-safe access — STL's absolute addressing triggers consistency warnings and disables some HMI/integration features.
  • Watch tables and trace show full symbol names instead of DBD[AR1,P#0.0] when symbolic access is used.

If STL is mandated by site standard, the FB can be wrapped, but the inner logic still benefits from SCL. Mixing the two in a single FB is supported but discouraged because debugging becomes painful.

3. Prerequisites

Confirm the engineering environment before writing code.

Item Minimum / Recommended
TIA Portal V17 (for ARRAY[*] of DINT support), recommended V18 or V19; tested against V20 documentation set
S7-1500 CPU firmware V2.5 or higher (SCL FOR loops over ARRAY[*] require ≥ V2.0; VARIANT IO works on ≥ V1.8)
DB block attribute Optimized block access enabled on all three DBs (S7_Optimized_Access := TRUE)
DB retention Non-retentive for DB3 (result block is overwritten each cycle)
Libraries Optional: Siemens LGF (Library of General Functions) for validated list helpers; optional: Oscat for buffer / list / find blocks
PLCSIM (commissioning) PLCSIM Advanced or PLCSIM V17+ for offline test of full array loops

The three DBs share the same element type but can carry different logical lengths. Declare arrays with upper bounds large enough for worst case and use a LENGTH tag to limit iteration. Avoid runtime resizing of the array itself.

4. Reference SCL Implementation (Direct Array IO)

This implementation is the recommended starting point. It uses fixed-size arrays and symbolic access. It compiles on TIA V17+ and runs without any external library.

4.1 FB Declaration

FUNCTION_BLOCK "FB_DBArrayMatcher"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      i_Enable        : Bool;        // Run once per TRUE transition
      i_LengthDB1     : Int;         // Logical elements in DB1 (0..N1)
      i_LengthDB2     : Int;         // Logical elements in DB2 (0..N2)
      i_DB1           : Variant;     // Source array      (DB1)
      i_DB2           : Variant;     // Search array      (DB2)
   END_VAR

   VAR_OUTPUT
      o_DB3           : Variant;     // Result array      (DB3)
      o_MatchCount    : Int;         // Total matches written
      o_Status        : Word;        // 16#0000 = OK, see table below
      o_LastDB1Index  : Int;         // Last DB1 index that produced a write
      o_LastDB2Index  : Int;         // Last DB2 index written
   END_VAR

   VAR
      s_RunActive     : Bool;
      s_i             : Int;
      s_j             : Int;
      s_NoMatch       : DInt := -1;  // Sentinel for unmatched slots
   END_VAR

   VAR CONSTANT
      c_STATUS_OK              : Word := 16#0000;
      c_STATUS_NOT_ENABLED     : Word := 16#0001;
      c_STATUS_TYPE_MISMATCH   : Word := 16#8001;
      c_STATUS_LENGTH_INVALID  : Word := 16#8002;
      c_STATUS_DB_MISSING      : Word := 16#8003;
   END_VAR
END_FUNCTION_BLOCK

4.2 FB Body (SCL)

// ---------------------------------------------------------------
// FB_DBArrayMatcher — main body
// Compares ARRAY[*] OF DInt of i_DB1 against i_DB2; writes the
// matched DB1 index into the matching slot of o_DB3.
// ---------------------------------------------------------------

IF NOT i_Enable THEN
    s_RunActive := FALSE;
    o_Status    := c_STATUS_NOT_ENABLED;
    RETURN;
END_IF;

// --- type / length validation ---------------------------------
IF TypeOf(i_DB1) <> TypeOf(i_DB2) OR TypeOf(i_DB1) <> TypeOf(o_DB3) THEN
    o_Status := c_STATUS_TYPE_MISMATCH;
    RETURN;
END_IF;

IF i_LengthDB1 < 0 OR i_LengthDB2 < 0 THEN
    o_Status := c_STATUS_LENGTH_INVALID;
    RETURN;
END_IF;

// --- symbolic access through AT view --------------------------
{IF defined(i_DB1)}
  VAR_TEMP
      p_DB1 : POINTER TO ARRAY[*] OF DInt;
      p_DB2 : POINTER TO ARRAY[*] OF DInt;
      p_DB3 : POINTER TO ARRAY[*] OF DInt;
  END_VAR

  p_DB1 := i_DB1;
  p_DB2 := i_DB2;
  p_DB3 := o_DB3;

  // Initialise result with sentinel
  FOR s_j := 0 TO i_LengthDB2 - 1 DO
      p_DB3^[s_j] := s_NoMatch;
  END_FOR;

  o_MatchCount   := 0;
  o_LastDB1Index := -1;
  o_LastDB2Index := -1;

  // Nested loop — write index of first DB1 match into DB3[j]
  FOR s_i := 0 TO i_LengthDB1 - 1 DO
      FOR s_j := 0 TO i_LengthDB2 - 1 DO
          IF p_DB1^[s_i] = p_DB2^[s_j] THEN
              p_DB3^[s_j] := s_i;
              o_MatchCount   := o_MatchCount + 1;
              o_LastDB1Index := s_i;
              o_LastDB2Index := s_j;
              // For "first match wins" semantics keep the inner loop;
              // remove EXIT for "last match wins" semantics.
              EXIT;
          END_IF;
      END_FOR;
  END_FOR;

  o_Status := c_STATUS_OK;
{END_IF}

Key design choices in the code above:

  • Sentinel s_NoMatch = -1 — explicitly marks slots that never matched. Avoids reading stale data from a previous cycle.
  • First-match-wins via EXIT — prevents one DB2 slot from being overwritten by every equal DB1 entry.
  • VARIANT IO with POINTER TO ARRAY[*] OF DInt — decouples the FB from any specific DB number; the caller passes the symbol or absolute DB reference.
  • Status word — machine-readable diagnostic instead of a single BOOL.

4.3 Calling the FB

// In OB1 or a higher-level FB
"DB_Instance_DBArrayMatcher"(i_Enable    := TRUE,
                              i_LengthDB1 := "DB_Source".LengthUsed,
                              i_LengthDB2 := "DB_Search".LengthUsed,
                              i_DB1       := "DB_Source".Values,
                              i_DB2       := "DB_Search".Values,
                              o_DB3       := "DB_Result".Indices);

The symbol of an ARRAY[*] OF DINT can be passed directly to a VARIANT input. The runtime resolves the DB number and offset; the FB does not need to know the DB number.

5. Alternative SCL Implementation (Slice Access, No VARIANT)

Where the project has a hard-coded array size and the FB does not need to be portable across data blocks, the simpler slice-based approach is easier to commission and faster to compile. It also avoids the TypeOf() check overhead.

FUNCTION_BLOCK "FB_ArrayMatcherFixed"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      i_Enable    : Bool;
      i_DB1       : ARRAY[0..999] OF DInt;   // adjust to max
      i_DB2       : ARRAY[0..999] OF DInt;
      i_LenDB1    : Int;
      i_LenDB2    : Int;
   END_VAR
   VAR_OUTPUT
      o_DB3       : ARRAY[0..999] OF DInt;
      o_Matches   : Int;
      o_Busy      : Bool;
   END_VAR
   VAR
      i : Int; j : Int; k : Int;
   END_VAR
BEGIN
   o_Busy := TRUE;
   // Clear result
   FOR k := 0 TO i_LenDB2 - 1 DO
       o_DB3[k] := -1;
   END_FOR;
   o_Matches := 0;

   FOR i := 0 TO i_LenDB1 - 1 DO
       FOR j := 0 TO i_LenDB2 - 1 DO
           IF i_DB1[i] = i_DB2[j] THEN
               o_DB3[j]  := i;
               o_Matches := o_Matches + 1;
               EXIT;       // first match wins
           END_IF;
       END_FOR;
   END_FOR;
   o_Busy := FALSE;
END_FUNCTION_BLOCK

This variant is recommended when:

  • The maximum array size is known at code-generation time and fits in work memory (each DInt = 4 B; 1000 elements ≈ 4 KB per array).
  • Commissioning speed is more important than portability.
  • Static analysis tools must see concrete bounds for SIL verification.

6. STL Reference (and Why It Is Discouraged)

The original STL draft shown in the source has three structural defects that appear only at runtime:

  1. AR1 is never advanced when the comparison is not equal. After +AR2 P#4.0 the inner register increments correctly, but LAR1 keeps pointing at P#0.0 of DB1 for every iteration, so the FB effectively compares DB1[0] against the whole DB2 instead of DB1[i].
  2. The sentinel write uses LAR2 with no offset, so it writes #DB3 at offset P#0.0 every time — overwriting the same DWord regardless of j.
  3. The end-of-loop jump skips the write entirely when no match is found, leaving stale data in DB3.

A correct STL translation is possible but uglier and not maintainable. Below is the equivalent STL for reference only:

      LAR1  P#0.0                      // address register for DB1
      OPN   "DB_Source"               // open DB1
      LAR2  P#0.0                      // address register for DB2
      OPN   "DB_Search"               // open DB2

      L     #i_LenDB2                  // outer loop counter
next1:T     #cnt1
      L     DBD [ AR2 , P#0.0 ]        // DB2[j]
      L     DBD [ AR1 , P#0.0 ]        // DB1[i]
      ==D
      JC    match
      +AR2  P#4.0                      // advance DB2 pointer
      L     #cnt1
      LOOP  next1
      JU    skip                       // no match, leave -1
match:NOP  0
      L     DBD [ AR1 , P#0.0 ]        // write DB1[i] into DB3[j] via temp
      T     #tempMatch
      OPN   "DB_Result"
      LAR2  P#0.0
      L     #cnt1
      L     #i_LenDB2
      -I
      SLD   3                          // multiply by 8 (bit offset for DWord)
      +AR2                              // AR2 now points at result[j]
      L     #tempMatch
      T DBD [ AR2 , P#0.0 ]

skip: NOP  0
      +AR1  P#4.0                      // advance DB1 pointer
      // reset AR2 for next outer pass
      LAR2  P#0.0
      OPN   "DB_Search"

Even this version is brittle because:

  • It depends on the DB numbers being literal, which blocks optimization.
  • The address registers cannot be inspected symbolically in trace.
  • It is incompatible with S7_Optimized_Access := TRUE on the DBs.
Use STL only as a last resort. The maintenance cost of rewriting this block whenever a DB is re-numbered or optimized usually exceeds any perceived performance gain on S7-1500.

7. Optimized DB Configuration — What to Check

The three DBs must be configured identically for the FB to behave predictably.

DB attribute Required value Why
Optimized block access Enabled Allows symbolic, type-safe access; required for POINTER TO ARRAY[*] AT-view
Accessible from HMI/OPC UA Enabled on DB1, DB2; optional on DB3 Permits external visualization of source data
Retain / remanence Non-retain on DB3 DB3 is overwritten every cycle; persistence wastes load memory
Array bounds Identical upper bound for DB2 and DB3 Out-of-range writes cause CPU SF LED + STOP with diagnostic buffer entry "Area length error"
Download without re-init Use carefully Changing array element type forces a re-init that erases current DB3 content

8. Library Re-use: Siemens LGF and Oscat

If the project already loads either of these libraries, leverage the existing helpers before writing a new FB.

8.1 Siemens LGF (Library of General Functions)

LGF provides validated list-processing blocks (search, insert, delete, sort) that operate on ARRAY[*]. The FB LGF_FindElementInArray (shipped with TIA Portal as part of the GlobalFunctions library) returns the index of the first match for a single value. Wrapping it in an outer loop gives the same result as the code in section 4 but inherits Siemens' validation, documentation, and version traceability. Reference: search the TIA Portal "LGF" library under Libraries → Global Libraries → LGF.

8.2 Oscat

Oscat (open-source, network-published) provides a richer set of buffer and list FB. The blocks of interest are BUFFER, LIST, and DECOMPOSE. They work on BYTE streams rather than DINT arrays, so an adapter wrapper that converts DINT → BYTE[4] is needed. The advantage is built-in support for duplicate handling, FIFO/LIFO modes, and configurable match strategies that the hand-written FB in section 4 does not provide.

9. Performance and Cycle-Time Impact

Complexity of the nested loop is O(N1 × N2). For typical values:

N1 × N2 Indicative scan impact on S7-1511-1 PN Indicative scan impact on S7-1518-4 PN/DP
100 × 100 (10 k comparisons) < 1 ms < 0.1 ms
1 000 × 1 000 (1 M comparisons) ~ 6 – 10 ms ~ 1 – 2 ms
10 000 × 10 000 (100 M comparisons) ~ 80 – 120 ms (exceeds OB1 budget) ~ 15 – 25 ms (still tight)
Rule of thumb: if N1 × N2 > 5 000 000, split the work across multiple OB1 cycles (use i_Enable with a state machine), or pre-index one of the arrays into a hash/dictionary structure (not natively supported in SCL — requires C/C++ via PLCSIM ODK or a sorted binary-search variant).

Optimization levers, in order of impact:

  1. Sort DB2 ascending and switch inner loop to binary search. Reduces cost from O(N2) to O(log N2).
  2. Use WORD or INT instead of DINT if the value range fits — halves memory bandwidth and improves cache locality.
  3. Move the inner loop body into a separate FB with S7_Optimized_Access — sometimes TIA generates better code with the loop factored out.
  4. Disable o_MatchCount accumulation if not needed; the increment touches a global accumulator that serializes writes on multi-CPU racks.

10. Commissioning and Verification

  1. Build a test OB that calls the FB with a small synthetic dataset (5 × 5 elements) and sets i_Enable := TRUE.
  2. Online → Monitor: open DB_Source, DB_Search, and DB_Result side by side. Confirm every matching slot in DB_Result contains the correct DB1 index.
  3. Force sentinel values into DB_Search at well-known positions and confirm DB_Result keeps -1 at those positions.
  4. Duplicate test: place the same value at DB1[2] and DB1[5]. Verify DB_Result holds index 2 (first-match-wins) at the corresponding DB2 slot.
  5. Boundary test: set i_LengthDB1 := 0. Confirm o_Status := 16#8002 and the FB returns immediately.
  6. Trace: use TIA Trace to capture DB_Result writes over one OB1 cycle. Expected waveform: staircase of single writes, one per matching pair.
  7. CPU diagnostic buffer: search for entries containing Area length error or Pointer does not point to valid data. Their presence indicates a bounds violation or an uninitialized VARIANT.

11. Status Word Reference

Status (hex) Meaning Operator action
16#0000 OK — run completed normally None
16#0001 Not enabled Check i_Enable interlock
16#8001 VARIANT types do not match across DB1/DB2/DB3 Confirm array element types are identical DINT
16#8002 Length input negative or zero Clamp caller-side; reject negative lengths
16#8003 DB not loaded / variant not initialised Check DBs are downloaded and not deleted

12. Troubleshooting Matrix

Symptom Likely cause Fix
CPU goes to STOP with SF LED on first run DB2 and DB3 array bounds differ; write goes past DB3 end Resize DB3 to match DB2 upper bound
DB3 stays at 0 Sentinel -1 overwritten by an FB that does not initialize result Add explicit result-clear loop at FB start
Wrong indices written Loop bound i_LenDB1 / i_LenDB2 off by one Use i_Length - 1 in the FOR upper bound (SCL semantics)
Compile error: type incompatibility DB1 declared ARRAY OF INT while FB expects DINT Align element types across all three DBs
VARIANT input shows "---" online Caller passed a scalar instead of an array slice Pass "DB_Source".Values, not the whole DB
OB1 cycle time spikes Nested loop runs every cycle on large arrays Wrap call in cyclic scheduler or sort+binsearch
Watch table shows wrong value Mix of optimized and non-optimized DBs confuses symbolic access Enable optimized access on all three DBs

13. Edge Cases

  • Empty array: i_LengthDB1 = 0 → outer loop does not execute; result is all -1. Status remains OK.
  • All elements equal: every DB2 slot receives 0. o_MatchCount equals i_LengthDB2.
  • DB1 contains values not present in DB2: those DB1 entries produce no write; DB3 stays at -1 in those slots.
  • Negative DB1 indices as values: allowed; DINT comparison is signed. Make sure the sentinel -1 is reserved and documented, or use LWORD#16#FFFF_FFFF_FFFF_FFFF if DInt range overlaps.
  • Calling from a higher-priority OB: the FB is non-reentrant. Mark Multi-instance capable and instantiate per OB to avoid static collisions.

14. Migration Path from S7-300/400 STL

Plants that still carry STL blocks from S7-300/400 can migrate without touching the calling code:

  1. Wrap the existing STL FB inside a thin SCL wrapper that exposes a VARIANT IO.
  2. Move the data blocks to optimized access one at a time. Run a parallel-instance A/B test for one shift.
  3. After three shifts of stable operation, retire the STL FB and replace with the SCL version from section 4.

This staged migration is important because converting STL → SCL in a single change often surfaces latent bugs in the original STL logic (such as the address-register defects listed in section 6) that would otherwise be hidden behind a passing acceptance test.

15. Frequently Asked Questions

Why is SCL recommended over STL on S7-1500 for array comparison?

SCL supports symbolic array indexing (ARR[i]), VARIANT IO for DB-agnostic blocks, and clean FOR loops. STL's address-register model (AR1/AR2) interacts poorly with optimized block access and makes the code nearly impossible to maintain. SCL compiles to the same machine code as equivalent STL on S7-1500.

Can this FB work with non-optimized (absolute-address) DBs?

Yes, but the FB must drop the POINTER TO ARRAY[*] AT-view and pass array slices directly. Performance drops slightly because the compiler can no longer assume contiguous symbolic layout, and watch tables show DBD[AR1,P#0.0] style offsets.

How large can the arrays be on a single FB call?

Limited by work memory and OB1 cycle time. Each DINT element uses 4 bytes, so a 10 000-element array is ~ 40 KB per block. The scan-time cost is O(N1 × N2); above roughly five million comparisons, split the work across cycles.

Does the FB work with ARRAY OF STRUCT instead of DINT?

Yes, but replace the inner = with a structured compare. The CompType instruction family handles tag-level equality of structured types. The loop body otherwise stays the same.

How do I keep multiple matches for the same DB2 value?

Remove the EXIT in the inner loop. Each DB1 match will then overwrite DB3[j]. To retain all of them, change DB3 to an array of vectors (e.g., ARRAY[0..N2, 0..7] of DINT) and append in a second loop.

What CPU firmware is required for ARRAY[*] OF DINT and VARIANT?

S7-1500 CPUs from firmware V2.0 onward support ARRAY[*]; VARIANT IO is supported from V1.8. TIA Portal V17 or higher is required to declare the syntax without workarounds.

Back to blog