Problem Overview
This reference describes how to implement a deterministic value search inside a multi-region INT array using STL (Statement List / AWL) on STEP 7 V5.5 with an S7-300 CPU. The technique generalizes to any cross-record value lookup that an SCL FOR...EXIT...END_FOR resolves in one line, but is required when STL is mandated by code-style conventions or when a deterministic, cycle-budgeted memory scan is preferred over compiler-generated SCL.
The concrete scenario: a single data block (DB2) holds 20 models, each occupying a fixed 174-byte record. Within each record a 20-element INT array Line_Code[0..19] is repeated. The first model's array starts at DB2.DBW235; the next model starts at DBW235 + 174. When an operator enters a new line code from an HMI panel, the PLC must determine whether that code already exists anywhere in the 400 stored values (20 models x 20 codes) before the entry is accepted.
What STL Provides That SCL Hides
STEP 7 V5.5 includes an SCL compiler that can express the same scan in a few lines, but several production environments still restrict source code to AWL for traceability, version-control diffing, or cycle-time determinism. AWL exposes:
- Address register manipulation (
LAR1,+AR1,TAR1) - The
LOOPinstruction with implicit decrement of ACCU1-L - ANY pointer construction for area-cross access (
P#DBX, area/DB/length prefix) - 3-LSB bit-offset arithmetic that the SCL compiler hides
For engineers familiar with web development, the conceptual parallel is the Array.prototype.find() method, which returns the first element that satisfies a predicate. The STL implementation below replicates that behaviour with explicit pointer arithmetic because the S7-300 instruction set has no built-in FIND primitive for DB-resident arrays.
Prerequisites
Confirm the following before opening the STL editor:
- STEP 7 V5.5 with Service Pack 2 or later. The base AWL editor is included with every STEP 7 install; SCL is optional and not required for this article.
-
S7-300 CPU from the 312, 314, 315, 317, or 319 family. The instruction set used here (
LOOP,+AR1, area-internalDBW[AR1,P#x.y]) is supported across the entire S7-300 range; no special technology CPU is needed. -
DB2 compiled and downloaded with the 20 model records. Open the DB in View > Data View to confirm the start offset of each
Line_Codearray. -
FB container rather than FC. The FB retains the
Position,ModelIndex, andInnerIndexoutputs in its instance DB so the HMI can poll them between scans.
Line_Code[20] array as occupying DB2.DBW235 through DB2.DBW253. Twenty INT values require 40 bytes (20 words), so the correct end address is DBW274. The 19-word range in the source specification appears to be a transcription error and should be cross-checked against the offline DB layout before commissioning. The engineering below uses the 20-element interpretation.Data Layout Analysis
The data block is a 20-record, fixed-stride structure. The byte offset for any model's Line_Code array is:
Offset(M) = 235 + M x 174 (where M = 0..19)
The byte offset increments in steps of 174 even though only the first 40 bytes of each record hold the array. The remaining 134 bytes are reserved for other fields (scaling factors, status flags, timestamps, recipes) that the search must skip without examining.
| Model Index (M) | Byte Offset | First DBW | Last DBW | Skip to Next (bytes) |
|---|---|---|---|---|
| 0 | 235 | DBW235 | DBW274 | 134 |
| 1 | 409 | DBW409 | DBW448 | 134 |
| 2 | 583 | DBW583 | DBW622 | 134 |
| 3 | 757 | DBW757 | DBW796 | 134 |
| 4 | 931 | DBW931 | DBW970 | 134 |
| 5 | 1105 | DBW1105 | DBW1144 | 134 |
| 6 | 1279 | DBW1279 | DBW1318 | 134 |
| 7 | 1453 | DBW1453 | DBW1492 | 134 |
| 8 | 1627 | DBW1627 | DBW1666 | 134 |
| 9 | 1801 | DBW1801 | DBW1840 | 134 |
| 10 | 1975 | DBW1975 | DBW2014 | 134 |
| 11 | 2149 | DBW2149 | DBW2188 | 134 |
| 12 | 2323 | DBW2323 | DBW2362 | 134 |
| 13 | 2497 | DBW2497 | DBW2536 | 134 |
| 14 | 2671 | DBW2671 | DBW2710 | 134 |
| 15 | 2845 | DBW2845 | DBW2884 | 134 |
| 16 | 3019 | DBW3019 | DBW3058 | 134 |
| 17 | 3193 | DBW3193 | DBW3232 | 134 |
| 18 | 3367 | DBW3367 | DBW3406 | 134 |
| 19 | 3541 | DBW3541 | DBW3580 | (end) |
Total scan volume: 20 models x 20 INTs = 400 comparisons covering 800 bytes of INT data plus 20 x 134 = 2,680 bytes of skip space, for a record footprint of 3,480 bytes (174 x 20).
STL/AWL Indirect Addressing Fundamentals
The S7-300 CPU offers three addressing modes relevant to this task:
| Mode | Example | Use Case |
|---|---|---|
| Direct | L DBW 274 |
Hard-coded offsets; not scalable |
| Area-internal indirect | L DBW [AR1, P#0.0] |
Pointer inside the currently opened DB; the area is taken from the instruction |
| Area-cross indirect |
L DBB [AR2, P#0.0] with DB area bits in AR2 |
Pointer can switch between DB, DI, M, I, Q, L areas |
For this scan the area-internal form is sufficient because the same DB (DB2) is open throughout. The pointer format stored in AR1 is a 32-bit double word:
- Bits 0-2: bit offset within the addressed byte (always 0 for INT access)
- Bits 3 onwards: byte offset within the area
-
Upper bits: area identifier (ignored by area-internal
DBW[AR1,P#x.y])
A constant such as P#235.0 is encoded by the STL editor into this 32-bit layout. Loading it via LAR1 places byte offset 235 into AR1 with bit offset 0. Reference: Statement List (STL) for S7-300 and S7-400 Programming.
Pointer Increment for INT Step
Because each Line_Code element is an INT (16 bits, 2 bytes), the pointer must advance by 2 bytes between comparisons. The instruction +AR1 P#2.0 adds the bit-encoded constant P#2.0 to AR1. For the model-boundary jump (after the 20th element of each model) the increment is 134 bytes, encoded as P#134.0.
The LOOP Instruction
LOOP <label> decrements the low word of ACCU1 and jumps to <label> if the result is non-zero. The instruction is the canonical AWL loop construct because it sets the CC1/CC0 status bits based on the decremented value, leaves the RLO untouched, and executes in a single micro-operation on every S7-300 CPU.
LOOP decrements only the low 16 bits of ACCU1. If the counter exceeds 32,767 the instruction will underflow and loop 65,536 times. The 20-element inner loop is well inside this limit, but if you refactor for cross-record scans larger than ~32,000 entries, switch to an explicit decrement/comparison or a state machine.Single-Array Search with LOOP and AR1
Before scaling to 20 models, build the single-array search. The following FB network scans DB2.DBW235..DBW274 for a match against SearchValue and reports the 0-based index in Position.
// FB101 - Single Array Search (Model 0 only)
// INPUT: SearchValue (INT)
// OUTPUT: Found (BOOL)
// Position (INT) - 0..19, -1 if not found
L 0
T #Position // Default clear
AUF DB 2 // Open DB2 (or OPN DB [#SearchDB])
L P#235.0 // Pointer to first element
LAR1
L 20 // Loop counter
T #LoopCounter
LOOP_BODY:
L DBW [AR1, P#0.0] // Indirect load
L #SearchValue
==I
JC FOUND // Jump on equal
+AR1 P#2.0 // Advance to next INT
L #LoopCounter
LOOP LOOP_BODY // Decrement and jump if > 0
// Not found - fall through
CLR
= #Found
L -1
T #Position
BE
FOUND:
SET
= #Found
L #LoopCounter
L 20
-I // Position = 20 - LoopCounter
T #Position
BE
The position decode exploits the property that LOOP decrements before testing the jump condition. If the counter was 20 on entry and the match occurs at index 0, no decrement has happened before the JC, so the counter still holds 20. After a match at index 1 the counter has decremented to 19, and so on. The arithmetic 20 - LoopCounter recovers the original 0-based index.
Multi-Model Scan: Nested Loop Structure
Scaling to 20 models adds an outer loop and a fixed-byte skip at each model boundary. The complete algorithm is:
- Open DB2.
- Initialize AR1 to
P#235.0(start of Model 0 array). - Initialize
Positionto 0 andOuterCountto 20. -
Outer loop body: Initialize
InnerCountto 20. -
Inner loop body: Load
DBW [AR1, P#0.0], compare toSearchValue, jump to FOUND on match. Otherwise addP#2.0to AR1, incrementPosition, andLOOP. - After inner loop ends without a match, add
P#134.0to AR1 to skip the remaining 134 bytes of the model record. - Decrement
OuterCount, loop if > 0. - Both loops fallen through: set
Found = FALSE,Position = -1,ModelIndex = -1,InnerIndex = -1.
Why Position, ModelIndex, and InnerIndex
Returning three indices trades a few extra clock cycles for a clearer HMI display:
-
Position(0..399): linear offset, useful for diagnostics or data export -
ModelIndex(0..19): the model whose array held the match -
InnerIndex(0..19): the array element inside that model
The decode uses the integer division instruction /I, which leaves the quotient in ACCU1 and the remainder in ACCU2:
L #Position // ACCU1 = Position
L 20 // ACCU2 = 20
/I // ACCU1 = Position / 20 = ModelIndex
// ACCU2 = Position MOD 20 = InnerIndex
T #ModelIndex // Save quotient
TAK // Swap ACCU1 and ACCU2
T #InnerIndex // Save remainder
/I instruction on S7-300 is the integer division that produces both quotient (ACCU1) and remainder (ACCU2) in one execution. Use the standalone MOD instruction only for floating-point modulo on REAL values; do not confuse the two.Complete FB Code with Interface
The following FB returns Found, Position, ModelIndex, and InnerIndex as static outputs of the instance DB. Wire it to OB1 (or a cyclic interrupt OB such as OB35 on a 100 ms tick) and pass the HMI-entered value to SearchValue. The rising edge on Execute initiates the scan; the outputs remain stable in the instance DB until the next Execute pulse.
FB Interface Declaration
| Declaration | Name | Type | Initial Value | Comment |
|---|---|---|---|---|
| INPUT | SearchValue | INT | 0 | Code to search for |
| INPUT | Execute | BOOL | FALSE | Trigger the scan (rising edge) |
| OUTPUT | Found | BOOL | FALSE | TRUE if a match is present |
| OUTPUT | Position | INT | -1 | 0..399 or -1 |
| OUTPUT | ModelIndex | INT | -1 | 0..19 or -1 |
| OUTPUT | InnerIndex | INT | -1 | 0..19 or -1 |
| STATIC | Busy | BOOL | FALSE | Scan in progress flag |
| STATIC | ExecuteTrg | BOOL | FALSE | Edge memory for FP |
| STATIC | OuterCount | INT | 0 | Outer loop counter |
| STATIC | InnerCount | INT | 0 | Inner loop counter |
AWL Code (Network 1: Trigger and Reset)
NETWORK 1 // Trigger and reset outputs
U #Execute
FP #ExecuteTrg // Edge memory, static
SPBN NO_TRIG
// New search requested - clear results
CLR
= #Found
L -1
T #Position
T #ModelIndex
T #InnerIndex
SET
= #Busy
JU SCAN
NO_TRIG:
UN #Busy
BEB // Skip if not busy
SCAN: NOP 0
AWL Code (Network 2: Multi-Model Scan)
NETWORK 2 // Multi-model scan
AUF DB 2 // Open DB2
L 0
T #Position // Linear position counter
L P#235.0 // Pointer to first element of Model 0
LAR1
L 20 // Outer loop counter
T #OuterCount
OUTER: L 20 // Inner loop counter
T #InnerCount
INNER: L DBW [AR1, P#0.0] // Load current INT
L #SearchValue
==I
JC FOUND
+AR1 P#2.0 // Advance to next INT
L #Position
L 1
+I
T #Position
L #InnerCount
LOOP INNER
// Skip remaining 134 bytes of this model's record
L P#134.0
+AR1
L #OuterCount
LOOP OUTER
// Not found - cleanup
CLR
= #Found
= #Busy
L -1
T #Position
T #ModelIndex
T #InnerIndex
BE
AWL Code (Network 3: Match Decode)
NETWORK 3 // Match found - decode indices
FOUND: SET
= #Found
CLR
= #Busy
L #Position
L 20
/I // Quotient (ModelIndex) in ACCU1
// Remainder (InnerIndex) in ACCU2
T #ModelIndex
TAK
T #InnerIndex
BE
Verification and Commissioning Tests
Use the following four test cases to confirm the FB behaviour on the target CPU. Run them from the STL editor's Monitor/Modify view with the CPU in STOP for DB write operations, then in RUN for FB observation.
| Test | DB2 Setup | Execute | Expected Found | Expected ModelIndex | Expected InnerIndex |
|---|---|---|---|---|---|
| Match at first element | DBW235 = 100, rest = 0 | TRUE pulse, SearchValue = 100 | TRUE | 0 | 0 |
| Match at last element of model 19 | DBW3580 = 999, others random | TRUE pulse, SearchValue = 999 | TRUE | 19 | 19 |
| No match anywhere | All Line_Code values = -32768 | TRUE pulse, SearchValue = 0 | FALSE | -1 | -1 |
| First occurrence only | DBW235 = 50 AND DBW409 = 50 | TRUE pulse, SearchValue = 50 | TRUE | 0 | 0 |
Add the test sequence to the CPU's commissioning function for repeatable regression after firmware updates. If any test fails, consult the Common Pitfalls section below before reloading the FB. Reference: STEP 7 V5.5 Basic Information.
Cycle-Time Budget
Per iteration the inner loop executes roughly eight AWL instructions (L DBW[AR1,P#0.0], L #SearchValue, ==I, JC taken-or-not, +AR1 P#2.0, two L/+/T for Position, and LOOP). On a CPU 315-2 DP at nominal clock that is approximately 18 microseconds per comparison. The 400-comparison scan therefore consumes about 7.5 ms worst-case (no match), and an early-exit on the first match takes under 25 microseconds.
| Scenario | Comparisons | Estimated Scan Time (CPU 315-2 DP) |
|---|---|---|
| Match at first element | 1 | ~25 microseconds |
| Match at last element of model 19 | 400 | ~7.5 ms |
| No match | 400 | ~7.5 ms |
If the OB1 cycle is 50 ms or shorter, schedule the scan in a lower-priority cyclic interrupt (e.g. OB35) to avoid jeopardizing I/O update timing. A CPU 312 with a 10 ms OB1 cycle should refactor the FB into a per-scan state machine as described in the FAQ below.
Common Pitfalls and Edge Cases
| Symptom | Likely Cause | Fix |
|---|---|---|
Found never goes TRUE even for a confirmed value |
DB2 not opened before the scan; AUF DB 2 missing |
Add AUF DB 2 at the top of Network 2 |
InnerIndex reports a value > 19 |
Loop counter not re-initialized inside the outer body | Load L 20 / T #InnerCount inside the OUTER block, not above it |
| Pointer reads the wrong byte after several models | AR1 not preserved across other FBs called during the scan | Save/restore AR1 with TAR1 / LAR1 at FB entry and exit |
Position stays at 0 on no match |
BE missing before FOUND, so the not-found path falls through into the FOUND decoder |
Insert BE at the end of the not-found branch |
| Match found but in wrong model after model-15 boundary |
P#134.0 mis-typed as P#174.0, skipping the first array element of the next model |
Verify the skip constant against the offset table in this article |
| Status word bits OV/OS illuminate | Arithmetic overflow during Position increment or /I
|
Confirm Position does not exceed 32,767; clear OV with CLR before /I if needed |
| First match only sometimes returned | HMI retriggers Execute while a previous scan is still running |
Guard with the Busy output and ignore new triggers until Busy falls |
| Watchdog reset during scan | OB1 cycle too short for the 7.5 ms scan | Move the FB call to OB35 (100 ms cyclic) or implement per-scan state machine |
ANY Pointer Alternative (Optional)
When the search must span multiple DBs or include marker/timer/IO areas, build an ANY pointer that encodes the source type, length, DB number, area, and byte offset. The standard ANY layout (10 bytes) is:
| Byte(s) | Field | Example Value |
|---|---|---|
| 0 | Type code | B#16#05 (INT) |
| 1 | Length high (count/element) | B#16#0 |
| 2-3 | Length (elements) | W#16#0014 (=20) |
| 4-5 | DB number | W#16#0002 (=DB2) |
| 6 | Area identifier | B#16#84 (DB) |
| 7-9 | Reserved (0) | DW#16#0 |
| 10-13 | Byte offset (pointer format) | P#235.0 |
Construction in AWL:
L B#16#05 // Type = INT
T LB 0
L 20 // Length = 20 INTs
T LW 2
L 2 // DB number
T LW 4
L B#16#84 // Area = DB
T LB 6
L DW#16#0 // Reserved
T LD 7
L P#235.0 // Byte offset
T LD 10
LAR1 P##temp_any // Pass ANY pointer to system FB
ANY pointers are the formal mechanism the STEP 7 editor uses internally when passing arrays to system FBs (e.g. FC21 FILL, FC20 BLKMOV, SFC20 BLKMOV). For the single-DB case above they are unnecessary, but they scale the technique to multi-DB data archives and to S7-400 CPUs that accept the area-cross form. Reference: Statement List (STL) for S7-300 and S7-400 Programming, chapter on ANY pointer handling.
Frequently Asked Questions
Can the search be split across multiple OB1 scans to reduce cycle load?
Yes. Replace the inner LOOP with a state machine that processes one comparison per scan and tracks the model/element index in the instance DB. At 7-8 ms worst case this is rarely needed on a CPU 315, but on a CPU 312 with a 10 ms OB1 it is the standard pattern. The Busy flag from the FB above already supports this refactor with no interface changes; move the inner-body code into a CASE #State OF ladder and let OB1 cycle between states.
What is the difference between L DBW [AR1, P#0.0] and L DBW [MD10]?
The AR1 form is area-internal: AR1 contains only the byte offset and the area is taken from the instruction. The MD form uses a memory double word that must include the area identifier in the upper bits and is required for area-cross access (e.g. moving between DI and DB). For a single-DB scan the AR1 form is faster because the CPU does not have to mask and check the area bits before the memory access. The AR1 form also avoids any risk of a corrupted area code corrupting the read.
Does the LOOP instruction affect the RLO or status bits used by preceding comparisons?
No. LOOP sets only the CC1 and CC0 condition codes based on the decremented value of ACCU1-L. The RLO, OV, and OS bits are untouched, which means a preceding ==I followed by JC can be combined with a subsequent LOOP without a status-word save. This is one reason STL programmers prefer LOOP over an explicit decrement-and-compare construct.
How do I extend the search to also report the source DB and offset when the data is split across DB2 and DB3?
Wrap each DB-specific scan in its own FB (one per DB), call them sequentially from OB1, and OR the Found outputs. Use the static Busy flag from each FB to chain the calls. If the same value range can appear in both DBs, the first call that returns TRUE short-circuits the second via a conditional call (CC) on the second FB's Execute input. For data spread across more than two DBs the ANY pointer technique above scales without rewriting the search body.
Why does the STL editor sometimes show a warning on L DBW [AR1, P#0.0]?
The editor requires that the byte offset stored in AR1 is word-aligned for a 16-bit access. P#235.0 encodes bit offset 0 and is therefore word-aligned, so no warning appears. If you ever store an odd bit offset (e.g. P#235.1) into AR1 and then issue L DBW [AR1, P#0.0], the editor flags a possible alignment error. The fix is to mask the low three bits of the offset before the load, or to use byte access (L DBB) when the offset is intentionally bit-aligned (for example, when scanning packed BOOL arrays instead of INTs).
Can the same FB work on an S7-400 CPU without changes?
Yes for the area-internal indirect form used in this article. The LOOP instruction, +AR1 P#x.y, and DBW[AR1,P#0.0] syntax are identical on S7-400. The execution time on an S7-400 is roughly half that of an S7-300 of the same vintage, so the cycle-time budget improves. The only edit you may consider for S7-400 is replacing the manual /I + TAK decode with the dedicated MOD instruction on a REAL conversion, which is purely stylistic.