Searching a Value in an Array Using STL/AWL on STEP 7 V5.5

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

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 LOOP instruction 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-internal DBW[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_Code array.
  • FB container rather than FC. The FB retains the Position, ModelIndex, and InnerIndex outputs in its instance DB so the HMI can poll them between scans.
Address-range ambiguity. The original specification gives the 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 counter width. 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:

  1. Open DB2.
  2. Initialize AR1 to P#235.0 (start of Model 0 array).
  3. Initialize Position to 0 and OuterCount to 20.
  4. Outer loop body: Initialize InnerCount to 20.
  5. Inner loop body: Load DBW [AR1, P#0.0], compare to SearchValue, jump to FOUND on match. Otherwise add P#2.0 to AR1, increment Position, and LOOP.
  6. After inner loop ends without a match, add P#134.0 to AR1 to skip the remaining 134 bytes of the model record.
  7. Decrement OuterCount, loop if > 0.
  8. 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 vs MOD. The /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.

Back to blog