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.
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.
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
- Open the Watch table in TIA Portal and add the source
Array_Inputelements. ForceArray_Input[1] := 16#0000_0005(binary...00000101). - Call the FC/FB from OB1 with
iWord := 1andiBit := 0. The result must beTRUE(bit 0 set). - Repeat with
iBit := 2. The result must beTRUE(bit 2 set). - Repeat with
iBit := 1. The result must beFALSE(bit 1 cleared). - Force
iWord := 41(out of range). The output must remainFALSEand any error flag set. - Force
iBit := 32(out of range forDINT). The output must remainFALSE. - 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.