Problem Overview
On a Siemens S7-1500 CPU programmed in TIA Portal, ladder logic that successfully indexes a global ARRAY via a memory word fails when the same logic is rewritten to use a local TEMP variable of the enclosing block:
| Form | Code | Result |
|---|---|---|
| Global index | ArrayName[%MW1] |
Compiles, downloads, executes |
| Local index (Int) | ArrayName[#TempPointer] |
Compiles, downloads, executes |
| Local index (Word) | ArrayName[#TempPointer] |
Compiles but raises a type-conversion / range error at runtime |
The defect is silent in the editor: the project builds, the program loads to the CPU, and the OB is entered. The failure surfaces either as the array element default (zero) persisting, or as CPU stop with a programming error OB (OB 121) reporting an illegal operand or out-of-range index. Diagnosing the root cause requires understanding the S7-1500 type system for array indices, the difference between standard access and optimized access blocks, and the lifetime rules for TEMP data.
TEMP variable is not a guaranteed-initialised storage location. Treat every read of a TEMP variable as uninitialised until the program has executed an explicit write in the current call. The pattern shown below enforces that contract.Root Cause Analysis
Two distinct but easily conflated defects are present in the failing snippet.
1. Word vs. Int / DInt type mismatch
S7-1500 array indices must be integer-typed. The valid element-index data types are INT, DINT, and (for symbolic array slicing) LREAL when the index is a constant expression. WORD is a 16-bit bit-string type and is not implicitly convertible to INT inside an array subscript. When the compiler accepts a WORD index it does so only because of relaxed implicit conversion rules in older TIA Portal versions; the runtime then sees a 16-bit pattern that may exceed the array's lower or upper bound and triggers OB 121 (programming error) or returns the element at index 0 / out-of-range fallback.
2. Optimised block TEMP lifetime
On an S7-1500 with optimised block access (the default for new FBs/FCs/OBs since TIA Portal V13), the compiler is free to reallocate TEMP storage between calls and even between code sections inside a block. The user-visible rules are:
- Elementary
TEMPvariables of typesBOOL,INT,DINT,REAL, etc. are initialised to their type default (FALSE,0,0.0) at the start of each block call. -
TEMPSTRUCT/ UDT instances are initialised to the UDT's start values. -
TEMPSTRINGvariables are initialised to length0and the terminating'$00'character. - When block access is set to standard,
TEMPmemory is the L stack and behaves as a global scratch pad; the previous value can be re-read in the next call if no other code has overwritten it. The runtime is not required to preserve that value, and a programmer must never depend on it.
Combining these two points, the failure sequence is: the TEMP variable of type WORD is read as the array index. The bit pattern is interpreted as an integer far outside the valid [LB..UB] range of the array, the CPU raises SF with a programming error, and the OB is requested.
Data Type Requirements for Array Indices
The following table summarises the accepted and rejected index types on S7-1500 / S7-1200.
| Index tag type | Accepted at index position | Notes |
|---|---|---|
BOOL |
No | Bit-string, never indexable |
BYTE |
No | Unsigned 8-bit bit-string |
WORD |
No (legacy implicit cast only) | Not an integer; cast was deprecated |
INT |
Yes | Signed 16-bit, range -32768..32767 |
DINT |
Yes (recommended) | Signed 32-bit, full CPU range |
USInt / UINT / UDINT |
Yes (≥ V15.1) | Unsigned variants; CPU firmware ≥ 2.0 |
| Constant literal | Yes |
Array[i] where i is INT literal |
For a production library the recommended type is DINT because it covers the full ARRAY[..] range available in S7-1500 (up to DINT-bound indices) and avoids any signed-16-bit boundary surprises when the offset crosses 32767.
Optimised vs. Standard Block Access: Lifetime Rules
| Property | Optimised access | Standard access |
|---|---|---|
| Default in TIA Portal V13+ | Yes | No (legacy) |
| TEMP default on entry | Type default (0, FALSE, '') | Undefined (residual L-stack value) |
| Compiler may re-use storage | Yes | No (fixed L-stack offset) |
| Symbolic, not absolute | Yes | No (slice notation required) |
| Download-in-run safe | Yes | Restricted |
To inspect or change the access mode in TIA Portal: right-click the FB/FC/OB in the project tree, choose Properties → Attributes, and toggle Optimized block access. The choice affects every variable in the block; it cannot be set per variable.
Solution: Corrected Ladder Logic
Declare the pointer as INT or DINT in the block's TEMP section, then assign the index from a deterministic source before the array read.
FB/FC interface declarations:
VAR_TEMP
// Index variable, integer-typed
iIndex : INT; // -32768..32767
iIndex32 : DINT; // full S7-1500 range, recommended
bFirstCycle : BOOL; // initialisation flag
END_VAR
Ladder network 1 — initialise the index every cycle:
┌────────┐
──┤M0.0├──┬─( = )─iIndex32 // default init to 0; overwrite below
└────────┘
Ladder network 2 — read the array element:
iIndex32 := DB_Recipe.nOffset; // explicit assignment FIRST
┌────────┐
EN ──┤MOVE ├── iIndex32 // any deterministic source
└────────┘
┌────────────────────────┐
│ ArrayName[iIndex32] │ // symbolic indexed access
└───────────┬────────────┘
│
▼
dValue ──> OUTPUT
Ladder network 3 — guard against out-of-range:
iIndex32 >= ArrayName.LB AND iIndex32 <= ArrayName.UB
┌─────────────┐
EN ──┤ CMP >= 0 ├── bInRange // LB and UB are INT literals
│ CMP <= 99 │
└─────────────┘
IF bInRange THEN
dValue := ArrayName[iIndex32];
ELSE
dValue := 0.0;
// optional: raise a flag for the HMI alarm log
END_IF;
The pattern above reproduces what a pure-ST snippet expresses more compactly:
// Structured Text equivalent
iIndex32 := "DB_Recipe".nOffset; // deterministic assignment
IF (iIndex32 >= 0) AND (iIndex32 <= 99) THEN
dValue := "ArrayName"[iIndex32];
ELSE
dValue := 0.0;
END_IF;
Safe TEMP Initialisation Pattern
Even with optimised access, an explicit initialisation is required when the index flows from a computation rather than a tag. The recommended pattern is a single assignment network at the very top of the block, executed unconditionally:
- Default every
TEMPto its type default. - Compute the index from a known-good source (input, instance DB, or a literal).
- Bound-check the index against the array's
LB(lower bound) andUB(upper bound) before the array read. - Read the array element only when the bound check passes; otherwise use a safe fallback.
Pre-built function block "FB_IndexGuard":
FUNCTION_BLOCK FB_IndexGuard
VAR_INPUT
iReq : DINT; // requested index
END_VAR
VAR_OUTPUT
bValid : BOOL; // in-range flag
END_VAR
VAR CONSTANT
iMin : DINT := 0;
iMax : DINT := 99;
END_VAR
BEGIN
bValid := (iReq >= iMin) AND (iReq <= iMax);
END_FUNCTION_BLOCK
Wrap every indirect array read in this guard. Cost is one comparison cycle; benefit is a guaranteed absence of OB 121 programming errors from the array step.
Variant Pointers and Slice Access
For pointer-style logic where the array itself is dynamic (e.g. a recipe selects a different DB), use the VARIANT type and the Variant access instructions. The pattern works in both LAD and FBD on S7-1500 ≥ firmware V2.0:
VAR_TEMP
vData : VARIANT;
iIndex : DINT;
dResult : LREAL;
END_VAR
// Source selection at runtime
CASE nDataSource OF
0: vData := "DB_RampA".aProfile;
1: vData := "DB_RampB".aProfile;
ELSE vData := "DB_RampDefault".aProfile;
END_CASE;
// Indirect read by Variant
IF IS_ARRAY(vData) THEN
iIndex := LIMIT(0, iIndex, COUNT_OF_ELEMENTS(vData) - 1);
dResult := VARIANT_TO_LREAL(vData[iIndex]);
END_IF;
Where slice access is still required (older programs, third-party library compatibility), keep the global %MW index but add a comment pointing the user to the symbolic equivalent:
// %MW10 = integer tag of type INT, drives ArrayName[%MW10]
// Symbolic alternative (preferred):
// ArrayName["Recipe".nOffset] // where nOffset : INT
PEEK / POKE and Legacy Forms
The legacy PEEK and POKE instructions from S7-300/400 are also available on S7-1500 as PEEK_WORD, PEEK_DWORD, POKE_WORD, POKE_DWORD, plus a generic PEEK_BLK / POKE_BLK for arbitrary byte ranges. They operate on absolute memory addresses, are slower than symbolic indexed access, and should be reserved for diagnostic or HMI-vendor interoperability. Use them only when symbolic access is not feasible (e.g. reading a tag whose name is known at runtime via a string parameter).
| Mechanism | Index type required | Scope |
|---|---|---|
Symbolic array index arr[i]
|
INT / DINT | Same block / instance DB |
Slice access %MW10
|
Absolute, fixed | Standard access only |
| Variant indirect | DINT inside vData[i]
|
Any DB or instance |
| PEEK / POKE | Bit / byte address, integer-encoded | Absolute I/O / M / DB |
Verification Procedure
- Compile and download. Use Project → Compile all (rebuild); the SCL/ST compiler will report any type mismatch on the index.
-
Online → Monitor / Modify. Open the FB in LAD, right-click
ArrayName[#iIndex32], choose Monitor all. Verify the integer value before and after the move instruction. -
Force a deliberate bad index. Modify
iIndex32to-1orUB + 1with the CPU in RUN-P. Confirm that OB 121 is not called because the bound check has intercepted the read. If OB 121 still fires, the guard is missing or wired in parallel rather than in series with the array step. - Check the diagnostic buffer. Online → Diagnostics → Diagnostic buffer. A clean buffer for the test run confirms the fix.
- Cycle-time sanity. With a guard in place, the indexed access adds one comparison (≈ a few hundred ns at OB1 priority class 1). No measurable cycle-time impact on a 1515-2 PN or larger.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Array element always 0, no CPU stop |
WORD index silently clamped or zero-extended |
Re-type to INT / DINT
|
| CPU stop, OB 121, Invalid operand | Index outside [LB..UB] of the array |
Add FB_IndexGuard around the read |
| Index 0 inside a multi-instance FB, other tags wrong | Optimised access, TEMP reused across calls |
Move the index to VAR (static) for persistence |
| LAD: red squiggle on subscript | Tag in subscript is not symbolic / not in the interface | Add a VAR_TEMP declaration, or pass via VAR_IN_OUT
|
| Pointer arithmetic (offset of STRUCT) not accepted | Indexer must be an integer, not a derived address | Pre-compute the byte offset, then use a VARIANT
|
| Library re-use breaks after CPU firmware update | Firmware-dependent USInt/UInt index support | Pin index to DINT, check Siemens compatibility list
|
Library Hygiene for Reusable Code
When the block is intended as a library element (the original poster's stated goal), three rules keep the indexed access safe across CPUs and firmware revisions:
- Always declare the index variable in the block's interface (
VAR_INPUTfor caller-supplied,VAR_TEMPfor computed). Never reach for a globalMWslice from inside a library block. - Wrap every indirect array read in an in-house
FB_IndexGuard. Centralising the bound check makes the safety property provable by inspection. - Document the index type contract in the block's Info text: "The index parameter is
DINT, in the inclusive range [0..99]. The caller is responsible for keeping it in range."
USInt/UINT/UDINT.Why does ArrayName[%MW1] work but ArrayName[#TempPointer] fail when both are 16-bit?
%MW1 is a global memory-word tag that the compiler treats as an absolute address; the implicit cast to integer index is allowed. A TEMP declared as WORD is a bit-string type, not an integer, and the S7-1500 array indexer requires INT or DINT. Re-type the TEMP to INT or DINT to restore the symbolic indexed access.
Does an S7-1500 TEMP variable keep its value after the block exits?
In optimised blocks, no: the value is reset to the type default (0, FALSE, '', UDT start value) on entry to the next call. In standard-access blocks, the L-stack memory is theoretically reusable, but the runtime is not required to preserve it, so the value must always be treated as uninitialised. Assign a value before every read.
Which data type is the safest array index on S7-1500?
DINT. It is signed 32-bit, covers the full S7-1500 ARRAY index range, and is accepted by every firmware version since V1.0. INT is acceptable for arrays of at most 32767 elements but will silently misbehave at the signed boundary.
What OB is called when an array index goes out of range?
OB 121 (Programming error) is called. The diagnostic buffer records the offending address and operand. To suppress the stop, install an OB 121 handler that increments a counter and clears the index, or, preferably, add a bound check in the application code so OB 121 is never called.
Can I use a VARIANT to point at any array and index it symbolically?
Yes, on S7-1500 firmware V2.0 and later, combined with TIA Portal V15.1 and later. Assign the VARIANT at runtime, test it with IS_ARRAY, and use VARIANT_TO_LREAL(vData[i]) (or a typed variant conversion) to read the element. Wrap the read in a bound check against COUNT_OF_ELEMENTS(vData).