1. Problem Definition and Functional Goal
On a SIMATIC S7-1500 controller, three global data blocks are present:
-
DB1 —
ARRAY[0..N1]ofDINT(source indices) -
DB2 —
ARRAY[0..N2]ofDINT(search values) -
DB3 —
ARRAY[0..N2]ofDINT(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
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:
-
AR1 is never advanced when the comparison is not equal. After
+AR2 P#4.0the inner register increments correctly, butLAR1keeps pointing atP#0.0of DB1 for every iteration, so the FB effectively comparesDB1[0]against the whole DB2 instead ofDB1[i]. -
The sentinel write uses
LAR2with no offset, so it writes#DB3at offsetP#0.0every time — overwriting the same DWord regardless ofj. - 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 := TRUEon the DBs.
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) |
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:
- Sort DB2 ascending and switch inner loop to binary search. Reduces cost from O(N2) to O(log N2).
-
Use
WORDorINTinstead ofDINTif the value range fits — halves memory bandwidth and improves cache locality. -
Move the inner loop body into a separate FB with
S7_Optimized_Access— sometimes TIA generates better code with the loop factored out. -
Disable
o_MatchCountaccumulation if not needed; the increment touches a global accumulator that serializes writes on multi-CPU racks.
10. Commissioning and Verification
-
Build a test OB that calls the FB with a small synthetic dataset (5 × 5 elements) and sets
i_Enable := TRUE. - 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.
-
Force sentinel values into DB_Search at well-known positions and confirm DB_Result keeps
-1at those positions. -
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. -
Boundary test: set
i_LengthDB1 := 0. Confirmo_Status := 16#8002and the FB returns immediately. - Trace: use TIA Trace to capture DB_Result writes over one OB1 cycle. Expected waveform: staircase of single writes, one per matching pair.
- 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_MatchCountequalsi_LengthDB2. -
DB1 contains values not present in DB2: those DB1 entries produce no write; DB3 stays at
-1in those slots. -
Negative DB1 indices as values: allowed;
DINTcomparison is signed. Make sure the sentinel-1is reserved and documented, or useLWORD#16#FFFF_FFFF_FFFF_FFFFifDIntrange 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:
- Wrap the existing STL FB inside a thin SCL wrapper that exposes a
VARIANTIO. - Move the data blocks to optimized access one at a time. Run a parallel-instance A/B test for one shift.
- 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.