S7-1500/S7-1200 Dynamic String Addressing via Indirect DB Access

David Krause20 min read
SiemensTechnical ReferenceTIA Portal
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

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 S7 STRING 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
Common misconception: the first byte is NOT the actual length. The first byte is the declared maximum length (for bounds checking). The actual length is at offset +2 as a 16-bit big-endian word. Reading 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.
When building an array of strings, the byte step between consecutive elements is the declared maximum length plus 4, not the actual length. For 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 single PEEK 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 of string 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 to BLKMOV (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 the VARIANT 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 instructions DB_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.
For per-string operations, PEEK_BLK or the ANY approach is preferred.

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 a SEARCH_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:
  1. The S7-1500 maintains a sorted index of strings in the registry DB.
  2. The KTP700 displays the user-entered string and asks the S7-1500 to perform the lookup.
  3. The S7-1500 returns the matching index (or -1).
  4. 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).
This shifts the work to the S7-1500 where it belongs, while still giving the user a responsive HMI experience.

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
For DBs generated by an external tool, set these attributes consistently. Optimized block access is incompatible with absolute PEEK access unless the offsets are managed via a symbolic SCL "AT" view overlay.

18. Verification Procedure

After implementing the chosen approach, verify with the following test plan:
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. 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.
  2. Reading byte 0 as the length. Byte 0 is the maximum length, not the actual length. Always read the WORD at offset +2.
  3. Big-endian length word. The ActLen is stored in big-endian (Motorola) byte order, not little-endian. A direct WORD_TO_INT on the bytes 03 00 (length 3) does not give 768 - it gives 3. Verify byte order on the actual hardware.
  4. 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.
  5. DB number 0 is invalid for PEEK in the DB area (16#81). Validate the runtime DB number before the PEEK call.
  6. 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).
  7. 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:
  1. 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.
  2. 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.
  3. 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.
  4. 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.
This eliminates dynamic-DB addressing entirely, fits the existing KTP700 workflow, and keeps the S7-1500 cycle time under 5 ms regardless of array size. For legacy systems where the schema cannot be refactored, the 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.
Always validate the dynamic DB layout against the search FB's stride assumptions. If the external generator changes the element size (e.g., adds a 4-byte timestamp), every PEEK offset in the search code silently breaks. Add a CRC or element-count check at startup, and alarm if it fails.

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.

Back to blog