SCL Bit Access in Array Elements: S7-1500 TIA Portal Guide

David Krause13 min read
S7-1200SiemensTutorial / 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

Extracting a single bit from an indexed element of a WORD, INT, DINT, or BYTE array is one of the most common SCL (Structured Control Language) tasks on SIMATIC S7-1200 and S7-1500 controllers. A typical scenario: a 40-element array of DINT values holds status words, and the application needs to read bit n of word m at runtime where both m and n are variables calculated in the program.

The intuitive SCL syntax — #Array[#Index].%X#Bit or #Array[#Index].x[#Bit] — fails to compile in TIA Portal because the slice suffix is evaluated as a compile-time constant, not a runtime variable. The compiler emits errors such as "The expression in the slice must be a constant" or "temp.x is not a defined identifier", even though the surrounding code is structurally valid. This article explains why the slice mechanism is restricted to literal constants and presents the three production-grade workarounds: the AT overlay, the CASE dispatch, and the auxiliary ARRAY OF BOOL technique.

Why Indirect Slice Access Fails

Slice access is a TIA Portal feature for S7-1200/S7-1500 introduced with STEP 7 V13 that allows you to address a sub-area of a tag at a known, fixed offset. The valid suffixes are:

Suffix Width Permitted Index Type Example
.%X<0..15> 1 bit Integer constant 0..15 #Status.%X3
.%B<0..3> 8 bits Integer constant 0..3 #Status.%B1
.%W<0..1> 16 bits Integer constant 0..1 #Status.%W0
.%D<0> 32 bits Constant 0 #Status.%D0

The reference manual "Programming and Operating Manual - S7-1200/S7-1500" and the TIA Portal help entry "Addressing areas of a tag with slice access (S7-1200, S7-1500)" confirm that slice indices must be numeric literals or symbolic constants. The compiler cannot generate a dynamic bit-selection instruction because the underlying STL/MC7 bit-access opcodes (U, UN, S, R, X) require a fixed bit address at translation time.

Consequently, this code does not compile on any firmware from V13 through V20:

#Output_Bit := #Array_Input[#Word_Index].%X#Bit_Index; // ERROR: bit index not constant

The error reads either "The expression after %X must be a constant" or, when the developer uses the older .x[<i>] notation inherited from classic STEP 7, "The tag '<temp>.x' has not been defined." The compiler treats the x[...] portion as an attempt to address a non-existent nested structure member rather than as a slice.

Key constraint: Slice access on S7-1500 is purely a syntactic convenience that maps 1:1 to a fixed bit/byte/word offset in the symbol's memory layout. The SCL compiler rejects any non-constant index because the generated MC7 code would require a runtime address calculation, which slice access does not perform. To do that calculation you must overlay the tag, dispatch via CASE, or use pointer-based PEEK/POKE.

Solution 1: AT Overlay (Recommended)

The AT declaration overlays a tag with a different interpretation at the same memory address. By overlaying a DINT with an ARRAY[0..31] OF BOOL (or a WORD with ARRAY[0..15] OF BOOL), you expose the individual bits as array elements, and those elements can be indexed by a variable.

Step 1 — Declare the overlay in the FC/FB static or temp section

VAR
    // Original 40-word input buffer
    Array_Input : ARRAY[1..40] OF DINT;
    Word_Index  : INT;
    Bit_Index   : INT;       // 0..31
    Output_Bit  : BOOL;

    // Scratch copy that the overlay will interpret bit-wise
    tempDint    : DINT;
    tempBits    : ARRAY[0..31] OF BOOL;
END_VAR

VAR_TEMP
    // AT overlay must live in VAR / VAR_TEMP / IN_OUT / STAT
    // In a function (FC) only IN, OUT, IN_OUT, TEMP are allowed.
END_VAR

The AT construct must be declared in the same code block; the syntax for an array element overlay is:

FUNCTION_BLOCK FB_BitAccess
VAR
    Array_Input : ARRAY[1..40] OF DINT;
END_VAR
VAR_TEMP
    iWord : INT;
    iBit  : INT;
    ret   : BOOL;

    // Overlay a SINGLE DINT with a bit array
    tempDint : DINT;
    tempBits : ARRAY[0..31] OF BOOL;
END_VAR

BEGIN
    // Step 1: bounds check on the word index
    IF (iWord < 1) OR (iWord > 40) THEN
        ret := FALSE;
        RETURN;
    END_IF;

    // Step 2: bounds check on the bit index (0..31 for DINT)
    IF (iBit < 0) OR (iBit > 31) THEN
        ret := FALSE;
        RETURN;
    END_IF;

    // Step 3: copy the indexed DINT into the overlay-friendly temp
    #tempDint := #Array_Input[#iWord];

    // Step 4: read the bit at the variable index
    #ret := #tempBits[#iBit];
END_FUNCTION_BLOCK

The compiler accepts #tempBits[#iBit] because both the base symbol (tempBits) and the element index (iBit) are valid operands for an array access. The trick is that the slice would have been syntactic sugar for the same memory location; the overlay achieves the same end result through a different syntax that the compiler does accept with dynamic indices.

Step 2 — Verify the bit order

Siemens bit ordering follows the IEC 61131-3 convention: bit 0 is the least significant bit (LSB) of the underlying word. The overlay ARRAY[0..31] OF BOOL on a DINT therefore places bit 0 at value 2⁰, bit 31 at value 2³¹. This is the same convention used by the slice suffix %X0..%X15 on a WORD, so swapping the overlay for a slice in a static (non-indexed) context produces identical results.

Element of tempBits Bit position in DINT Mask value
tempBits[0] LSB 16#0000_0001
tempBits[1] 16#0000_0002
tempBits[7] 16#0000_0080
tempBits[15] 16#0000_8000
tempBits[31] MSB 16#8000_0000

Solution 2: CASE Statement Dispatch

For applications where the bit index is restricted to a small, known set (for example, four status flags per word), a CASE dispatch is more readable than a generic overlay and lets you catch illegal bit indices explicitly. The trade-off is that you must list every legitimate value of #Bit_Index.

VAR_TEMP
    iWord   : INT;
    iBit    : INT;
    ret     : BOOL;
    tempDint: DINT;
END_VAR

BEGIN
    ret := FALSE;

    IF (iWord < 1) OR (iWord > 40) THEN
        RETURN;
    END_IF;

    tempDint := Array_Input[iWord];

    CASE iBit OF
        0:  ret := tempDint.%X0;
        1:  ret := tempDint.%X1;
        2:  ret := tempDint.%X2;
        3:  ret := tempDint.%X3;
        4:  ret := tempDint.%X4;
        5:  ret := tempDint.%X5;
        6:  ret := tempDint.%X6;
        7:  ret := tempDint.%X7;
        8:  ret := tempDint.%X8;
        9:  ret := tempDint.%X9;
        10: ret := tempDint.%X10;
        11: ret := tempDint.%X11;
        12: ret := tempDint.%X12;
        13: ret := tempDint.%X13;
        14: ret := tempDint.%X14;
        15: ret := tempDint.%X15;
    ELSE
        ret := FALSE;   // illegal bit index — handle as fault
    END_CASE;
END_FUNCTION_BLOCK

Each tempDint.%X<n> uses a constant suffix, so the compiler is happy. At runtime the CASE evaluates the variable iBit and selects the matching constant slice. The ELSE branch is the cleanest place to set a fault flag, log the violation, or feed an error word back to HMI.

Performance note: A CASE on 16 alternatives compiles to a small jump table on the S7-1500 and executes in roughly 0.1–0.3 µs, comparable to the AT overlay. The CASE approach scales poorly above ~32 alternatives; for full 32-bit coverage use the AT overlay or a modulo-and-mask trick.

Solution 3: PEEK / POKE with Pointer Arithmetic

On S7-1500 firmware V2.0 and later you can use the PEEK and POKE instructions in SCL to read a single bit by computing its byte-and-bit address from the index. This technique is the closest equivalent to classic STEP 7 "pointer-driven indirect addressing" and is also used in legacy STL code that has been migrated to SCL.

VAR_TEMP
    byteAddr  : DWORD;   // absolute byte address of the bit
    bResult   : BOOL;
    pByte     : POINTER TO BYTE;
    bValue    : BYTE;
END_VAR

BEGIN
    // Byte address = (Word_Index - 1) * 4  (because DINT is 4 bytes)
    byteAddr := DWORD#16#0000_0000  // replace with DB base address
              + DWORD#4 * DWORD#16#0000_0000;   // illustrative
    // Use PEEK to read one byte at the calculated address,
    // then mask with the bit index.
    // (Full implementation depends on whether Array_Input lives in
    //  a global DB, instance DB, or local TEMP area.)
END

The PEEK family works on a POINTER TO BYTE constructed at runtime and is documented in the S7-1500 system manual under "Extended instructions — Pointer operations". The trade-off is verbosity and a higher risk of address-calculation bugs, so prefer the AT overlay unless you need to access bits in a DB that you cannot structurally overlay (e.g., a parameter passed as VARIANT).

Solution 4: ARRAY OF BOOL as a Reusable Pattern

If the same conversion is performed many times in the project, define a user-defined type (UDT) that pairs a DINT with its bit-array alias and use it consistently:

TYPE "UDT_DINT_BITS"
    STRUCT
        Value : DINT;
        Bit   : ARRAY[0..31] OF BOOL;   // AT overlay in a UDT
    END_STRUCT;
END_TYPE

Note: TIA Portal V16+ supports AT in a UDT only if the overlaid member is a non-UDT elementary type and the overlay is declared in the same STRUCT. If the UDT form is rejected, declare a small FB with two VAR members (Value : DINT and Bits : ARRAY[0..31] OF BOOL) plus an AT overlay, then instantiate the FB instead of the DINT array.

Compiler Errors and What They Mean

Compiler / Online Error Likely Cause Fix
"The expression after %X must be a constant" Variable used as slice index (e.g., .%X#iBit) Use AT overlay or CASE dispatch
"The tag 'temp.x' has not been defined" Legacy .x[i] syntax on S7-1500 Replace with AT overlay or CASE
"An AT declaration is only allowed in VAR, VAR_TEMP, VAR_IN_OUT, or STAT" Attempted AT in VAR CONSTANT or wrong section Move AT to VAR_TEMP (FC) or VAR (FB)
"The AT overlay has a different length than the original" Overlay size mismatch (e.g., 16-bit overlay on 32-bit tag) Match overlay size: 32 bits on DINT, 16 on WORD, 8 on BYTE
"Slice access is not supported for this data type" Slice applied to REAL, LREAL, STRING, or WSTRING Convert to DWORD/WORD first, or use AT overlay on an alias tag

Bounds Checking and Error Handling

An out-of-range word index will otherwise read garbage from adjacent memory in a DB and could expose a security flaw (e.g., a process interlock that is bypassed by a malformed HMI tag). The TIA Portal compiler cannot detect a runtime range violation, so explicit IF guards or CASE ELSE branches are mandatory for safety-related code.

FUNCTION_BLOCK FB_BitAccess_Safe
VAR CONSTANT
    C_MIN_WORD : INT := 1;
    C_MAX_WORD : INT := 40;
    C_MIN_BIT  : INT := 0;
    C_MAX_BIT  : INT := 31;
END_VAR
VAR
    Array_Input : ARRAY[1..40] OF DINT;
END_VAR
VAR_TEMP
    tempDint : DINT;
    tempBits : ARRAY[0..31] OF BOOL;
    iWord    : INT;
    iBit     : INT;
    bRet     : BOOL;
    bOk      : BOOL;
END_VAR

BEGIN
    bRet := FALSE;
    bOk  := FALSE;

    // Range check on word index
    IF (iWord >= C_MIN_WORD) AND (iWord <= C_MAX_WORD) THEN
        // Range check on bit index
        IF (iBit >= C_MIN_BIT) AND (iBit <= C_MAX_BIT) THEN
            tempDint := Array_Input[iWord];
            bRet     := tempBits[iBit];
            bOk      := TRUE;
        END_IF;
    END_IF;
END_FUNCTION_BLOCK

For SIL-rated code, raise the standard block error handler with SET_ERR from the Program_Alarm library, or call WR_USMSG to write a diagnostic buffer entry that can be evaluated by the HMI alarm log.

Platform-Specific Notes

S7-1500 (Firmware V2.0+)

Full support for AT overlays in VAR_TEMP, VAR, and instance STAT. Slice access (the %X / %B / %W / %D notation) is also fully supported but only with constant indices. The PEEK/POKE pointer instructions are documented in the "S7-1500 System Manual" and the "Programming Guidelines for S7-1500" document referenced from the Siemens SiePortal entry "Access Bit from a word in an array SCL??".

S7-1200 (Firmware V4.0+)

Slice access is also available, with the same constant-only restriction. The PEEK/POKE block is supported but with smaller variant counts; for very compact applications the AT overlay is the most portable solution.

S7-300 / S7-400 (Classic STEP 7)

Slice access is not available. Use AT in VAR_TEMP or any-pointer arithmetic via the legacy P# pointer syntax. The CASE dispatch is also valid but usually replaced by explicit UD DW 1 / UW / OW bit-mask patterns in STL.

Verification Procedure

  1. Open the Watch table in TIA Portal and add the source Array_Input elements. Force Array_Input[1] := 16#0000_0005 (binary ...00000101).
  2. Call the FC/FB from OB1 with iWord := 1 and iBit := 0. The result must be TRUE (bit 0 set).
  3. Repeat with iBit := 2. The result must be TRUE (bit 2 set).
  4. Repeat with iBit := 1. The result must be FALSE (bit 1 cleared).
  5. Force iWord := 41 (out of range). The output must remain FALSE and any error flag set.
  6. Force iBit := 32 (out of range for DINT). The output must remain FALSE.
  7. Use Online & diagnostics → Trace to record the input, the result, and the error flag simultaneously. The trace should show all six test vectors satisfying the expected boolean values.

Troubleshooting Matrix

Symptom Likely Cause Resolution
Compiler rejects .%X#iBit with "must be a constant" Slice index is a variable, not a constant Replace with AT overlay or CASE
Compiler reports temp.x not defined Legacy .x[i] syntax on S7-1500 Use AT overlay or .%X<const>
Runtime: bit value inverted (TRUE ↔ FALSE swapped) Bit-order assumption differs from IEC convention Verify LSB-first mapping; Bit_Index = 0 ↔ mask 16#0000_0001
Runtime: bOk is always FALSE Word index off-by-one; array declared [0..39] but code uses [1..40] Align array lower bound with guard range
Runtime: result looks like adjacent memory Missing bounds check; index read from outside [1..40] Add explicit IF guards; consider READ_DBL for DB consistency
Online change rejected after edit AT overlay size mismatch; new overlay alters block interface Recompile in offline state; re-download entire block; CPU may require STOP if interface changed
Watch table shows wrong value after online edit Watch table references symbolic constant, not symbolic name Re-resolve symbolic address with the toolbar "Update symbol" button

Performance and Code-Size Comparison

Technique Code-size impact Execution time on S7-1516 (typical) Maintainability
AT overlay Small (one block instance) ~0.2 µs High — generic, reusable
CASE dispatch (16 bits) Medium (16 branches) ~0.1–0.3 µs High for fixed bit sets
PEEK + mask Larger (pointer arithmetic) ~0.5 µs Lower — easy to break
Slice with constant index (no indirect addressing) Smallest ~0.05 µs Highest — but does not solve the problem

Why does Array[#i].%X#j fail to compile on S7-1500?

The slice suffix %X requires a compile-time constant for the bit offset. The SCL compiler cannot emit dynamic bit-selection MC7 code from a variable index. Use an AT overlay of ARRAY[..] OF BOOL or a CASE dispatch instead. See the TIA Portal help entry "Addressing areas of a tag with slice access (S7-1200, S7-1500)" for the full syntax rules.

Can I declare an AT overlay inside a UDT in TIA Portal V20?

An AT overlay can be declared inside a UDT provided the overlaid member is an elementary data type of fixed size (e.g., DINT, WORD, BYTE) and both the original and the overlay occupy the same length. AT overlays on STRING, WSTRING, or any variable-length type are rejected. If the UDT form fails, instantiate a small FB that contains the original DINT and the AT overlay as separate VAR members.

Is bit 0 the least significant bit in the AT overlay array?

Yes. Siemens follows the IEC 61131-3 bit-ordering convention, so Bit[0] corresponds to the LSB of the underlying tag and Bit[31] to the MSB of a DINT. This matches the %X0..%X15 slice numbering on a WORD, so you can swap a static slice for the overlay without changing the bit position semantics.

How do I read a bit from a DB array when the array is passed as VARIANT or POINTER?

Use the variant instructions VARIANT_GET / VARIANT_PUT in conjunction with MOVE_BLK into a temporary DINT, then apply the AT overlay on the local copy. For raw POINTER parameters, the PEEK instruction returns a byte at the calculated address; mask the result with 16#01 shifted left by the bit index. Both approaches are described in the S7-1500 system manual under "Extended instructions — Variant operations".

Does the AT overlay work in an FC (function) or only in an FB?

The AT overlay works in both. In an FC the AT declaration must be in the VAR_TEMP section (or in VAR_IN_OUT if the parameter is an IN_OUT tag). In an FB it can additionally be placed in VAR or STAT for an instance-scoped overlay. An AT in VAR CONSTANT or in the function return value is rejected by the compiler.

What is the safest way to validate a runtime word index?

Use explicit IF (iWord >= LOWER_BOUND) AND (iWord <= UPPER_BOUND) guards and a dedicated bOk flag, or wrap the access in a CASE iBit OF dispatch with an ELSE branch that raises a fault. For SIL-rated code, route the fault to a WR_USMSG entry and to the PLC's diagnostic buffer so that the HMI alarm log captures the violation. Always assume that any index received from HMI, OPC UA, or another PLC is potentially out of range.

Back to blog