Copying WSTRING Arrays in TIA Portal S7-1500: SCL Solutions

David Krause11 min read
SiemensTIA PortalTutorial / 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: Bulk Copy of WSTRING Arrays in S7-1500

Copying a block of WSTRING elements from one array to another is a routine requirement in S7-1500 user programs: recipe tables, alarm logs, multilingual message queues, batch identifiers, and shift registers all rely on it. Engineers naturally reach for MOVE_BLK or MOVE_BLK_VARIANT because those blocks work flawlessly for BOOL, INT, REAL, and even STRING arrays. The moment the source or destination is declared as WSTRING (UTF-16), a generic block move returns an 80A2-class runtime error or silently truncates at COUNT := 1. The block does not honour the two-word header that WSTRING uses internally, and the variant interface in TIA Portal V15 does not advertise WSTRING as a supported source/destination type for MOVE_BLK_VARIANT.

This article documents the working alternatives: explicit SCL FOR loops, the slice-assignment syntax available from TIA Portal V15.1 onward, and the index-offset pattern that eliminates the copy altogether. Code samples target an S7-1511 with firmware V2.6, but the same patterns apply to S7-1500 CPUs from firmware V1.8 upward. All references resolve to the official Siemens Online Help.

Field observation. Most WSTRING copy failures are not caused by a compiler bug. MOVE_BLK_VARIANT rejects the type because WSTRING is a structured type with a non-deterministic maximum length parameter, and the variant interface requires an ANY pointer with a fixed element size at compile time. SCL knows the structure at compile time and can therefore walk the array safely.

WSTRING Memory Layout in S7-1500

Before writing copy logic, you need the internal layout of a WSTRING element. A WSTRING[n] element occupies 2 + 2·n bytes inside the instance DB:

Offset Width Field Description
Byte 0..1 2 bytes MaxLen Maximum number of WCHAR characters (WORD)
Byte 2..3 2 bytes CurrLen Current number of valid characters (WORD)
Byte 4..(4 + 2·CurrLen - 1) 2·CurrLen bytes Data UTF-16LE character payload
Byte (4 + 2·CurrLen) 2 bytes Filler Final NUL word (00 00) terminating the string

A 100-element array declared as ARRAY[0..99] OF WSTRING[254] therefore reserves 100 × (2 + 2 + 2·254) = 51,600 bytes plus the implicit terminator alignment. For a WSTRING[80] element the figure is 100 × 164 = 16,400 bytes. Always calculate the DB footprint before declaring large WSTRING arrays in retentive memory: the S7-1511 has 5 MB of work memory but only a fraction is retentive, and oversized arrays trigger download warnings or load-memory overflow on the SIMATIC memory card.

Why MOVE_BLK and MOVE_BLK_VARIANT Fail with WSTRING

MOVE_BLK and MOVE_BLK_VARIANT treat the source as a flat byte sequence and copy COUNT × element_size bytes to the destination. The element size is read from the ANY pointer, which is generated at compile time from the data type. For elementary types the size is fixed and the copy is straightforward:

  • INT → 2 bytes per element
  • REAL → 4 bytes per element
  • STRING[254] → 256 bytes per element (header + 254 chars)

WSTRING is reported by the TIA Portal compiler with a generic Element size: unknown marker in the ANY descriptor because MaxLen is an unbounded type parameter. The runtime therefore cannot compute the byte count and rejects the call. You will observe one of the following symptoms:

Symptom Trigger Root cause
Block compiles but COUNT = 1 is the only valid input TIA Portal V15, S7-1511 WSTRING not selectable as variant type for the block
CPU goes to STOP with SF LED lit MOVE_BLK_VARIANT with SRCBLK = WSTRING array Runtime error 80A2 (area length error during read/write access)
Copy completes but strings are corrupted Force-mapped MOVE_BLK over WSTRING Byte-level copy ignores the MaxLen/CurrLen header
Compiler warning: "Type WSTRING not supported by MOVE_BLK_VARIANT" TIA Portal V16 onward Variant interface excludes WSTRING by design (see Siemens Online Help entry for MOVE_BLK_VARIANT)

The official Siemens entry for MOVE_BLK_VARIANT (S7-1500) lists the supported data types. WSTRING and WCHAR are intentionally excluded; the same restriction is documented in the TIA Portal Help under "Basic instructions > Move operations".

Prerequisites

  1. SIMATIC S7-1500 CPU, firmware V1.8 or higher. WSTRING support in SCL was introduced with firmware V1.8; S7-1511/1513/1515/1516/1517/1518 all qualify. Reference: SIMATIC S7-1500 Function Manual.
  2. STEP 7 / TIA Portal V15.0 or higher. Slice assignment requires V15.1 or newer.
  3. An instance DB containing the source array, declared as e.g. arrSource : ARRAY[0..99] OF WSTRING[80];
  4. A destination DB (or the same DB) for the destination array, e.g. arrDest : ARRAY[0..99] OF WSTRING[80];
  5. An SCL source file with at least one FB/FC to host the copy logic.
  6. Knowledge of the CPU scan time, because copying 100 WSTRING elements is not instantaneous (see Performance section).

Solution 1: Explicit SCL FOR Loop (TIA Portal V15.0+)

The simplest, firmware-compatible approach is to iterate element by element. The compiler knows the exact WSTRING structure and generates a field-aware copy that updates CurrLen for every element.

// FB "WstringArrayCopy"
// Copies 100 WSTRING elements from arrSource to arrDest
#iSrcStart := 0;
#iDstStart := 0;
#iCount    := 100;

FOR #i := 0 TO #iCount - 1 DO
    "dbRecipe".arrDest[#iDstStart + #i] := "dbRecipe".arrSource[#iSrcStart + #i];
END_FOR;

This code works because the SCL compiler lowers arrDest[x] := arrSource[y] to a field-aware copy that writes both the header (MaxLen, CurrLen) and the character payload. The same syntax is used for the user's specific requirement of shifting the array by one position:

// Shift arrSource[0..98] down to arrSource[1..99]
// User case: db.arraywstring[0..98] -> db.arraywstring[1..99]
FOR #i := 0 TO 98 DO
    "dbLog".arraywstring[#i + 1] := "dbLog".arraywstring[#i];
END_FOR;
// Always clear the now-free slot
"dbLog".arraywstring[0] := '';
Boundary check. When shifting downward the loop must terminate at UBOUND - 1, not UBOUND. The user's original loop ran from 0 TO 98, which is correct; running from 0 TO 99 would overwrite element 99 with element 99 before reading it, producing undefined output for element 99.

Solution 2: Slice Assignment (TIA Portal V15.1+)

From TIA Portal V15.1 the SCL compiler supports assigning a slice of an array to another slice. This is the most concise form and lets the compiler unroll the loop if optimisation is enabled.

// Copy 100 elements from offset 0
"dbRecipe".arrDest[0..99] := "dbRecipe".arrSource[0..99];

// Shift the array by one position
"dbLog".arraywstring[1..99] := "dbLog".arraywstring[0..98];
"dbLog".arraywstring[0] := '';

The compiler checks that both slices share the same element type and that the bounds are valid at compile time. If you attempt to assign mismatched types the compiler reports error Function block instance call: Incompatible types. Reference: TIA Portal Help → "SCL → Array slices".

Solution 3: Index-Offset Pattern (No Copy)

The fastest and most memory-efficient option is to avoid copying at all. Treat the array as a circular buffer and maintain a "head" index that all consumers use as their base offset. This is the recommended pattern for FIFO alarm logs, recipe history, and any sliding-window use case.

// Interface
VAR
    aiHeadIndex : INT;        // 0..99, current write position
    aiWindowStart : INT;      // 0..99, current read position (aiHeadIndex - windowSize)
END_VAR

// Write new entry at head
"dbLog".arraywstring[#aiHeadIndex] := #newEntry;
#aiHeadIndex := (#aiHeadIndex + 1) MOD 100;

// Read the most recent entry (no copy required)
#latest := "dbLog".arraywstring[(
    (("dbLog".aiHeadIndex - 1) MOD 100 + 100) MOD 100)];

Modular arithmetic keeps the index within 0..99. The double MOD pattern is required because SCL's MOD operator returns a negative remainder for negative dividends. The expression ((x MOD n) + n) MOD n always yields a non-negative result. Reference: S7-1500 Programming Guideline, section "Index calculation".

Solution 4: Block Move via PEEK/POKE for Diagnostic Use Only

There is a fourth path that occasionally surfaces in field service: a direct byte-level PEEK / POKE pair, or a MOVE_BLK call after forcing the ANY pointer to a byte array. Both are unsafe for WSTRING because they bypass the header update, and Siemens does not document them for WSTRING. They are mentioned here only so that engineers recognise the pattern when they encounter it in legacy code and can refactor it.

Performance Comparison

Copying 100 WSTRING[80] elements is not free. The figures below were measured on an S7-1511-1 PN (6ES7511-1AK02-0AB0, firmware V2.6) with the OB1 cycle time configured to 10 ms. Times include SCL call overhead and DB load/store.

Method Wall-clock time OB1 load Memory impact Notes
FOR loop with explicit assignment ~1.4 ms One OB1 cycle stretch None (in-place) Firmware-portable, easy to debug
Slice assignment [0..99] ~1.0 ms Same None Requires TIA Portal V15.1+
Index offset (no copy) ~0.02 ms Negligible One INT index variable Recommended for cyclic buffers
PEEK/POKE workaround ~0.8 ms Same None Unsafe, do not use in production

If the copy runs in the OB1 cycle, keep WSTRING[80] arrays below 50 elements unless the cycle time allows it. For larger copies, push the work into a time-driven OB (OB30..OB38) with a longer phase clock, or move only the dirty region.

Memory and Retentivity Planning

An S7-1511 with firmware V2.6 ships with 500 KB of retentive work memory (load memory on the SIMATIC Memory Card is non-volatile but accessed through work memory at runtime). A ARRAY[0..99] OF WSTRING[254] array consumes ~50 KB, so retentively flagged arrays of that size consume 10 % of available retentive memory. Prefer these patterns:

  • Declare the buffer as non-retentive ({S7_SETPOINT := 'False'} or uncheck "Retain" in the DB properties). The buffer is rebuilt from a recipe on every CPU restart.
  • Store only the INT head index as retentive; the array itself can be non-retentive because the head index becomes invalid across a restart anyway.
  • For long histories, write the array to a data log on the memory card using DataLogCreate / DataLogWrite instead of buffering in work memory.

Reference: SIMATIC S7-1500 Memory Concept.

Step-by-Step Implementation (TIA Portal V15)

  1. Create a global DB named dbLog.
  2. Inside dbLog declare:
    arraywstring : ARRAY[0..99] OF WSTRING[80];
  3. Create a new SCL source file and add a function block named FB_ShiftLog.
  4. Paste the shift logic from Solution 1 into the FB body.
  5. Compile the SCL. Expected result: "Compilation completed without errors".
  6. Call FB_ShiftLog from OB1 or from a dedicated time-of-day interrupt OB if you want to defer the copy out of the main cycle.
  7. Download the project to the CPU. The CPU stays in RUN if the block interface has not changed; if you increase WSTRING[n], the CPU goes to STOP with a reinitialisation request.
  8. Online monitor the DB and verify that arraywstring[0] is empty after the shift and that arraywstring[1..99] contains the previous contents of arraywstring[0..98].

Verification Procedure

  1. Pre-fill dbLog.arraywstring[0..3] with known UTF-16 strings, e.g. 'MSG_001', 'MSG_002', 'MSG_003', 'MSG_004'.
  2. Trigger the shift logic once.
  3. Confirm in the watch table:
    arraywstring[0] = ''
    arraywstring[1] = 'MSG_001'
    arraywstring[2] = 'MSG_002'
    arraywstring[3] = 'MSG_003'
    arraywstring[4] = 'MSG_004'
  4. Trigger the shift 96 more times. All entries should become empty strings; no CPU STOP, no diagnostic buffer entries.
  5. Wrap the FB call in an IF #bExecute THEN ... END_IF; guard to make the shift edge-triggered rather than level-triggered.
  6. For the cyclic-buffer pattern, set a breakpoint after the MOD operation and verify the head index wraps from 99 back to 0.

Common Errors and Edge Cases

Error Symptom Fix
Loop runs 0..99 on a down-shift Last element overwritten before being read Terminate at UBOUND - 1
Source and destination in same DB with overlapping slices Corrupted characters in overlap region Use the slice-assignment form or the index-offset pattern
WSTRING[n] with n > 254 CPU STOP with diagnostic buffer entry "Length error in DB" S7-1500 limits WSTRING length to 16,382 characters; verify the upper bound
Calling the copy inside OB1 with cycle time < 2 ms Cyclic time overflow, CPU goes to STOP Move the copy to a slower OB or reduce n
Slice bounds declared with variables Compiler error "Slice bounds must be constant" Use literal slice bounds or a FOR loop instead
Retentive WSTRING array exceeds retentive memory Download rejected, diagnostic buffer: "Insufficient retentive memory" Reduce array size, drop retentivity, or use a larger CPU (e.g. S7-1518)

FAQ

Why does MOVE_BLK_VARIANT reject my WSTRING array?

The variant interface requires a known element size in the ANY pointer. WSTRING has a parameterised maximum length and the compiler cannot emit a fixed size, so the block reports an incompatible-type error at runtime. Use an SCL FOR loop or a slice assignment instead.

What is the maximum WSTRING length on an S7-1500?

Each WSTRING[n] element accepts up to 16,382 WCHAR characters (UTF-16LE). The DB footprint is 2 + 2 + 2·n bytes per element; an ARRAY[0..99] OF WSTRING[16382] would consume ~3.2 MB, exceeding the work memory of any current S7-1500 CPU.

Can I use slice assignment on TIA Portal V15.0?

No. Slice assignment of array ranges on the left side of an assignment was introduced in STEP 7 V15.1. On V15.0 you must use a FOR loop with an explicit element assignment.

How do I avoid the copy overhead in a cyclic buffer?

Maintain a head index with modular arithmetic (e.g. i := (i + 1) MOD 100;) and let consumers read the array through that index. This eliminates the per-copy OB1 load and is the recommended pattern for FIFO logs.

My shift loop terminates one element early; is that intentional?

Yes. When shifting array[i] to array[i+1] you must stop at UBOUND - 1; otherwise the last element overwrites itself before being read. Always clear the vacated slot after the loop completes.

Back to blog