S7-1500/S7-1200 Dynamic String Addressing via Indirect DB Access
1. Problem Definition
A common automation scenario: an S7-1500 (or S7-1200) controller must determine whether a user-entered string exists in any one of several data blocks, where the target DB number is only resolved at runtime. The DBs are generated automatically from a higher-level database (recipes, device lists, material codes), so symbolic naming at compile time is impractical. With 200 KTP700 panels feeding S7-1200 stations, which in turn forward data to a central S7-1500, doing the comparison on the S7-1500 is the only feasible location. The challenge: symbolic addressing in SCL requires the block name at compile time. The statement"MyDB".StringArray[i] := ...; is resolved by the TIA Portal compiler to an absolute address. If MyDB is not known when the code is compiled, the absolute address cannot be embedded, and the compiler rejects the reference.
This article documents the practical techniques for runtime string access on S7-1200/S7-1500, the trade-offs of each, and a verified path to a high-performance solution for 3,000-element arrays.
2. S7 STRING Memory Layout
Every S7STRING variable carries a 4-byte header in front of the character data. The SCL declaration str : STRING[254]; generates the following on-disk layout (lowest address first):
| Offset | Field | Size | Description |
|---|---|---|---|
| +0 | MaxLen | BYTE | Maximum string length (e.g., 254 = 0xFE) |
| +1 | Reserved | BYTE | Always 0; not a length field |
| +2 | ActLen | WORD (2 bytes) | Actual length of payload (big-endian) |
| +4 | Chars[1..ActLen] | BYTE each | Character data |
PEEK_BYTE(... +0) yields a value of 254 for a STRING[254]; it does not tell you the populated length.
For WSTRING (Unicode), the header is 6 bytes and characters are 2 bytes each; max payload is 16,346 characters. The same indirect-access techniques apply, with doubled offsets.
ARRAY[0..2999] OF STRING[80], each element occupies 84 bytes on disk (4 header + 80 chars), and the array consumes 84 × 3000 = 252,000 bytes. The InstDB (Instance DB) must be sized accordingly.
3. Why Symbolic Addressing Fails at Runtime
TIA Portal resolves every symbolic access in SCL/STL/LAD/FBD at compile time. The compiler converts"DB_Recipes".Recipes[3] into an absolute address of the form DB 200, DBB 256. The "DB 200" portion is then replaced with a fully qualified pointer to data block 200, byte 256. The PLC runtime never sees the original symbolic name.
This is why passing a DB number to a function and trying to call something like:
IF DB[i].Field = x THEN ... END_IF;
is impossible in standard SCL. The compiler has no way to generate code that defers the DB selection to runtime; it would have to allocate an arbitrary pointer at scan time, and the static type system of S7-1200/S7-1500 SCL does not support that without specific instruction variants (POINTER, ANY, VARIANT).
4. PEEK and POKE Instruction Family
The PEEK/POKE instructions provide absolute access to memory areas by specifying a numeric area identifier, block number, and byte offset. They are the workhorse of dynamic DB access.| Instruction | Return Type | Parameters | Use |
|---|---|---|---|
| PEEK | BYTE/WORD/DWORD variants | area, dbNumber, byteOffset | Read one scalar element |
| PEEK_BLK | VOID | src_area, src_db, src_offset, dest_area, dest_db, dest_offset, count | Block copy (preferred for strings) |
| POKE | VOID | area, dbNumber, byteOffset, value | Write one scalar element |
| POKE_BLK | VOID | src_area, src_db, src_offset, dest_area, dest_db, dest_offset, count | Block write |
4.1 Area Identifiers
| Area ID (hex) | Memory Area |
|---|---|
| 0x81 | Data block (PA) |
| 0x82 | Process image of inputs (PE) |
| 0x83 | Process image of outputs (PAA) |
| 0x84 | Bit memory (M) |
| 0x86 | Local data / temp area (L stack) |
5. SCL Pattern: Iterative String Search with PEEK
The baseline implementation uses PEEK_BLK to copy a candidate string from the target DB into a temporary STRING variable, then performs a regular SCL string comparison. The user enters the search string once; the search block iterates over the array and returns the first matching index. ```scl FUNCTION_BLOCK "fbSearchStringInDynamicDB" VAR_INPUT i_targetDBNumber : INT; // Runtime-resolved DB number i_stringMaxLen : INT; // Declared STRING[N] i_arrayCount : INT; // Number of strings in array i_searchString : STRING; // User input from KTP700 END_VAR VAR_OUTPUT o_matchIndex : INT; // -1 = not found, else 0..n-1 o_searchComplete : BOOL; END_VAR VAR s_tempString : STRING; // Sized to match max length n_byteOffset : DINT; n_actualLen : WORD; n_index : INT; END_VAR BEGIN o_matchIndex := -1; o_searchComplete := FALSE; // Header is always 4 bytes; element stride = i_stringMaxLen + 4 FOR n_index := 0 TO i_arrayCount - 1 DO n_byteOffset := INT_TO_DINT(n_index) * (i_stringMaxLen + 4); // Read ActLen (WORD at offset +2) PEEK_BLK( src_area := 16#81, src_db := INT_TO_WORD(i_targetDBNumber), src_offset := DINT_TO_DWORD(n_byteOffset + 2), dest_area := 16#86, dest_db := 0, dest_offset := 0, count := 2); // Read string payload into temp PEEK_BLK( src_area := 16#81, src_db := INT_TO_WORD(i_targetDBNumber), src_offset := DINT_TO_DWORD(n_byteOffset + 4), dest_area := 16#86, dest_db := 0, dest_offset := 0, count := INT_TO_WORD(i_stringMaxLen)); // Copy block-moved payload into STRING variable // (use BLKMOV-via-AT-view or assign to a temp STRING) IF s_tempString = i_searchString THEN o_matchIndex := n_index; EXIT; END_IF; END_FOR; o_searchComplete := TRUE; END_FUNCTION_BLOCK ```5.1 Practical Performance Cost
For 3,000 elements × ~80 char strings, the FOR loop runs 3,000 times per scan. Each iteration performs two PEEK_BLK calls plus a string comparison. On an S7-1500 CPU 1515-2 PN at typical OB1 cycle pressure, this can consume 10-30 ms per scan in the search block alone, which is on par with the 10 ms cycle budget mentioned in field reports. Spreading execution across multiple cycles is mandatory.6. PEEK_BLK vs. Recursive Byte-by-Byte PEEK
A more elegant pattern is to skip copying the whole string. Instead, fetch the candidate's ActLen once, then compare character-by-character using a singlePEEK per byte, and abort as soon as a mismatch is found.
```scl
// Inside the FOR loop, after n_byteOffset is known
n_actualLenWord := PEEK_WORD(
area := 16#81,
dbNumber := i_targetDBNumber,
byteOffset := DINT_TO_WORD(n_byteOffset + 2));
IF n_actualLenWord = LEN(i_searchString) THEN
n_match := TRUE;
FOR n_char := 0 TO WORD_TO_INT(n_actualLenWord) - 1 DO
IF PEEK_BYTE(area := 16#81,
dbNumber := i_targetDBNumber,
byteOffset := DINT_TO_WORD(n_byteOffset + 4 + n_char))
<> MID(i_searchString, n_char + 1, 1) THEN
n_match := FALSE;
EXIT;
END_IF;
END_FOR;
IF n_match THEN
o_matchIndex := n_index;
EXIT;
END_IF;
END_IF;
```
The early-exit on length mismatch is the critical optimization: when the user's input is short and the DB contains 80-char strings, the inner loop never executes. This reduces worst-case comparisons from 240,000 (3,000 × 80) to 3,000 (3,000 × 1).
Caveat: PEEK per byte is still a function-call-level operation; for very large arrays, the FOR loop overhead can itself be a bottleneck. See Section 9 for hash-based optimization.
7. Optimized Element Stride for Mixed-Record Arrays
The user reported a stride ofstring length + 2 + 6. This indicates the array elements are records containing a STRING, a DINT (4 bytes), and an INT (2 bytes), for an effective step of 4 + 4 + 2 = 10 bytes overhead + string payload. If your array is:
TYPE Recipe : STRUCT id : DINT; name : STRING[80]; qty : INT; END_STRUCT; END_TYPE
the per-element stride is 4 (DINT) + 4 (STRING header) + 80 (STRING chars) + 2 (INT) = 90 bytes. The PEEK_BLK source offset must step by 90 per index, not 84.
When generating DBs from an external schema, it is good practice to add a constant SIZE_OF alias in the controller and pass it to the search FB so changes to the record structure require only one parameter update, not a full re-engineer of the search loop.
8. Avoiding the Copy Entirely: ANY Pointer Block Moves
Siemens SCL on S7-1500 supports the ANY data type, which is a 10-byte descriptor containing the data type code, repetition count, and DB number/offset. Constructing an ANY at runtime and passing it toBLKMOV (or its successor, MOVE_BLK) lets you copy an entire string from an unknown DB into a local variable in a single SCL statement.
```scl
VAR_TEMP
s_srcAny : ANY;
s_dstAny : ANY;
n_actual : INT;
END_VAR
// Build the source ANY pointing at the string header
s_srcAny := NULL; // clear
s_srcAny.at[0] := 16#10; // Syntax ID: S7-300/400/1500 ANY
s_srcAny.at[1] := 16#02; // Transport size: BYTE
s_srcAny.at[2..3] := WORD_TO_BCD(2 + i_stringMaxLen) // Length
s_srcAny.at[4..5] := WORD#16#81; // DB area
s_srcAny.at[6..9] := DWORD#0; // DB number (set below)
s_srcAny.db := INT_TO_WORD(i_targetDBNumber);
s_srcAny.offset := DWORD_TO_DWORD(n_byteOffset);
MOVE_BLK_VARIANT(src := s_srcAny, dst := s_dstAny, count := ...);
```
The ANY approach is significantly more efficient than the looped PEEK pattern, but it requires a working data block for POKE_BLK / BLKMOV with the ANY pointer. The ANY pointer structure details are documented in the Siemens S7-1500 System Manual, Volume 1, section "ANY pointer format".
Compatibility: the ANY pointer as a SCL local variable is supported on S7-1500 firmware 2.0+ and S7-1200 firmware 4.4+. For older S7-1200 firmware, the string copy must be implemented with PEEK_BLK.
9. Hash-Based Lookup: O(1) Search Across 3,000 Strings
For a per-cycle search over 3,000 strings, the linear algorithm (O(n) per query) is the wrong design. A 32-bit hash of each string, stored alongside it in the DB, reduces the search to O(n) one-time precompute, and O(1) per query.9.1 Pre-computation (Run Once at DB Load Time)
When the external database generator emits the DB, generate a parallel DB containing the hash of each string. Use a simple FNV-1a 32-bit hash, or CRC32 via the Siemens CRC instructions available in the "Extended instructions" palette. The hash DB has the same number of elements as the string array, indexed in parallel. ```scl // At startup, walk the string array once and build the hash table FOR n_index := 0 TO i_arrayCount - 1 DO // Build ANY pointer to candidate string // (omitted for brevity; see Section 8) // Read full string s_hash[i_index] := fnv1a_32(s_tempString); END_FOR; ```9.2 Runtime Query
Once the hash table exists in a known DB (which can be the original dynamic DB or a separate registry), the query path becomes: ```scl n_searchHash := fnv1a_32(i_searchString); // Direct O(1) lookup if the hash DB is indexed symbolically // (assuming the hash table is in a fixed DB) o_matchIndex := n_searchHash MOD i_arrayCount; // simple hash bucket // Collision check at the bucket IF "HashDB".Bucket[o_matchIndex].Hash = n_searchHash THEN // verify by reading the full string with PEEK_BLK // (collision probability ~1/i_arrayCount, very rare) ELSE o_matchIndex := -1; END_IF; ``` The advantage: the per-query cost is one hash calculation plus one PEEK_BLK collision-check, independent of array size. For 3,000 entries, the linear search does 3,000 worst-case iterations; the hash lookup does 1. The catch: the hash table is itself in a DB, and if the underlying string DBs are dynamic, the hash DB must be regenerated whenever the string DBs are regenerated. This is acceptable if both are produced by the same external generator.10. The VARIANT Approach (S7-1500)
S7-1500 firmware 1.8+ and S7-1200 firmware 4.0+ support theVARIANT data type. A VARIANT can hold any data type, including a STRING, and the runtime can inspect its actual type and contents.
```scl
FUNCTION_BLOCK "fbVariantStringAccess"
VAR_INPUT
i_stringRef : VARIANT; // Pointed at runtime to the desired string
END_VAR
VAR
s_value : STRING;
n_type : INT;
END_VAR
BEGIN
n_type := TypeOf(i_stringRef);
IF n_type = 12 THEN // 12 = STRING in TypeOf encoding
VariantGet(src := i_stringRef, dst := s_value);
// s_value now holds the actual string contents
END_IF;
END_FUNCTION_BLOCK
```
The catch: constructing the VARIANT itself still requires an ANY pointer under the hood. The user is still responsible for pointing it at the right DB/offset at runtime. The VARIANT type simplifies the read (no manual byte-by-byte handling) but does not bypass the problem of "I don't know which DB".
The practical pattern is: use PEEK/POINTER to compute the source address, build a VARIANT, then use VariantGet to extract the string into a local variable. This combination is the cleanest S7-1500 solution.
11. Direct DB Access via DB_GET and DB_PUT (S7-1500)
The S7-1500-specific instructionsDB_GET and DB_PUT read and write the entire contents of a DB whose number is given as a runtime parameter. They use the standard internal WRREC/RDREC mechanism (DS 0/1 access path).
```scl
VAR
s_dummySource : ARRAY[0..8191] OF BYTE; // Sized to largest DB
END_VAR
DB_GET(req := TRUE,
dbNumber := i_targetDBNumber,
dbLength := 0, // 0 = read full length
pDestData := s_dummySource,
busy => ...,
done => ...,
error => ...);
// s_dummySource now contains the full DB bytes; walk it manually
```
Limitations:
- DB_GET operates on a per-call basis, with asynchronous busy/done semantics. The result is not available in the same OB1 cycle.
- The full DB content is copied to a buffer in the work memory; for a 1 MB DB, this is a substantial copy.
- Optimized DBs are not fully supported; you may need to access the DB with absolute addressing only.
- For string lookups specifically, DB_GET is overkill - it copies the entire DB when only one string is needed.
12. Centralized Registry Pattern
The cleanest architectural fix is to not have a large set of small dynamic DBs at all. Instead, have a single fixed symbolic DB whose structure is sized to the maximum possible number of dynamic lists. The generator writes the contents into a known, indexed region of that DB. The search FB then only deals with one DB number, and symbolic access works. ```scl TYPE "tRegEntry" : STRUCT category : INT; // Identifies which "list" this row belongs to value : STRING[80]; hash : DWORD; END_STRUCT; END_TYPE DATA_BLOCK "DB_Registry" "Entries" : ARRAY[0..99999] OF "tRegEntry"; END_DATA_BLOCK ``` Trade-off: the registry DB is large and grows linearly with total entry count, but the search code is symbolic, the FB is portable across projects, and the cycle-time cost is O(1) per query (if you also index by hash). This is the recommended pattern for new designs.13. OSCAT Library Alternative
The OSCAT (Open Source Community for Automation Technology) library provides aSEARCH_IN_BUFFER function block that searches a byte stream for a needle substring, returning the byte offset of the first match. The user can pre-load the entire DB into a byte buffer (or a slice of one), then call SEARCH_IN_BUFFER.
OSCAT considerations:
- Originally written for S7-300/400; porting to S7-1200/1500 requires the SCL source converter.
- After conversion, the FB attributes must be reviewed. The default block may have non-optimized access and IN/OUT parameters that need updating to TIA Portal conventions.
- Performance is comparable to the manual PEEK_BLK + compare pattern; no significant speedup unless combined with a pre-computed hash table.
- OSCAT is third-party code: review its licensing terms (it is generally free for commercial use) and validate the function blocks on a test PLC before deploying.
14. HMI-Side Filtering with KTP700
The KTP700 has limited processing capability and a maximum of 3,000 dynamic text list entries is a stretch. WinCC Comfort/Professional supports text lists and symbolic IO fields, but generating 3,000 entries per panel × 200 panels = 600,000 HMI-side list entries is a configuration burden. A workable approach is the indexed symbolic IO pattern:- The S7-1500 maintains a sorted index of strings in the registry DB.
- The KTP700 displays the user-entered string and asks the S7-1500 to perform the lookup.
- The S7-1500 returns the matching index (or -1).
- The KTP700 looks up the index in a small, fixed symbolic HMI tag array (perhaps 100 entries that are loaded on demand from the S7-1500).
15. Comparison of Approaches
| Method | Cycle Cost per Query (3,000 strings) | Code Complexity | Maintainability | Best For |
|---|---|---|---|---|
| PEEK_BLK per element, full string copy | 10-30 ms | Low | High | Small arrays (< 100) |
| PEEK_BLK per element, length precheck + early exit | 3-10 ms | Medium | High | Mixed-length arrays |
| Recursive byte-by-byte PEEK | 5-15 ms | High | Medium | Variable-length matches |
| ANY pointer + BLKMOV | 2-5 ms | High | Medium | S7-1500 with FW 2.0+ |
| DB_GET full copy | 20-100 ms (async) | Low | Low | Bulk processing, not search |
| Hash-based lookup (precomputed) | < 1 ms | High | High | Repeated queries |
| Registry DB (single fixed DB) | < 1 ms | Medium | High | New designs |
16. Cycle-Spreading Execution
If the linear search must be retained (e.g., a legacy system), the impact on the OB1 cycle can be controlled by spreading the work across N cycles. A common pattern: ```scl VAR n_state : INT; // 0 = idle, 1..N = search index, N+1 = done n_batchSize : INT := 50; s_scanCycle : INT; // Increments each OB1 cycle END_VAR IF n_state = 0 AND i_trigger THEN n_state := 1; END_IF; IF n_state > 0 AND n_state < i_arrayCount THEN FOR n_index := n_state TO MIN(n_state + n_batchSize - 1, i_arrayCount - 1) DO // ... same as the inline search code above END_FOR; n_state := n_state + n_batchSize; END_IF; IF n_state >= i_arrayCount THEN o_searchComplete := TRUE; n_state := 0; END_IF; ``` With a batch of 50 strings per cycle, a 3,000-element search completes in 60 cycles. At a 10 ms cycle time, that's 600 ms total response time - acceptable for a recipe-validation query but not for a per-keystroke HMI filter.17. Required Block Attributes for PEEK Access
For PEEK to access a DB, the target DB must be enabled for absolute access. The relevant settings in TIA Portal:| Attribute | Setting | Required For |
|---|---|---|
| Optimized block access | Disabled (or enabled for symbolic only) | PEEK/POKE |
| Data block is write-protected | Disabled | POKE |
| Accessible from HMI/OPC UA | Enabled | External visibility |
| Standard S7 block access | Enabled (S7-1500) | DB_GET/DB_PUT |
18. Verification Procedure
After implementing the chosen approach, verify with the following test plan:- Static test: Set up a DB with a known array of strings (e.g., 5 entries with known values). Trigger the search with each entry and confirm the index is correct. Trigger with a non-existent string and confirm -1.
- Boundary test: Use a string of length 0 (empty), length 1, and length equal to the declared max length. Confirm correct behavior, including handling of trailing whitespace.
- Performance test: Use the S7-1500 web server or the TIA Portal online diagnostics to measure OB1 cycle time before and after enabling the search. Acceptable increase: 1-2 ms.
- Stress test: If using cycle-spreading, trigger the search and measure the wall-clock time from trigger to completion. Compare to the 1/N-of-cycle-time prediction.
- Field test: Validate against a representative DB set on the production line. The first time the external generator emits a new DB, verify the new DB's layout matches the search FB's stride assumption.
19. Common Pitfalls
- Confusing STRING[80] with 80 bytes of usable space. Total storage is 84 bytes. A loop iterating with step 80 silently reads into the next element's header.
- Reading byte 0 as the length. Byte 0 is the maximum length, not the actual length. Always read the WORD at offset +2.
-
Big-endian length word. The ActLen is stored in big-endian (Motorola) byte order, not little-endian. A direct
WORD_TO_INTon the bytes 03 00 (length 3) does not give 768 - it gives 3. Verify byte order on the actual hardware. - Optimized DB blocks. PEEK access to an optimized block may fail with a runtime error. Either disable optimization for these DBs, or build an AT view that maps the dynamic area as an array of BYTE.
- DB number 0 is invalid for PEEK in the DB area (16#81). Validate the runtime DB number before the PEEK call.
- Max DB number depends on the CPU model. S7-1500 supports up to 32,768 DBs; S7-1200 supports up to 6,000 (CPU 1214C and up).
- WSTRING handling. The WSTRING header is 6 bytes and characters are 2 bytes each. The same techniques apply, but the offset arithmetic must be doubled.
20. Recommendation for This Application
For the described system (200 KTP700 panels, S7-1200 feeders, S7-1500 aggregator, dynamic DBs from external generator, 3,000-string lists), the recommended path is:- Refactor the schema: have the external generator write to a single registry DB on the S7-1500 rather than generating many small DBs. The registry DB is fixed, symbolic, and indexed by category.
- Build a hash index at DB load time (or have the generator emit precomputed hashes). The hash index is an array of DWORD in the same registry DB.
- Query path: hash the user's input, direct-lookup in the hash index, and read the full string with PEEK_BLK (or symbolic access if the registry is single-DB) for collision verification.
- KTP700 interaction: the panel submits a request tag, the S7-1500 returns the matching index. The HMI reads the description from a small per-category cache loaded on panel startup.
PEEK_BLK + early-exit length precheck pattern (Section 6) with cycle-spreading (Section 16) is the best balance of code complexity, runtime cost, and maintainability.
How do I read an S7 STRING's actual length when I only have a DB number and offset?
Use PEEK_BLK to copy 2 bytes from offset +2 (the ActLen field) into a local WORD. The ActLen is stored in big-endian byte order; if you read a single BYTE, you get the high byte. The first byte at offset +0 is the maximum length, not the actual length.
Why does PEEK access fail on my optimized data block?
Optimized block access in TIA Portal removes the absolute address mapping that PEEK relies on. Either disable optimization for the DB (Properties → Attributes → Optimized block access) or build an SCL AT view that overlays the dynamic region as ARRAY[*] OF BYTE, then use symbolic access via the AT view.
Can I use VARIANT to avoid the PEEK step entirely?
No. VARIANT simplifies the read of a string once you have a reference, but constructing the reference still requires an ANY pointer built from the DB number and offset at runtime. The VariantGet instruction reads through the ANY pointer; it does not eliminate it.
What is the fastest way to search 3,000 strings per cycle on an S7-1500?
Precompute a 32-bit hash per string into a parallel hash table. At query time, hash the input, index into the hash table (O(1)), and verify the match with a single PEEK_BLK. This reduces a 3,000-iteration linear search to a single comparison plus one PEEK_BLK. Cycle-time impact: under 1 ms.
Is the OSCAT SEARCH_IN_BUFFER function a drop-in replacement?
It works after conversion to TIA Portal SCL, but it is a search-by-substring primitive, not a search-by-record primitive. You still need to wrap it with a loop over the array and provide the target DB's contents as a byte buffer. The performance is comparable to a manual PEEK_BLK + compare loop, not better. It is useful as a code-reuse starting point but not a magical speedup.
What is the maximum number of data blocks supported on an S7-1500 vs. S7-1200?
S7-1500 CPUs support up to 32,768 DBs (subject to CPU-specific limits in the configuration); S7-1200 supports up to 6,000 DBs on the larger CPU models (1215C, 1217C) and fewer on smaller ones. Check the specific CPU datasheet in TIA Portal under "Maximum number of data blocks" for the exact value.
Can I avoid the problem entirely by using a single registry DB?
Yes, and it is the recommended pattern for new designs. Use one fixed symbolic DB with a structure of {category: INT; value: STRING; hash: DWORD} and let the external generator append rows. The search FB then uses symbolic access; cycle-time cost is constant; the code is portable.