Overview
Scanning a byte-wise character array inside a STEP 7 Data Block is a recurring task on S7-300 and S7-400 controllers programmed in STL (Statement List, also called AWL). The classic C-style array1[n] syntax does not exist in STL at source level: every indexed read has to be assembled with AR1/AR2 and the L D[AR1,P#x.y] or L DBW[AR1,P#x.y] indirect forms. Once the index loop and the ANY pointer de-referencing are understood, a single-byte search and a multi-byte pattern search (for example "oz") can be written in a few lines of STL and reused as a standard FC.
This reference covers the canonical approach used in STEP 7 V5.x: parse an ANY input, walk the DB byte by byte using AR1 plus a loop counter, compare the loaded byte, and return either the byte offset or the absolute address. A TIA Portal / SCL variant is included at the end for teams that have migrated to S7-1200/S7-1500.
Prerequisites
- STEP 7 V5.5 or V5.6 (TIA Portal users should jump to the SCL section at the end).
- S7-300 or S7-400 CPU, firmware as installed in the project (315-2 DP/PN, 416-3, or equivalent).
- A Data Block containing an array, e.g.
ARRAY[0..2047] OF CHAR. The DB must not be optimised (optimised blocks remove the absolute byte address, which STL requires). - Working knowledge of AR1/AR2 addressing and the
L P##...macro that produces a pointer to an ANY in the local data area. - STEP 7 reference manual "Programming and Operating Manual - STL" (Siemens entry ID 45523446 on the SiePortal) for the complete instruction set.
STRING type is limited to 254 characters of payload. Arrays above 254 bytes must stay as ARRAY OF CHAR and be scanned byte by byte. Do not attempt to cast a 2048-byte CHAR array into a STRING.Understanding the ANY Pointer
When a formal parameter of an FC is declared as type ANY, the caller passes a 10-byte descriptor onto the local stack. The descriptor layout is fixed by the STEP 7 runtime and is the only way STL can talk about a variable-length source at run time.
| Byte offset | Width | Content | Meaning |
|---|---|---|---|
| +0 | 2 | Syntax ID / flags | 10h = BYTE area, 11h = WORD, 12h = DWORD, 13h = INT, 14h = DINT, 15h = REAL, 16h = STRING, 19h = BLOCK_FB/DB/SDB |
| +2 | 2 | Data type length | 1 for BYTE/CHAR, 2 for WORD/INT, 4 for DWORD/REAL |
| +4 | 2 | Count | Number of elements (here: 2048) |
| +6 | 4 | DB number + area + byte offset | High byte area code (84h/85h/86h/8Fh for DI/DO/DB/Local), low 24 bits = byte offset in the area |
The classic fragment in the source does exactly this extraction and should be kept verbatim in the FC's BEGIN network:
// ANY parameter named "UID" of type ANY
TAR1 #_ar1 // Save caller's AR1
L P##UID // Pointer to ANY descriptor in L stack
LAR1 // AR1 now points at the 10-byte ANY
L W [AR1,P#0.0] // Syntax ID / flags
T #_wPLC_Type
L W [AR1,P#2.0] // Data type length (1 for CHAR)
T #_iLen
L W [AR1,P#4.0] // Element count
T #_iCnt
L D [AR1,P#6.0] // DB number + byte offset
T #_dwArea // Whole double-word
AD DW#16#FFFFFF // Mask out the high area code
SRD 3 // Convert bit offset to byte offset
T #_dwByteOff // Byte offset within the area
After these eight lines, #_dwByteOff holds the absolute byte offset of the first CHAR in the DB, and #_iCnt holds 2048. From here the indexed loop can be built.
Indexed DB Byte Access in STL
STL does not accept a variable subscript on a symbolic name, but it does accept a fully-resolved byte access through AR1: L DBB[AR1,P#0.0]. The DBB is implicit because the area code of the ANY was parsed above, but the more explicit and CPU-agnostic form is the register-indirect DBB[AR1,P#...] which is honoured by both S7-300 and S7-400. Combine it with a per-iteration increment on AR1 to walk the array.
// Re-load the byte offset into AR1 to start the scan
L #_dwByteOff
LAR1
// Loop counter, working copy of the element count
L #_iCnt
T #_iRem
// Initialise the return value to "not found"
L -1
T #_iResult
Now the comparison loop. The branch label LOOP is local to the FC and is jumped to by the conditional jump JC or unconditional JU.
Single-Byte Search (Find First 'b')
The complete FC body for searching a single CHAR returns the zero-based index of the first match, or -1 if not found.
// --- FC1 "FindCharInArray" ---
// IN : UID (ANY) - source array
// #_cSearch (CHAR) - character to find
// OUT: #_iResult (INT) - index, or -1 if not found
TAR1 #_ar1_save
L P##UID
LAR1
L W [AR1,P#4.0] // Count
T #_iCnt
L D [AR1,P#6.0]
AD DW#16#FFFFFF
SRD 3
LAR1 // AR1 = start byte offset
L -1
T #_iResult // default: not found
L 0
T #_iIndex // running index
LOOP: L #_iCnt
L 0
==I // 0 vs iCnt: any non-zero is "still busy"
JC END // if iCnt == 0, leave
L DBB[AR1,P#0.0] // load one CHAR from the DB
L #_cSearch
==B
JC FOUND // CC1 set on equal
+AR1 P#1.0 // next byte
L #_iIndex
+ 1
T #_iIndex
L #_iCnt
+ -1
T #_iCnt
JU LOOP
FOUND:L #_iIndex
T #_iResult
END: LAR1 #_ar1_save // restore caller's AR1
BE
Notes that come up in the field:
-
==Bcompares two bytes loaded into ACCU1/ACCU2. It setsCC1on equality, which the nextJC FOUNDconsumes. -
+AR1 P#1.0is the correct way to add a pointer constant; do not useSLDor arithmetic on AR1 directly. -
BEis the standard block-end for an FC; it jumps to the saved BR stack entry that STEP 7 set up on the CALL. - The output is the zero-based index, not a pointer. If the caller needs the absolute byte address in the DB, add
#_iResultto the base offset returned in#_dwByteOff.
Multi-Byte Pattern Search (Find "oz")
Once the loop is in place, a two-character search is just a two-byte peek. The principle is: at every position i, check array[i] == 'o' and array[i+1] == 'z', guarding the upper bound so the read does not wrap.
// --- FC2 "FindPattern2InArray" ---
// IN : UID (ANY), #_c1 (CHAR), #_c2 (CHAR)
// OUT: #_iResult (INT) - index of first char of match, or -1
TAR1 #_ar1_save
L P##UID
LAR1
L W [AR1,P#4.0]
T #_iCnt
L D [AR1,P#6.0]
AD DW#16#FFFFFF
SRD 3
LAR1
L -1
T #_iResult
L 0
T #_iIndex
L #_iCnt
+ -1
T #_iLast // last valid start index = count-1
LOOP: L #_iIndex
L #_iLast
>I
JC END // index > last: stop
L DBB[AR1,P#0.0] // first byte
L #_c1
==B
JCN NEXT
L DBB[AR1,P#1.0] // second byte (peek +1)
L #_c2
==B
JCN NEXT
L #_iIndex
T #_iResult // match at this index
JU END
NEXT: +AR1 P#1.0
L #_iIndex
+ 1
T #_iIndex
JU LOOP
END: LAR1 #_ar1_save
BE
Extending the pattern to N bytes is a straight loop of N L DBB[AR1,P#n.0] reads with their own ==B and JCN NEXT tests, while advancing AR1 only after the full pattern is evaluated. The bound check is #_iIndex > #_iCnt - N instead of > #_iCnt - 1.
Returning the Absolute Address
The original question asks for the address, not just the index. Two useful forms are common in STEP 7:
-
Byte offset inside the DB:
ABS_OFFSET = #_iResult + #_dwByteOff(from the ANY parser above). This is aDINTfrom 0 to_iCnt - 1. -
Pointer in P# form: a
POINTER(6 bytes) is area + byte/bit offset. Build it as:
L #_dwArea// original 32-bit area word (already includes the 84h/85h/86h code)
L #_iResult
+D// add index to byte offset
T #_retPointer
For a CHAR inside DB1 starting at byte 100, the pointer in P#DB1.DBX100.0 BYTE form has area 84h 01 00 00 (area DB, DB number 1) and byte-offset field 00 00 64 00 for 100. Pasting the index adds to the low 24 bits of the byte offset.
SRD 3 the byte offset is correct as long as the source has no bit-granularity data, which is true for an ARRAY OF CHAR. If you ever adapt this code to ARRAY OF BOOL, keep the low 3 bits of the original D [AR1,P#6.0] and shift them back in.Edge Cases and Field-Proven Caveats
| Symptom | Root cause | Fix |
|---|---|---|
| SF (system fault) on first read, CPU goes STOP | Optimised DB; STL cannot resolve the absolute address | Open the DB, untick "Optimised access" in the attributes |
| Always returns -1 even when the character is present |
==B not setting CC1 due to ACCU mix-up |
Make sure the first L DBB[...] is the last load before the compare |
| Loop never terminates | Forgot to decrement #_iCnt or to test it |
Use the ==I / JC pattern shown above, or use the LOOP instruction with a separate loop counter |
| Address returned is one byte off | AR1 was loaded with the raw D [AR1,P#6.0] that still contains the bit offset in the low 3 bits |
Mask with AD DW#16#FFFFFF and SRD 3 as shown in the parsing block |
| Crash on the last byte of the pattern search | Peeked DBB[AR1,P#1.0] when the pattern started at iCnt-1
|
Bound check #_iIndex > #_iCnt - N before the pattern test |
| Works in PLCSIM but not on the real CPU | Symbolic-versus-absolute conflict; STL inserted a different access path on the real CPU | Compile with "absolute address" mode and check the BSS / interface in the STL source view |
Performance Notes
A 2048-byte linear scan completes in roughly 1-2 ms on a 315-2 PN/DP and 0.3-0.6 ms on a 416-3, dominated by the 2048 L DBB[...] loads and the equal compares. Calling the FC in OB1 every cycle is safe up to arrays of about 8 KB; above that, place the call in a time-of-day interrupt OB (OB10-OB17) or a cyclic interrupt OB (OB30-OB38) with a 50-100 ms period to avoid starving OB1.
For arrays above 16 KB, consider scanning in 1 KB chunks and tracking the last index in a static VAR of the calling FB, which gives you the option to early-exit as soon as a match is found.
Modern Alternative: SCL on TIA Portal (S7-1200 / S7-1500)
If the project is on TIA Portal V16 or later and the CPU is an S7-1200/S7-1500, prefer SCL. The same logic is one line per search because SCL understands FOR, indexed array access, and the CONCAT / FIND IEC functions natively.
// SCL equivalent of FC1
FUNCTION "FindCharInArray" : Int
VAR_INPUT
aBuf : ARRAY[0..2047] OF CHAR; // pass as IN-OUT or Array[*]
cSearch: CHAR;
END_VAR
VAR_TEMP
i : Int;
END_VAR
BEGIN
FOR i := 0 TO 2047 DO
IF aBuf[i] = cSearch THEN
FindCharInArray := i;
RETURN;
END_IF;
END_FOR;
FindCharInArray := -1;
END_FUNCTION
For a 2048-byte buffer the SCL approach is competitive on S7-1500 (compiled to MC7) and far easier to maintain. STL is still required only when the project is locked to STEP 7 V5.x or when the scan must run in an S7-300 / S7-400.
Verification and Commissioning
- Open the DB in STEP 7 and fill
array1[0..15]with the pattern under test, e.g.'A','b','C','D','o','z','F','G'in a VAT table. - In OB1, call the FC and watch
#_iResultin the VAT. The expected value for the 'b' search is1; for "oz" it is4. - Set a VAT watch on the first ten bytes of the array and on the result variable, then force the array to
'x','x','x','x','x','x','x','x'to confirm the FC returns-1when the pattern is absent. - Step through the FC in "single scan" mode (CRST + SR/ST toggled to RUN-P, then SWITCH to single step via Test > Operation > Single Step) to watch AR1, the index and the
==Bresult bit (CC1) per iteration. - For long runs, record the OB1 cycle time in the diagnostic buffer (Tools > Diagnostics > Diagnostic Buffer) before and after enabling the search call. A spike of 1-2 ms on a 315-2 is acceptable; more indicates the array is being scanned twice (e.g., once for 'b' and once for "oz") and the FCs should be merged.
Can I avoid pointers entirely and use symbolic array indexing in STL?
No. STL on S7-300/S7-400 compiles symbolic access to absolute addresses, but it does not accept a variable subscript on a symbolic name. The supported ways to read array1[n] are register-indirect via AR1/AR2 (used throughout this article) or the LOOP instruction combined with a constant-offset index table. SCL is the only language that gives you a true C-style subscript.
Why does my FC crash when the array is declared in an FB instance or a multi-instance?
Multi-instance and instance-DB areas use the area code 85h (DI) instead of 86h (DB). The parsing block above reads the area code in the high byte of D [AR1,P#6.0], but the byte offset is in the low 24 bits regardless of the area. As long as you mask with AD DW#16#FFFFFF and SRD 3 you will get a correct offset, but you must open the area with OPN DI (or pass the instance-DB number separately) before issuing DBB[AR1,P#0.0]; otherwise the CPU will raise an area-length error.
What is the maximum array size I can scan with this approach?
The loop counter is an INT in the example, so the practical limit is 32 767 bytes. For a 32 KB buffer, change #_iCnt and #_iIndex to DINT and use the ==D / >D variants of the compares. STEP 7 itself has no architectural limit; only the available work memory of the CPU matters for the DB itself (DBs above 64 KB must be split or the SFC / SFB extended-DB functions used).
My CALL of the FC shows "Actual parameter UID: type mismatch". What is wrong?
The formal parameter is typed as ANY in the FC interface, but the caller is passing a symbolic operand that is not declared as ANY-compatible. In OB1, the call site has to use the absolute form "DB1".array1 or a temporary ANY variable initialised with BLD 7 / BLD 255 / ... block. Open the FC's interface, set the parameter type to ANY (not POINTER), recompile, and the warning will disappear.
Is there a standard FC for string search that I can just drop in?
For arrays up to 254 characters, use the IEC FIND function from the "Standard Library > IEC Function Blocks" palette in STEP 7: it returns the 1-based position of a sub-string. For longer arrays you must use the STL pattern shown in this article, because IEC STRING is capped at 254 bytes of payload. On TIA Portal the same FIND function is available in the "String + Char" extended instructions and works on a 16 KB STRING with no STL needed.