S7-1500 Ladder: Indexing Arrays with Local TEMP Variables

David Krause10 min read
SiemensTIA PortalTroubleshooting
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

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.

Safety notice. A 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 TEMP variables of types BOOL, INT, DINT, REAL, etc. are initialised to their type default (FALSE, 0, 0.0) at the start of each block call.
  • TEMP STRUCT / UDT instances are initialised to the UDT's start values.
  • TEMP STRING variables are initialised to length 0 and the terminating '$00' character.
  • When block access is set to standard, TEMP memory 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:

  1. Default every TEMP to its type default.
  2. Compute the index from a known-good source (input, instance DB, or a literal).
  3. Bound-check the index against the array's LB (lower bound) and UB (upper bound) before the array read.
  4. 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

  1. Compile and download. Use Project → Compile all (rebuild); the SCL/ST compiler will report any type mismatch on the index.
  2. 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.
  3. Force a deliberate bad index. Modify iIndex32 to -1 or UB + 1 with 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.
  4. Check the diagnostic buffer. Online → Diagnostics → Diagnostic buffer. A clean buffer for the test run confirms the fix.
  5. 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:

  1. Always declare the index variable in the block's interface (VAR_INPUT for caller-supplied, VAR_TEMP for computed). Never reach for a global MW slice from inside a library block.
  2. Wrap every indirect array read in an in-house FB_IndexGuard. Centralising the bound check makes the safety property provable by inspection.
  3. 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."
Reference material. The TEMP lifetime rules and array-index data-type requirements are documented in the Siemens SIMATIC S7-1500 / ET 200MP Automation System function manual and the STEP 7 (TIA Portal) Programming and Operating Manual. For block-level optimisation attributes, see the S7-1500 system manual's chapter on Block access modes. Cross-reference the CPU firmware release notes when extending the index type to 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).

Back to blog