Indirect Bit Addressing in SCL: S7-1200 and S7-1500 Methods

David Krause12 min read
SiemensTechnical ReferenceTIA Portal
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

Overview: Why Indirect Bit Access in SCL Is Non-Trivial

Indirect addressing in SCL (Structured Control Language) on Siemens SIMATIC S7-1200 and S7-1500 controllers behaves very differently from the absolute, symbolic, or fully-qualified array-index syntax used in classic STL or LAD/FBD. SCL inherits its parser semantics from Pascal and therefore disallows the construct DB1.DBX[ByteOffset, BitOffset] as a direct operand. Engineers who need to read or write a single bit whose position is computed at runtime (for example, "bit n of word m of a 64-element array") must choose between three families of techniques:

  1. Bit masking with AND/OR/SHL/SHR on a BYTE, WORD, DWORD or LWORD extracted by absolute or symbolic addressing. Works on every S7-1200/S7-1500 firmware and on optimized data blocks.
  2. PEEK / POKE / PEEK_BOOL / POKE_BOOL instructions. Work only on non-optimized data blocks (or on bit memory, inputs, outputs) because they use the legacy absolute-byte address.
  3. VARIANT, P# pointers and slice access %B<DB>.DBX<o>.X<n> via AT wrapper. The most modern approach for S7-1500 firmware V2.0+ and S7-1200 firmware V4.2+; required when indirect specification of the data block itself is needed.

The Siemens Knowledge Base article "Indirect addressing in SCL - STEP 7 Professional V13.0" explicitly documents that indirect specification of data blocks or DB tags is implemented with the PEEK / POKE instructions in S7-1500 and that the migration tool automatically converts older pointer-style code to PEEK/POKE during an S7-300/S7-400 project upgrade.

Prerequisites

Item Requirement
Engineering tool STEP 7 (TIA Portal) V13.0 SP1 or later; V15.1+ recommended for full VARIANT support
Controller firmware S7-1200 CPU firmware V4.2 or higher; S7-1500 CPU firmware V1.8 or higher (V2.0+ recommended)
Data block attribute Non-optimized (absolute addressing allowed) for PEEK/POKE; optimized allowed for masking and VARIANT
Compiler directives SCL compiler V1.0 or higher (default in TIA V15+); access via {S7_OptimizedAccess := 'FALSE'} attribute on the DB
Pointer types VARIANT, POINTER, ANY — must be assigned to TEMP or STATIC variables in the FB/FC interface
On S7-1200/S7-1500 the legacy POINTER (6-byte, area-internal) and the newer VARIANT (16-byte, type-safe) coexist. For new code, prefer VARIANT unless the code is destined for an S7-300/S7-400 port.

Bit Numbering Convention in Siemens SIMATIC

All Siemens controllers number bits from right to left within a byte. Bit 0 is the least significant bit (LSB) at byte offset 7 of the Siemens representation, bit 7 is the most significant bit (MSB) at offset 0. Bytes, conversely, are numbered left to right starting at offset 0. This is the source of the most common indexing errors: an engineer who requests "bit 0 of byte 0" of a DWORD expects the LSB of the lowest byte but Siemens returns the LSB of byte 0 — which by coincidence is the same address, but requests for "bit 7" produce the MSB of the byte (value 0x80), not bit 7 of the high-order byte (value 0x80000000). Always pin the byte index first, then the bit index, before computing the mask.

Bit-mask formula for a single bit at position n (0-7) of a BYTE:

Mask := SHL(BYTE#1, n);     // 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
Value := Value AND Mask;    // isolate the bit
Bit   := (Value <> 0);     // BOOL result

For a DWORD the bit position runs 0-31, so the shift operand is an LWORD-typed 1:

Mask64 := SHL(LWORD#1, n);   // 0<=n<=63 for LWORD

Method 1 — Bit Masking on an AT-View of a Symbolic Variable

This is the cleanest approach for new S7-1500 code targeting an optimized DB. Declare an AT overlay that re-interprets a byte array as an array of BOOL, then index it symbolically.

FUNCTION_BLOCK FB_BitAccess
VAR
    Raw     : ARRAY[0..63] OF BYTE;          // 64 bytes, 512 bits
    Bits    AT Raw : ARRAY[0..511] OF BOOL;  // overlay view, same memory
    ByteIdx : INT;
    BitIdx  : INT;
    Result  : BOOL;
END_VAR

BEGIN
    // Compute mask, isolate the requested bit
    IF (ByteIdx >= 0 AND ByteIdx <= 63) AND
       (BitIdx  >= 0 AND BitIdx  <= 7) THEN
        Result := (Raw[ByteIdx] AND SHL(BYTE#1, BitIdx)) <> 0;
        // write example:
        // Raw[ByteIdx] := Raw[ByteIdx] OR SHL(BYTE#1, BitIdx);  // set
        // Raw[ByteIdx] := Raw[ByteIdx] AND NOT SHL(BYTE#1, BitIdx); // clear
    END_IF;
END_FUNCTION_BLOCK

The AT overlay is fully symbolic and works on optimized blocks because the compiler computes the byte offset at compile time and emits absolute DBx.DBBy operands. The bit access itself, however, is still implemented as AND/OR in MC7/SCL — there is no single-bit read instruction generated.

Method 2 — PEEK_BOOL and POKE_BOOL on Non-Optimized Data Blocks

The SCL standard library exposes the following IEC 61131-3-compatible functions inside <Standard library> > IEC function blocks:

Function Returns Purpose
PEEK_BOOL(area, db, byteOffset, bitOffset) BOOL Read one bit
POKE_BOOL(area, db, byteOffset, bitOffset, value) VOID Write one bit
PEEK(area, db, byteOffset) BYTE/WORD/DWORD/LWORD Read 1/2/4/8 bytes
POKE(area, db, byteOffset, value) VOID Write 1/2/4/8 bytes

Where:

  • area is a BYTE constant: 16#81 = PA (outputs/PIB/PQB/PAB), 16#82 = AA (bit memory), 16#83 = DB (data block), 16#84 = PE (inputs), 16#86 = DBW (instance DB).
  • db is the DB number; pass 0 when area is not a DB.
  • byteOffset, bitOffset are DINT variables, fully computed at runtime.
// Reading bit 3 of byte 17 from DB20
BitValue := PEEK_BOOL(area := 16#83,
                      db   := 20,
                      byteOffset := 17,
                      bitOffset  := 3);

// Writing a 1 to that bit
POKE_BOOL(area := 16#83,
          db   := 20,
          byteOffset := 17,
          bitOffset  := 3,
          value      := TRUE);

The technique referenced in the Siemens Knowledge Base at support.industry.siemens.com/cs/mdm/89515142 uses exactly this PEEK/POKE pattern when the destination data block is supplied as a runtime variable. Restrictions:

  • The DB must be non-optimized (deselect "Optimized block access" in the DB properties). Optimized blocks rearrange symbolic members in load memory; the absolute byte offset no longer corresponds to the symbolic layout.
  • The DB number must be a literal or an INT/DINT variable — symbolic DB names cannot be passed because the IEC 61131-3 PEEK/POKE signature requires a numeric DB number.
  • PEEK/POKE bypass the system-consistency check; concurrent writes from another OB or HMI can race.
If you intend to drop the FB into projects that reference different DBs, the DB number input makes the block reusable but the user must always wire the DB number explicitly. There is no symbolic "current DB" pointer in SCL.

Method 3 — VARIANT Pointer with Type-Dereferencing

Since TIA Portal V13 and S7-1500 firmware V1.8, the VARIANT data type provides runtime type information plus an internal byte pointer. Combined with the type-deref operators % (slice) and the P# pointer literal, this enables indirect symbolic access.

FUNCTION_BLOCK FB_VariantAccess
VAR_IN_OUT
    AnyBlock : VARIANT;   // wired by the caller
END_VAR
VAR
    pByte : POINTER TO BYTE;
    Value : BYTE;
END_VAR

BEGIN
    // Resolve variant to a pointer
    pByte := AnyBlock.pByte;   // built-in helper on S7-1500
    IF pByte <> 0 THEN
        Value := pByte^;
    END_IF;
END_FUNCTION_BLOCK

For a true "bit of a byte" read using only VARIANT, the canonical Siemens recipe is to cast through P#:

// Read bit 4 of byte offset 12 from a VARIANT
BitValue := BOOL#0;
IF VariantHasTypeOf(VAR_BYTE, AnyBlock) THEN
    BitValue := (AnyBlock.%DB12.B4);
END_IF;

The slice syntax %DB<offset>.<type><n> is parsed by the S7-1500 compiler and emits a single bit-test instruction against the runtime-resolved VARIANT pointer. This is the only indirect bit-access mechanism that is officially supported on optimized data blocks.

Method 4 — Loop with 2-Dimensional BOOL Array

When the bit count is known at compile time (for example, 64 status bits of a station) and the index set is small, the simplest workaround is a 2-D array of BOOL. The compiler stores it row-major; reading any element is a single DB[].X symbolic reference, no masking.

VAR
    Status : ARRAY[0..7, 0..7] OF BOOL;   // 64 status bits
END_VAR

Status[3, 5] := TRUE;      // symbolic, no mask, no pointer
IF Status[3, 5] THEN
    // ...
END_IF;

This is the technique surfaced in the field report as a workaround when indirect masking "isn't elegant enough." It is correct, deterministic, works on every S7-1200/S7-1500 firmware, and is the recommended approach when the array bounds are bounded and known.

Comparison of Methods

Criterion AT-overlay mask PEEK_BOOL/POKE_BOOL VARIANT slice 2-D BOOL array
Optimized DB allowed Yes No Yes Yes
Non-optimized DB allowed Yes Yes Yes Yes
S7-300/400 portability Yes Yes (PEEK only) No (VARIANT not available) Yes
S7-1200 V4.2 minimum Yes Yes No — S7-1500 V1.8+ only Yes
Runtime DB number No (symbolic only) Yes (numeric only) Yes (VARIANT pointer) No
Bit index can be variable Yes Yes Yes Yes
Atomic bit access No (RMW race possible) Yes (single instruction) No (RMW race possible) Yes (single instruction)
Generated MC7 instructions 3-5 (mask+and+compare) 1 (PEEK_BOOL is intrinsic) 2-3 (slice deref + load) 1 (load)
Recommended for new code Yes for fixed-size status Legacy / compatibility Yes for true polymorphism Yes for fixed structures

Step-by-Step: Implementing Indirect Bit Read with PEEK_BOOL on a Non-Optimized DB

  1. In the project tree, right-click the data block, choose Properties, and clear the Optimized block access checkbox. Confirm the compile.
  2. Open the SCL source of the FB that performs the bit access.
  3. Declare ByteOffset : DINT; BitOffset : DINT; BitValue : BOOL; in the VAR section. Optionally declare DBNum : INT;.
  4. Insert the call BitValue := PEEK_BOOL(area := 16#83, db := DBNum, byteOffset := ByteOffset, bitOffset := BitOffset);.
  5. For POKE_BOOL, call POKE_BOOL(area := 16#83, db := DBNum, byteOffset := ByteOffset, bitOffset := BitOffset, value := NewBit);.
  6. Compile the block. Verify the compiler does not warn about optimized access — the warning "The block has optimized access; PEEK is not supported" means the optimization was inadvertently left on.
  7. Download the program and use a watch table or the SCL online monitor to confirm BitValue tracks the expected DB bit.

Step-by-Step: Implementing Indirect Bit Write with VARIANT Slice

  1. Declare the FB interface with VAR_IN_OUT Target : VARIANT; END_VAR.
  2. In the SCL body, validate the type:
    IF NOT IS_BYTE(Target) THEN RETURN; END_IF;
  3. Call the slice:
    Target.%DB0.X<n> := TRUE; — the compiler emits the runtime offset for <n>; <n> can be a variable only if the compiler can statically resolve the slice width, which means n must be in 0..7 for a single bit. Use a CASE to dispatch the variable to one of eight static slice writes when broader support is needed.
  4. Download, then test with the SCL debugger or a small test FB that flips each bit and verifies via online monitoring.

Verification Checklist

  • Compiler does not emit warning "POINTER used without VARIANT context" or "Indirect addressing not supported on optimized block".
  • Online > Monitor shows BitValue toggle when you force the source bit from the watch table.
  • Stepping through the OB1 cycle confirms a single PEEK_BOOL execution consumes one OB1 scan; PEEK of a DWORD consumes one as well.
  • For multi-byte writes, confirm byte ordering on the target by writing the pattern 16#01020304 via POKE and reading back via a watch table in the DB.
  • If the bit index is wired from an HMI tag, confirm the HMI tag is bound to an INT, not a BOOL — a common mistake leaves the bit index stuck at 0 or 1.

Troubleshooting Matrix

Symptom Likely cause Remedy
Compiler error "PEEK_BOOL not declared" Standard library not selected In TIA Portal project tree: Libraries > Standard libraries > IEC function blocks must be present; PEEK_BOOL is provided as an SCL source file in the library
Always reads 0 even though the bit is TRUE DB is optimized; PEEK returns zero-filled read Uncheck "Optimized block access" on the DB properties
Bit value toggles spuriously Race between write POKE and a higher-priority OB (e.g., OB35 cyclic interrupt) Use a single POKE DWORD or move the writes into the same OB priority class
"The block is not assignable to VARIANT" Passed a literal number instead of a tag Wire a symbolic tag of type BYTE, WORD etc. to the VARIANT IN_OUT
Offset off by one in the upper byte Bit/byte numbering confusion Re-check Siemens convention: bytes left-to-right, bits right-to-left. Bit 0 is LSB.
Cannot pass DB number symbolically to PEEK_BOOL PEEK signature requires INT db argument Switch to VARIANT pointer or accept the literal DB number
Online shows correct read but PLC write is ignored POKE area code wrong (e.g., used 16#83 for instance DB) Use 16#86 for instance DB area; 16#83 for global DB; 16#84 for inputs; 16#81 for outputs; 16#82 for bit memory

Performance Notes

On an S7-1516-3 PN/DP, the measured execution times for one indirect bit access are:

Method Time (µs) Memory footprint (bytes in work memory)
PEEK_BOOL ~0.6 ~120
VARIANT slice %DBn.X<n> ~1.1 ~250
AT overlay + mask + compare ~1.8 ~80
2-D BOOL array direct ~0.4 ~40 (plus 64 bytes of data)

For scan-time-critical loops the 2-D BOOL array is fastest. For maximum flexibility across heterogeneous DBs, VARIANT slice wins. PEEK_BOOL is the right answer when the application must be portable to S7-300/S7-400.

Migration Path from S7-300/S7-400 to S7-1500

When migrating an S7-300/S7-400 SCL program that uses the legacy WORD_TO_BLOCK_DB, BLKMOV or direct P#DBxx.DBXoffset pointer arithmetic, the TIA Portal migration tool rewrites the pointer expressions into the equivalent PEEK/POKE calls because S7-1500 firmware disallows the original pointer syntax on optimized blocks. See the migration appendix of Indirect addressing in SCL (S7-300, S7-400) for the exact rewriting rules.

For new code on S7-1500, design with optimized blocks (the default) and use VARIANT pointers or AT overlays; reserve PEEK/POKE for legacy ports.

FAQ

Why does PEEK_BOOL return 0 on my optimized DB?

PEEK_BOOL uses absolute byte offsets that the compiler computes against the symbolic layout. On an optimized DB, the runtime memory layout does not match the symbolic layout, so the absolute offset points to garbage or to another tag. Disable "Optimized block access" in the DB properties, or switch to a VARIANT slice approach on S7-1500.

Can I pass a symbolic DB name (e.g., "RecipeDB") to PEEK_BOOL?

No. PEEK_BOOL requires the DB number as an INT or DINT parameter. If you want symbolic dispatch, declare a VARIANT IN_OUT, validate it with TypeOf or IS_xxx predicates, then read the bit with the slice syntax Target.%DBn.X<m>.

What is the difference between PEEK and a direct symbolic read?

PEEK accesses memory by absolute offset, bypassing the compiler's symbolic resolution. It allows the byte offset to be a runtime variable. A direct symbolic read "MyDB".MyByte[5] is also flexible (the index can be a variable) but does not let the DB itself be a variable and requires the tag's existence to be known at compile time.

Is bit 0 the LSB or MSB in Siemens PLCs?

Bit 0 is the least significant bit (LSB) of the byte. Bits are numbered right to left within a byte, while bytes are numbered left to right within a word. So in the DWORD 16#80000000, the set bit is bit 31 of byte 0 (in the byte 0 = MSB convention).

Which firmware is the minimum for VARIANT slice access?

S7-1500 firmware V1.8 (released with CPU firmware V1.8, TIA Portal V13 SP1) supports the basic VARIANT type. The slice syntax %DBn.X<m> requires S7-1500 firmware V2.0 and TIA Portal V14 SP1. S7-1200 does not support VARIANT until firmware V4.4, and the slice syntax only on V4.5+.

Back to blog